Skip to content

[BUG] Emergency condense emits incomplete condense_context event - breaks rewind and hides errors #1769

Closed

Description

@DaubnerF

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.

  1. Set autoCondenseContext = false (Settings, Context Management) so proactive condensing never interferes, and pick a small-context model or a long-running task.
  2. 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.
  3. Delete the emergency condense's own condense_context chat 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.)
  4. Variation for the second symptom: repeat with a provider whose summarization call fails (a rate-limited key, for example). Code-derived: no condense_context_error row 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:

  1. condenseId missing from the event. The emergency path builds ContextCondense = { summary, cost, newContextTokens, prevContextTokens } without a condenseId (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 collects condenseIds from condense_context events (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.

  2. Error dropped. manageContext returns error/errorDetails on failure (src/core/context-management/index.ts:374-380, returned at index.ts:413-426). The automatic path surfaces them with say("condense_context_error", ...) (Task.ts:4750-4752); the emergency block checks only summary and truncationId (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:

  1. It omits customCondensingPrompt, filesReadByRoo, and cwd. An emergency condense therefore silently uses the default CONDENSE prompt even when the user configured a custom one (which the automatic path reads at Task.ts:4582) and skips the folded file context the other paths attach.

  2. It passes profileThresholds/currentProfileId (Task.ts:4478-4479) alongside the forced autoCondenseContextPercent of 75, and manageContext lets 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 includes condenseId, matching the manual and automatic paths.
  • When manageContext returns an error, the emergency path emits say("condense_context_error", error) like the automatic path.
  • Regression tests driving the real handleContextWindowExceededError (not a mock of it): a successful forced condense emits a contextCondense.condenseId equal to the summary message's condenseId, and a condense error emits a condense_context_error say.
  • A rewind-level test (the src/core/message-manager/ test style) proving an emergency-created summary is removed when rewinding past its condense_context event.
  • 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.

  1. 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).
  2. Should profileThresholds be 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 explicit forceThreshold flag to manageContext. 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.
  3. 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:

Activity

  1. PierrunoYT commented on Sep 28, 2026

    @PierrunoYT
    Contributor

    Working on this!

  2. added a commit that references this issue on Nov 5, 2026
    4d1a543
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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