Repository navigation
Compaction summaries compound instead of replacing: context explodes and sessions 400 to death #1003
Description
Summary
Compaction summaries compound instead of replacing. Each compaction entry's summary embeds the previous summaries in full, so the stack grows geometrically with every compaction. The stack dominates the resumed context, the context meter explodes past the model limit, compaction can no longer reduce it (each pass adds another layer), and every request is rejected with a 400. The session is dead, and restarting does not help because the transcript survives it.
Observed session
Session 3e33503f (hermes-agent issue #134107 work), 2026-10-07 11:44 → 2026-10-08 08:33 UTC. Seven compactions. The summary field of each, from ~/.commandcode/projects/.../3e33503f-...jsonl (3.69MB, 355 entries):
| # | Time (UTC) | summary chars | tokensBefore | firstKeptEntryId |
|---|---|---|---|---|
| 1 | 11:44 | 3,037 | 128,133 | 98e83e22 |
| 2 | 21:02 | 48,202 | 187,903 | 8c65e93a |
| 3 | 22:57 | 88,308 | 90,398 | 8c65e93a |
| 4 | 06:30 | 216,724 | 151,462 | f350ffb1 |
| 5 | 08:21 | 345,202 | 140,818 | e920fd9a |
| 6 | 08:31 | 582,078 | 197,749 | f8f26f11 |
| 7 | 08:33 | 1,144,246 | 321,348 | f8f26f11 |
Totals: 2,427,797 chars of stacked summaries, 66% of the transcript file. The largest summary contains the "use clean-writing skill" text 30 times and the "Goal" header 10 times: prior summaries re-embedded verbatim. Compactions 2/3 and 6/7 share an identical firstKeptEntryId. The second of each pair re-summarized a range the first had already summarized.
Symptoms, in the order they were hit:
- Compaction #6 reported "saved ~5.3k tokens" (meta.json
lastTokensSaved: 5335). Almost nothing was left to save, because almost everything was already summary text. - Compaction #7 doubled the meter:
~648.8K / 256K used (100%). - Every request then failed:
Error: 400 Bad Request, thenError: 400 The stealth model could not complete the request.The agent stopped with "Invalid request / Something went wrong".
The session is unrecoverable in place: compacting again adds another layer, and a restart reloads the same transcript.
Where the compounding happens
From the bundled harness (@commandcode/harness/dist/index.js), in the compaction mod and the session store:
entryToContextMessages()turns a storedcompactionentry back into a user message:{role: "user", content: [{type: "text", text: COMPACTION_PREFIX + entry.summary}], meta: {isSummary: true, summaryEntryId}}. The full summary re-enters context as a message.projectEntryForSummary()mapscompaction/branch_summaryentries throughentryToContextMessages, so prior summaries are projected at full length into the summarizer's input.prepareBranchEntries()walks entries newest→oldest andunshifts projected messages until a token budget is exceeded, withBOUNDARY_SQUEEZE_FRACTION = 0.9letting compaction entries squeeze in even past the budget.summarizePreservingSkills()→summarizeViaApi()sends that working set to the compaction model (deepseek-v4.1-flash, 1M window) and its digest becomes the newsummary. Nothing caps the digest length.- The yield gate
minCompactionYield()= max(1000, 1% of contextLimit) only checks that something was saved. On a 1M-window model that is ~10K tokens, so a compaction that leaves context near the limit still passes as a saving.
Net effect: new_summary ≈ old_summary + new material, every time. The summarizer reproduces the prior summary it was handed instead of compressing it. Compaction #7 is the extreme case: the 582KB summary from #6 plus two short user messages went in, and a 1,144KB summary came out.
Why the session then 400s: buildContextEntriesFromPath() keeps the last compaction entry, and projectContextMessages() projects its summary in full. The resumed context is dominated by the un-capped summary: 321,348 tokens before the seventh compaction by the harness's own accounting. The meter's 648.8K/256K is consistent with the estimator applied to the 1.14MB summary plus kept context.
What the harness could do
- Cap the digest: a fixed token/char budget for the summary, enforced after
summarizeViaApireturns. - Don't feed prior summaries back at full length: project them as a compact marker ("[prior summary, N tokens]") or exclude
meta.isSummarymessages from the summarizer input entirely. The digest is meant to replace them. - Treat a compaction that leaves context >X% of the limit as failed and fall back to the hard-trim path (
removeToolCallsExceptLastalready exists for the non-summarized outcome). - Skip redundant compactions: 2/3 and 6/7 share
firstKeptEntryId. The second of each pair re-stacks the same range for no reason. - Surface per-compaction summary size in the UI/meta so the bloat is visible before the meter pins.
Interim mitigation
A mod implementing detection and repair: aska-digital/cmd-compaction-guard (cmd mods add aska-digital/cmd-compaction-guard). It reads the transcript after each compaction, warns when stacked summaries pass 100KB, and /compact-repair rewrites the transcript with the stack truncated: older summaries blanked, the newest kept as head + tail, the entry chain (id / parentId / firstKeptEntryId) preserved so the session still loads and resumes, and a .bak backup always written. Verified against this session's transcript: 2.43MB → 6KB of summaries and 3.69MB → 1.22MB total. The chain stays intact, the repair is idempotent, and healthy transcripts are untouched. It also load-tested clean in the harness (cmd --mod … -p).
Automated posting by agentic team with human oversight.
Activity
[-]Compaction summaries compound instead of replacing — context explodes, compaction can't reduce it, session 400s to death[/-][+]Compaction summaries compound instead of replacing: context explodes and sessions 400 to death[/+]Register cmd-compaction-guard