Skip to content

feat: inject compression summaries as tool-result recaps (acp_context_recap)#95

Merged
ranxianglei merged 4 commits into
masterfrom
2026-07-10_tool-result-recap
Jul 10, 2026
Merged

feat: inject compression summaries as tool-result recaps (acp_context_recap)#95
ranxianglei merged 4 commits into
masterfrom
2026-07-10_tool-result-recap

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Summary

Compression summaries are now injected as synthetic tool-result pairs (acp_context_recap) instead of text-based user/assistant messages. This fixes two long-standing bugs:

  • Issue chore: release v1.4.0 #20 (GLM-5.2 echo): model echoed compression summaries because they were injected as role:assistant text — the model treated them as its own prior output and continued/echoed them.
  • Issue [Bug]: 上下文压缩后目标漂移 #78 (gpt-5.5 drift): model treated compression summaries as user instructions because they were injected as role:user text — the model chased old topics as if the user asked about them.

Root cause

Compression summaries are system metadata (neither user nor assistant), but opencode's Message type only allows role: "user" | "assistant". Both roles misled the model:

Solution

Inject summaries as synthetic tool-call + tool-result pairs. At the API level, the model sees role: "tool" — a neutral third role that means "data from a tool", neither instruction nor own voice.

Why this works

Constraint Status
No user-role confusion (#78) ✅ tool output ≠ user instruction
No assistant-role confusion (#20) ✅ tool output ≠ own prior voice
No prefix cache breakage ✅ mid-stream injection, system prompt unchanged
Cross-provider compatible ✅ OpenAI role:tool / Anthropic tool_result content type
No opencode changes needed ✅ ToolPart already in SDK Message type
Backward compatible ✅ All blocks (old + new) get tool-result format on next transform hook run

Commits

  1. feat: inject compression summaries as tool-result recaps (acp_context_recap) — core implementation
  2. refactor: remove dead MERGED_SUMMARY header/footer + prependCompressionSummary — dead code cleanup
  3. refactor: remove [ACP system message] wrapper from notifications — redundant wrapper removal

Verification

  • TypeScript: 0 errors
  • Tests: 599/599 pass
  • Deployed and tested live — all blocks confirmed as tool-result format in debug logs

…_recap)

Replace dual-path injection (Bug 36 merge-into-user + Bug 37 standalone-
assistant) with single-path tool-result injection. Each active compression
block is now emitted as a synthetic AssistantMessage containing a completed
ToolPart for the acp_context_recap tool.

At the API level the model sees role:tool output — semantically neutral data
that is neither user instruction (fixes #78 goal drift) nor own prior voice
(fixes #20 verbatim echo). Works across OpenAI (role:tool) and Anthropic
(tool_result content type) without provider-specific handling.

Does not break prefix caching: injection is mid-stream at compression anchor
positions, system prompt stays stable between compressions.

- lib/messages/utils.ts: add createSyntheticToolRecap + ACP_RECAP_TOOL_NAME
- lib/messages/prune.ts: simplify filterCompressedRanges to single path
- lib/prompts/system.ts: update recap description for tool format
- tests: 9 tests updated to assert tool-result format
- 599/599 tests pass, typecheck clean
…onSummary

These were used by the old user-merge injection path (Bug 36) which is
now replaced by the tool-result recap injection. No remaining callers.

Verified: bundle has 0 occurrences of 'ACP SYSTEM METADATA' or
'MERGED_SUMMARY' or 'prependCompressionSummary'.
The wrapper text was added to prevent the model from confusing
notifications with user input. Now redundant — notifications use
noReply:true + ignored:true flags.
… fix + multi-block test

Review fixes from dual-agent code review (PR #95 / #22):
- isSyntheticMessage now recognizes msg_acp_recap_ prefix (prevents
  recap messages from being treated as real messages in nudge anchor
  selection)
- system.ts tool count: four → five
- New test: multiple active blocks in one filterCompressedRanges pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant