Repository navigation
Description
Problem
When a request fails because the context window is exceeded, the task runs an emergency condense to shrink the context, but that path reports its result incompletely: the UI event is missing the summary's condenseId, so the carried id-based rewind cleanup can never remove the stale summary and its fate falls to the timestamp filter - a cutoff after the summary's timestamp, deleting the emergency condense's own chat row being the direct one, leaves it in place; the condense_context_error row the automatic path emits never appears on the emergency path - when the sliding-window fallback fires (src/core/context-management/index.ts:384-423 -> truncationId -> src/core/task/Task.ts:4502-4521) the user sees only a truncation row, not the failure; when it does not fire nothing is reported and the request retries blind into the same wall.
Context (who is affected and when)
Any user whose requests hit the provider's context window limit: long tasks, small-context models. The id-based cleanup can never match an emergency-created summary (the event carries no condenseId), so whether a rewind removes it comes down to the timestamp filter alone (message-manager/index.ts:182): everything older than the cutoff survives, and the summary sits just past the last message at condense time (src/core/condense/index.ts:456-461) - so rewinding to any checkpoint before the condense, the triggering request included, removes it and the pre-condense history returns. The sticky class is a cutoff that lands after the summary's timestamp: the cutoff is the target message's own timestamp, and the skew correction the code documents at index.ts:140-146 can only move it later, so deleting the emergency condense's own chat row - the action a user takes specifically to undo it - cuts after the summary and leaves it anchored while the pre-condense history stays hidden. Users discover this as "I rewound but the agent still talks about the summary." The second symptom hits the same population whenever the emergency summarization call itself errors (provider hiccup, rate-limited key): the condense_context_error row the automatic path emits never appears, the request retries up to 3 times (MAX_CONTEXT_WINDOW_RETRIES) and fails again, which looks like a hang.
Both symptoms share one root cause: the emergency path duplicates the automatic path's result handling without sharing code, and the copies have diverged. One issue for both.
Reproduction steps
The observations below are about what the task's API history contains after each step, which is visible in the request the next turn sends; the code paths cited later pin down exactly what to look for.
- Set
autoCondenseContext = false(Settings, Context Management) so proactive condensing never interferes, and pick a small-context model or a long-running task. - Keep interacting until the provider rejects a request with a context-window error. The output logs
Context window exceeded for model ... Forcing truncation to 75%(src/core/task/Task.ts:4419-4422) and the emergency condense runs. - Delete the emergency condense's own
condense_contextchat row. Code-derived: the chat row disappears, but the API history keeps the summary - the deletion cuts the history after the summary's timestamp, and the id-based cleanup has no id to match on - so subsequent requests still start from## Conversation Summary, and the pre-condense messages do not return. (Rewinding to a checkpoint from before the condense, by contrast, does remove the summary and bring the pre-condense history back; the sticky class is the cutoff-after-summary one, and deleting the row is the user action that produces it.) - Variation for the second symptom: repeat with a provider whose summarization call fails (a rate-limited key, for example). Code-derived: no
condense_context_errorrow appears in the chat; when the sliding-window fallback does not fire, the request retries silently and dies.
Expected result
The emergency condense behaves like the manual and automatic paths: rewinding past it removes the summary, and a failed condense shows a condense_context_error row.
Actual result
The summary survives the deletion (the id-based cleanup never matches an emergency-created summary, and cutting the history at the deleted row's timestamp keeps everything older than the cut, the summary included); the error returned by manageContext is discarded; no condense_context_error row appears, and unless the truncation fallback fires nothing at all is shown.
App Version
3.82.2 (src/package.json:6). All code references in this issue are permalinks to main at f7806475331fcae5f4e8b5558d04415eeb5da88c (extension version 3.82.2).
Code path (verified at f7806475331fcae5f4e8b5558d04415eeb5da88c)
Two of the three condense triggers enter through manageContext: attemptApiRequest() (called at Task.ts:4727) and the emergency recovery path handleContextWindowExceededError() (src/core/task/Task.ts:4390, called at Task.ts:4468); the manual condenseContext() runs the same summarize-and-report sequence directly on summarizeConversation (Task.ts:1955). The emergency path duplicates the automatic path's result-handling block without sharing code, and the copies have silently diverged. It is missing two statements the other paths have:
-
condenseIdmissing from the event. The emergency path buildsContextCondense = { summary, cost, newContextTokens, prevContextTokens }without acondenseId(src/core/task/Task.ts:4489-4491). The manual path includes one (Task.ts:1987-1993), and so does the automatic path (Task.ts:4754-4761). That id is load-bearing: rewind cleanup collectscondenseIds fromcondense_contextevents (src/core/message-manager/index.ts:99-122) to find and delete the matching summary message (index.ts:184-193). An event without an id leaves its summary beyond the reach of the id-based cleanup: it stays anchored as the model's fresh-start context for every cutoff after its timestamp - including the cutoff produced by deleting its own event row. -
Error dropped.
manageContextreturnserror/errorDetailson failure (src/core/context-management/index.ts:374-380, returned atindex.ts:413-426). The automatic path surfaces them withsay("condense_context_error", ...)(Task.ts:4750-4752); the emergency block checks onlysummaryandtruncationId(Task.ts:4489-4521) and discards them. The request then retries into the same context-exceeded wall.
Two more divergences sit in the emergency call's arguments (Task.ts:4468-4483), verified in the same read:
-
It omits
customCondensingPrompt,filesReadByRoo, andcwd. An emergency condense therefore silently uses the default CONDENSE prompt even when the user configured a custom one (which the automatic path reads atTask.ts:4582) and skips the folded file context the other paths attach. -
It passes
profileThresholds/currentProfileId(Task.ts:4478-4479) alongside the forcedautoCondenseContextPercentof 75, andmanageContextlets a valid profile threshold override the passed percent (context-management/index.ts:333-349). A profile pinned to 95 turns "force condense at 75%" into "condense at 95% or later", defeating the recovery reduction on the one path that exists because the local token estimates just proved wrong.
Divergences 3 and 4 are verified behavior, but whether each should change is a judgment call; they are argued under Open questions below. Divergences 1 and 2 are the defect.
Nothing in the current test suite catches any of this: two of the tests that exercise this path through the retry loop mock the emergency method out (Task.spec.ts:3843, :3916), and the three that exercise it for real assert only on surrounding behavior - condense-call metadata (Task.spec.ts:3685-3791), prompt and snapshot threading (:3945-3989, and the one direct call :1155-1204) - so no test pins the contents of the emitted event. That gap is how the drift shipped.
Suggested fix direction (recommendations, not part of the defect)
The minimal fix is two statements: include condenseId in the emergency event, and mirror the automatic path's error say. That closes the bug but leaves the duplicated block in place, with four divergences from the automatic path's handling now accumulated. My recommendation is to converge the three paths on one shared handler, rather than patch this instance again: a single helper called by all three sites (condenseContext(), attemptApiRequest(), handleContextWindowExceededError()) that owns both argument assembly from task-local state and result reporting to the webview (something like buildContextManagementArgs(...) plus emitContextManagementResult(result), or one manageTaskContext(...) wrapper). With one owner, a field can no longer be present in two paths and missing in the third.
Acceptance criteria
- The emergency path's
say("condense_context", ...)payload includescondenseId, matching the manual and automatic paths. - When
manageContextreturns an error, the emergency path emitssay("condense_context_error", error)like the automatic path. - Regression tests driving the real
handleContextWindowExceededError(not a mock of it): a successful forced condense emits acontextCondense.condenseIdequal to the summary message'scondenseId, and a condense error emits acondense_context_errorsay. - A rewind-level test (the
src/core/message-manager/test style) proving an emergency-created summary is removed when rewinding past itscondense_contextevent. - If the shared-helper direction is taken: no behavior change on the manual or automatic paths beyond the refactor.
Open questions
Three scoping decisions for the fix. None of them is about whether the bug above is real.
- Should the emergency path honor the user's custom CONDENSE prompt, folded file context, and
cwd(divergence 3), like the other two paths? I recommend yes. A user who set a custom prompt expects the same summary structure whether condensing fired proactively or in recovery, and the emergency path is the one least able to afford lost file continuity: it fires exactly when the task is at its largest. A shared argument builder (see the suggested helper) delivers this mechanically; the only argument for keeping the payload minimal during recovery is weak anyway, since the folded context is already capped at 50,000 characters (src/core/condense/foldedFileContext.ts:80). - Should
profileThresholdsbe able to override the forced 75% threshold during recovery (divergence 4)? Today it can. I recommend that recovery wins: pass an empty threshold map on the emergency call, or add an explicitforceThresholdflag tomanageContext. In the common case, the provider has just rejected a request the extension had sized below the configured threshold; that is exactly when trusting a pinned profile value, possibly 95, undermines the one mechanism built for unreliable estimates. - How much belongs in the first PR? The two defect criteria, plus optionally just the result-reporting half of the shared helper, stand alone as a pure bugfix with no behavior change beyond the fixed symptoms. Divergences 3 and 4 are the only behavior-changing items and can ship as a separate follow-up. Up to triage which split the team prefers.
Scope
Out of scope either way: the CONDENSE prompt template's content, the trigger math for the manual and automatic paths, and i18n.
Related issues
I could not find this defect reported in an existing issue: searches for condenseId, condense_context_error, and handleContextWindowExceeded in issue bodies and comments returned nothing relevant as of 2026-09-24. The closest neighbors, all around the same recovery path:
- [BUG] Un-controllable Token Exceed with MCP results #648, [BUG] Bound native read_file output by the active context budget #1252, [BUG] Bound MCP tool results before adding them to model history #1253, [BUG] Ensure context truncation always makes measurable progress #1254 cover the condense-failure neighborhood; [BUG] Ensure context truncation always makes measurable progress #1254 is the inverse observability defect (truncation reports success with zero messages removed), and this issue is the other half of "recovery-path failures must be observable."
- [BUG] OpenAI Compatible Qwen3.8:27B condense context token usage #1342 (condense request token inflation) shows visible condense errors in its report, which is precisely what the manual and automatic paths surface and the emergency path drops.
- [BUG] new_task delegation permanently broken after child sub-task context exhaustion return #887 (fixed via feat(subtasks): abandon interrupted subtask — sever parent-child link and surface delegation status #935) and [BUG] Many times, it lost my latest session content #1279 (fixed) cover lifecycle breakage after interrupted or terminated runs, through a different mechanism.
- The rewind-desync class was fixed upstream (Roo Code issue 8295), and this codebase carries that fix:
MessageManager's id-based summary cleanup (src/core/message-manager/index.ts:99-122,index.ts:184-193) with a regression suite (src/core/condense/__tests__/rewind-after-condense.spec.tstitled for #8295). The defect here is narrower: the emergency path emitscondense_contextevents the carried fix cannot process, because they carry nocondenseId.
Activity
PierrunoYT commentedon Sep 28, 2026
Working on this!
fix(task): report emergency condense like the automatic path (Zoo-Cod…