fix(session): compact once per overflow and list shell-written files in the ledger
Issue for this PR
Closes #1638
Type of change
- Bug fix
- New feature
- Refactor / code improvement
- Documentation
What does this PR do?
Two compaction fixes. One makes a single overflow produce a single summary. The other lists files written by shell commands in the post-compaction ledger.
1. One overflow produced two summaries (session/prompt.ts).
After a compaction that keeps a recent tail, filterCompacted renders [compaction, summary, ...tail, continue]. The loop took the array-last finished assistant as lastFinished. That is the retained pre-compaction turn, which still carries its overflowing token usage, so the overflow check fired again before any new request and created a second compaction. The second summary was built from the kept tail only (system + one tool step + instruction). It replaced a full summary with a summary of the last few messages, and it cost an extra model call.
Fix: select lastFinished by id with MessageV2.latest(msgs).finished. That lands on the summary, which the overflow check already skips.
2. The ledger missed files written by shell commands (session/compaction.ts).
The "Session state ledger" listed only files written by write/edit/apply_patch. Files created or changed through bash (redirects, scripts, sed -i) were reported only as "possible but unverified", so the continuing agent re-read or re-created files it had already written. The session already records a per-step snapshot diff as a patch part that names every changed file.
Fix: buildLedger reads patch parts and lists those files as snapshot entries, using the step's last tool completion time (or the message completion time when the step has no completed tool).
- Snapshot entries have their own section after the write-tool entries: "Files changed in the project during your steps (workspace snapshot diffs; may include changes by other processes such as IDE saves or formatters)". Each reads "changed during your step at … (snapshot)", with "via shell" only when that step ran
bash. The snapshot cannot tell what changed a file, so these entries do not say "written by you". - Snapshot entries are capped at 5 paths per step and 15 overall, with "… and N more files changed during your step at …" and "… and N more files changed during M earlier steps" lines. If the ledger is still over its token budget, the snapshot list shrinks before the note, the advisory or the recent tool calls are dropped. Paths inside the project root render relative to it.
- A file written by a write/edit tool in the same step keeps that tool's attribution.
- When the session has snapshot diffs, entries whose file no longer exists are removed at the end of
buildLedger(one existence check per distinct path). This covers a shell delete, a shell rename (the patch lists only the new path) and a file written and deleted in the same step. - Only write-tool entries, or snapshot entries for a path a write tool wrote earlier in the session, corroborate a carried summary claim as
[verified]. A patch-only entry never does. - When snapshot diffs are present, the note reads: "Workspace snapshot diffs list files changed during the steps that recorded one, including shell changes; they omit git-ignored files, files outside the project directory, and new files over 2 MB. Shell changes in steps without a snapshot diff are possible but unverified." (
Snapshot.patchdiffs-- .; files already tracked that exceed 2 MB stay staged and are listed.) - The existence check is injectable for unit tests.
Not in this PR: sending the summary request on the agent's cached prefix (same tool definitions and first system message, with toolChoice: "none"). The summary request still sends tools: {} and the compaction agent's own system prompt, so it misses the prefix cache. It cannot be done as specified:
@ai-sdk/anthropic3.0.78 mapstoolChoice: "none"totools: undefined, and@ai-sdk/amazon-bedrockempties the tool list. On those adapters the tool block, which is first in the cache order, never reaches the wire.- With tools declared in the call,
llm.tsno longer flattens history tool calls, so the request would also carrytool_useblocks with no tool definitions (the #678 400). @ai-sdk/openai-compatiblekeeps the tools withtool_choice: "none", so the gateway model could share the prefix. That needs a check that the gateway and its fallback providers accept the shape, which this PR does not do.
A follow-up should gate prefix sharing by adapter and pass the prompt loop's last request (system, environment, tools, user.system) into SessionCompaction.process, with tool execute stripped. The mock capture below records the current miss.
How did you verify your code works?
- New
test/session/compaction-double-summary.test.ts: seeds a completed tail-keeping compaction (tail assistant at 95K of a 100K context, then compaction, summary, continue) and runsprompt.loopon a mock provider. It asserts exactly one compaction user and one summary after the loop. With the by-idlastFinishedline removed it fails (2 and 2), so it guards the fix behaviourally. test/session/compaction-ledger.test.ts, "snapshot diffs" blocks: a bash-written file is listed "via shell"; a write-tool file in the same step keeps its tool; a later shell change updates an earlier write entry; a deleted file is dropped; withoutpatchparts the "unverified" note stays. Review follow-ups (all failed before the fix): 120 shell files + 1 write-tool file atmaxTokens: 500keep the write-tool entry, the note, the advisory and the recent calls; an overall cap across 30 steps; relative paths; "changed during your step (snapshot)" withoutbashand "via shell" with it; the note names files outside the project directory; shell rename; write and delete in one step; message completion time; a patch-only entry does not verify a carried claim, while a write-tool file later changed by shell still does.- Gateway run on a stacked branch with #1631, #1632, #1641 and #1643 (comment): 1 summary call per compaction event, 9 shell-written files listed in the ledger.
- Review follow-up regression check,
bun test test/session test/tool test/agent(packages/opencode) on the merged head before the changes and after: before 2134 pass / 1 fail (tool.registry > loads Zod-schema custom tools with JSON Schema and validation, a 5 s timeout under the full run); after 2144 pass / 0 fail. No test fails after that passed before. - Request-body capture of both builds against a local mock gateway (comment): on
maintwo overflows produce 4 summary requests, and the ledger never lists the file the agent wrote withbash. On this branch the same run produces 2 summary requests, and the ledger listsnotes.md"via shell" after each compaction. bun test(packages/opencode): 19,855 pass, 254 fail. 243 of the failures are the DuckDB native binding missing from this worktree's install; with the binding copied in, the 19 affected files pass (409/409). The other 11 also fail onmainin the same run setup (mcp.headers×6,mcp HttpApistatus,session diff with missing patch×2,--time-budget×2 injson-output.test.ts).mainadditionally failed the twoTelemetry.init()opt-out cases, which pass on this branch.- Typecheck clean;
script/check-tracker-leaks.tsandanalyze.ts --markers --base origin/main --strictpass.
Cost side effects
Reduces cost: one fewer summary model call per overflow when compaction keeps a tail. Adds no model calls, queries or resources. The ledger change reads patch parts already stored in the session and, when the session has snapshot diffs, makes one existsSync per distinct ledger path at compaction time.
Screenshots / recordings
N/A
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
- I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"
🤖 Generated with Claude Code
Summary by CodeRabbit
- Bug Fixes
- Improved conversation continuity after compaction, helping the assistant use the latest completed response when continuing a session.
- Reduced the chance of repeated summaries or summary instructions appearing during continued conversations.
- Improved how workspace file changes are reflected in session context, including changes made through shell commands and files that no longer exist.