Skip to content

Roo Code 3.54.0 - Bug Report: a single failing operation aborts a whole batch and prevents the UI refresh #1262

Description

@attack444

Roo Code 3.54.0 — Bug Report: a single failing operation aborts a whole batch and prevents the UI refresh

Found during a full audit of the webview ↔ extension message handlers in the bundled/minified dist/extension.js.
Affected extension: rooveterinaryinc.roo-cline-3.54.0.

Summary

Several flows batch persistence operations with Promise.all(...) and no error handling. When one item rejects:

  1. the remaining items in the batch are silently never executed (e.g. other settings never get written to globalState),
  2. the code after the batch is skipped (e.g. postStateToWebview(), initialize(), setModeConfig(...), provider-profile-change emit) — so the UI does not refresh and the extension state is left inconsistent.

Users observe this as "settings are not saved" / "UI did not update" after a single bad value/file/mode.

Fix strategy: replace the aborting Promise.all(...) with Promise.allSettled(...) + per-item error logging, or (where the batch result is used, e.g. image data URLs) per-item try/catch + filter.


The 6 bugs and their fixes

1. updateSettings (webview handler)

Trigger: one failing workspace-config update (e.g. allowedCommands / deniedCommands) aborts the whole for loop, so checkpointTimeout, soundEnabled, … are never written to globalState, and postStateToWebview() is never called → settings appear "not saved".

Before:

case"updateSettings":if(e.updatedSettings){for(let[S,Z]of Object.entries(e.updatedSettings)){let V=Z;
// ... resolve V ...
await t.contextProxy.setValue(S,V)}await t.postStateToWebview()

After: each iteration wrapped in try { … } catch { console.error(...) }; the loop always completes and postStateToWebview() always runs.

case"updateSettings":if(e.updatedSettings){for(let[S,Z]of Object.entries(e.updatedSettings)){try{let V=Z;
// ... resolve V ...
await t.contextProxy.setValue(S,V)}catch(RoErr){console.error("Roo Code: updateSettings failed for key '"+S+"':",RoErr)}}await t.postStateToWebview()

2. ContextProxy.setValues (batch persistence primitive)

Location: minified index ≈ 12618341.

Trigger: setValues is the shared primitive used by setConfiguration, deleteProviderProfile, createTask, importSettings, setProviderSettings, activateProviderProfile. One failing setValue rejects the whole Promise.all, so none of the subsequent values are written and every caller that does await setValues(...); postStateToWebview() skips the refresh.

Before:

async setValues(e){let r=Object.entries(e);await Promise.all(r.map(([a,o])=>this.setValue(a,o)))}

After:

async setValues(e){let r=Object.entries(e);(await Promise.allSettled(r.map(([a,o])=>this.setValue(a,o)))).forEach(o=>{if(o.status==="rejected")console.error("Roo Code: setValues failed for a key:",o.reason)})}

3. activateProviderProfile (API provider profile switch)

Location: minified index ≈ 14729861.

Trigger: profile switch runs Promise.all over 3 persistence calls (listApiConfigMeta, currentApiConfigName, setProviderSettings). One failure skips the subsequent setModeConfig(...), postStateToWebview() and the providerProfileChanged event → UI shows stale provider.

Before:

await Promise.all([this.contextProxy.setValue("listApiConfigMeta",await this.providerSettingsManager.listConfig()),this.contextProxy.setValue("currentApiConfigName",o),this.contextProxy.setProviderSettings(n)]);let{mode:h}=await this.getState();

After:

(await Promise.allSettled([this.contextProxy.setValue("listApiConfigMeta",await this.providerSettingsManager.listConfig()),this.contextProxy.setValue("currentApiConfigName",o),this.contextProxy.setProviderSettings(n)])).forEach(I=>{if(I.status==="rejected")console.error("Roo Code: activateProviderProfile failed to persist a value:",I.reason)});let{mode:h}=await this.getState();

4. resetAllState ("Reset State" / factory reset)

Location: minified index ≈ 12618638.

Trigger: reset deletes every globalState key and secret via one Promise.all. One failing globalState.update(...)/secrets.delete(...) aborts the whole reset and initialize() is never called → partially cleared, inconsistent state.

Before:

async resetAllState(){this.stateCache={},this.secretCache={},await Promise.all([...vye.map(e=>this.originalContext.globalState.update(e,void 0)),...Ck.map(e=>this.originalContext.secrets.delete(e)),...wk.map(e=>this.originalContext.secrets.delete(e))]),await this.initialize()}

After:

async resetAllState(){this.stateCache={},this.secretCache={},(await Promise.allSettled([...vye.map(e=>this.originalContext.globalState.update(e,void 0)),...Ck.map(e=>this.originalContext.secrets.delete(e)),...wk.map(e=>this.originalContext.secrets.delete(e))])).forEach(o=>{if(o.status==="rejected")console.error("Roo Code: resetAllState failed to clear a value:",o.reason)}),await this.initialize()}

5. importSettings → custom modes import

Location: minified index ≈ 14199255.

Trigger: importing settings first imports all custom modes via Promise.all(...updateCustomMode(...)). One malformed custom mode rejects the batch, so no custom mode is imported and — worse — the subsequent e.import(A) (tasks/settings import) and r.setValues(h) never run → the whole "Import Settings" flow fails because of a single bad custom-mode entry.

Before:

await Promise.all((h.customModes??[]).map(E=>a.updateCustomMode(E.slug,E))),await e.import(A),await r.setValues(h);

After:

(await Promise.allSettled((h.customModes??[]).map(E=>a.updateCustomMode(E.slug,E)))).forEach(o=>{if(o.status==="rejected")console.error("Roo Code: importSettings failed to import a custom mode:",o.reason)}),await e.import(A),await r.setValues(h);

6. selectImages (image picker → data URLs)

Location: minified index ≈ 14650972.

Trigger: converting the selected images to base64 data URLs runs Promise.all(e.map(...readFile...)). One unreadable file rejects the whole batch → selectedImages is never sent to the webview → the user loses all selected images because of one bad file.

Before:

return!e||e.length===0?[]:await Promise.all(e.map(async r=>{let a=r.fsPath,s=(await Ypi.default.readFile(a)).toString("base64");return`data:${tRs(a)};base64,${s}`}))}

After: per-item try/catch, failed files are skipped (null filtered out), good images are still returned.

return!e||e.length===0?[]:await Promise.all(e.map(async r=>{try{let a=r.fsPath,s=(await Ypi.default.readFile(a)).toString("base64");return`data:${tRs(a)};base64,${s}`}catch(RoErr){return console.error("Roo Code: selectImages failed to read an image:",RoErr),null}})).then(o=>o.filter(Boolean))}

Files attached

File Purpose
roo-cline-3.54.0-extension-fixed.js The fully patched, node --check-validated dist/extension.js bundle (drop-in replacement)
patch-extension.mjs Patch script for bug 1 (updateSettings)
patch-audit-fixes.mjs Patch script for bugs 2–6 (idempotent, with pre-flight checks)

Verification performed on the installed bundle:

  • all 6 patch markers present (exact substring match),
  • node --check on the patched bundle → syntax OK,
  • file size: original 14 806 332 B → patched 14 807 051 B,
  • each needle appears exactly once; re-running the scripts reports "already applied" and skips.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions