fix(compaction): sanitize Anthropic tool pairs on summarization requests#306
Merged
Merged
Conversation
Local compaction summarization called stream() directly without the before_provider_request hooks that normal turns run, so an orphan tool_result could reach the Anthropic Messages API and permanently reject compaction for an over-limit session (400: each tool_result block must have a corresponding tool_use block). Apply the existing sanitizeAnthropicPayload as onPayload for anthropic-messages summary streams, and add a wire-level regression using the exact rejected id.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A real over-limit session (
019f82ff-…) became permanently stuck: every turn died at the output-token limit, and the only recovery path — auto-compaction — itself failed with a hard400:Once compaction 400s repeatedly, the circuit breaker opens and the session bricks (context already
> 1,000,000tokens).Root cause
tool_result. It has an errored assistant tool call (stopReason: "error",errorMessage: "terminated") at transcript line 556 with no matching result. An older process build synthesized a placeholder result for that dangling call and then dropped the errored assistant, leaving an unconsumed (orphan)tool_result.before_provider_requesthook chain, which runstool-pair-guard'ssanitizeAnthropicPayload().stream()directly (speculative.ts) and does not run those hooks, so the orphan reached the Anthropic Messages API unchanged and was rejected — killing the one escape hatch.Fix
Apply the existing Anthropic payload sanitizer at the direct summarization-request boundary, only for
anthropic-messagesmodels:This adds only the missing tool-pair invariant guard to the side request; it deliberately does not replay the full extension hook chain.
Tests / verification
test/compaction/anthropic-tool-pair-guard.test.tsdrives the real Anthropic adapter against a local endpoint that rejects orphan/non-adjacenttool_resultblocks, using the exact rejected id. Fails before the fix (same 400), passes after.npm run check: pass (exit 0) —tsgo --noEmit, biome, pinned-deps, shrinkwrap, install-lock, web-ui, and Neo build/vet/test all green (one pre-existing info-levelnoFlatMapIdentitynote in an unrelated test).npm run build: pass across all packages..agents/skills/senpi-qa) self-tests: mock-loop 20/20, rpc 4/4, cli-smoke 7/7, common 9/9 — zero real provider calls, real auth untouched. Evidence underlocal-ignore/qa-evidence/20260723-compaction-tool-pair/.Pre-existing, unrelated full-suite failures (not touched by this change):
app-server-daemon.test.tstimeout andapp-server-protocol.test.tsmanifest mismatch.Note
A currently running old senpi process must be restarted to load this fix; after restart, manual
/compactbypasses the circuit-breaker cooldown and the summarization request is sanitized before transmission.Summary by cubic
Sanitizes Anthropic tool-use/tool-result pairs on compaction summarization requests to prevent 400s from orphan tool_result blocks. This unblocks auto-compaction for over-limit sessions and avoids circuit-breaker lockouts.
Bug Fixes
onPayloadwhen the model API isanthropic-messages, ensuring no orphantool_resultreaches Anthropic during compaction’s directstream()call.Migration
/compactbypasses cooldown and sends a sanitized request.Written for commit f7d92c0. Summary will update on new commits.