Skip to content

Compaction paths can wipe the entire chat history instead of compacting it #1166

Description

@nordicnode

While investigating a user report ("Model losing all chat history instead of compacting"), I found two verified mechanisms in the compaction paths that replace the model's entire context with nothing. Both reproduce against the current code; repro details below.

1. /compact with an empty model response wipes the history

packages/agent-runtime/src/run-agent-step.ts:562-574 replaces the whole message history with a single summary message built from fullResponse:

agentState.messageHistory = [
  userMessage(withSystemTags(`The following is a summary of the conversation between you and the user. ...:\n\n${fullResponse}`)),
]

There is no check that fullResponse is non-empty. A silent empty stop (finish part with a real reason and usage but no content) yields no recovery chunk — classifyStreamEndRecovery returns null for it (sdk/src/impl/stream-interruption.ts) — so nothing catches it. I reproduced the full shape with a mocked stream: fullResponse === "", zero recovery/error chunks, and the replacement message is 'The following is a summary of the conversation...:\n\n' — i.e. the model's next turn starts with an empty summary and no earlier turns. Near the context limit (exactly when a user reaches for /compact) empty completions are also the most likely, so the trigger lines up with the report.

2. A user message quoting the summary markers steals the summary's identity and erases the older memory

compactMessages (packages/agent-runtime/src/compact-history.ts) recognizes its own summary by CONTENT: a user-role message containing <conversation_summary> and the header sentence. Two consequences:

  • previousSummary = messages.findLast(isConversationSummary) — the LAST match wins.
  • isRealHistory excludes anything matching, so the matched message is never summarized.

So a later user message that merely quotes both markers (asking about this very mechanism, pasting a summary back) is taken for the real summary; extractSummaryContent on the quote returns '' (no closing tag) or the quoted text; the REAL summary is neither re-parsed nor kept as history. Every earlier turn vanishes from the model's context at that compaction. Reproduced with a 3-generation compactMessages chain: generation 3's summary retained none of generations 1-2 once the quoting message was present (previous_summary_entry_count: 0).

The inlined copy in agents/context-pruner.ts (still live for the base2 family — it is spawned before every step there) matches on the bare tag ALONE, so a user message that merely mentions <conversation_summary> triggers the same wipe there, and set_messages adopts it with no validation (packages/agent-runtime/src/tools/handlers/tool/set-messages.ts:20).

Suggested fix

  1. Guard the /compact replacement: only replace when fullResponse.trim() is non-empty; otherwise keep the history (the forced next step retries the summary anyway) and log.
  2. Identify summaries by provenance, not content: stamp the summary message with a CONVERSATION_SUMMARY tag in both buildSummaryMessage (compact-history.ts) and the pruner's copy, and have isConversationSummary require the tag, with a legacy fallback that requires the FULL envelope (open+close tag, header, <historical_memory>) so pre-tag summaries still fold in. A user message can then never gain summary identity by content alone. (The parity test's deliberate divergence — pruner matching the bare tag — closes too.)

Happy to open a PR with both, plus regression tests.

Related (same symptom family, separate cause, not covered by the fix above)

On desktop/CLI resume, an unreadable run-state.json falls back to a RunState with no sessionState (cli/src/utils/run-state-storage.ts:518-549), so the UI transcript survives but the model starts amnesiac next turn — worth checking in telemetry if reports continue after the compaction fixes.

Activity

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

    area:cliThe Codebuff/Freebuff terminal clientbot:triagedClassified by the community triage bottype:bugA defect in the code with a reproducible failure

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions