Skip to content

fix(compaction): sanitize Anthropic tool pairs on summarization requests#306

Merged
code-yeongyu merged 1 commit into
mainfrom
fix/compaction-anthropic-tool-pair
Jul 23, 2026
Merged

fix(compaction): sanitize Anthropic tool pairs on summarization requests#306
code-yeongyu merged 1 commit into
mainfrom
fix/compaction-anthropic-tool-pair

Conversation

@code-yeongyu

@code-yeongyu code-yeongyu commented Jul 23, 2026

Copy link
Copy Markdown
Owner

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 hard 400:

messages.280.content.1: unexpected tool_use_id found in tool_result blocks:
tool_MT8C3KqVPHoD0Xw5GLGvSQBO. Each tool_result block must have a
corresponding tool_use block in the previous message.

Once compaction 400s repeatedly, the circuit breaker opens and the session bricks (context already > 1,000,000 tokens).

Root cause

  • The stored transcript does not contain an orphan 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.
  • Normal agent turns strip such orphans because the provider payload goes through the before_provider_request hook chain, which runs tool-pair-guard's sanitizeAnthropicPayload().
  • Compaction summarization calls 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.
  • The circuit breaker was secondary: it amplified the repeated 400s but did not create the orphan.

Fix

Apply the existing Anthropic payload sanitizer at the direct summarization-request boundary, only for anthropic-messages models:

...(options.snapshot.model.api === "anthropic-messages"
    ? { onPayload: sanitizeAnthropicPayload }
    : {}),

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

  • New wire-level regression test/compaction/anthropic-tool-pair-guard.test.ts drives the real Anthropic adapter against a local endpoint that rejects orphan/non-adjacent tool_result blocks, using the exact rejected id. Fails before the fix (same 400), passes after.
  • Scoped compaction + tool-pair suites: 50/50 pass.
  • 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-level noFlatMapIdentity note in an unrelated test).
  • npm run build: pass across all packages.
  • Real-CLI QA (.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 under local-ignore/qa-evidence/20260723-compaction-tool-pair/.

Pre-existing, unrelated full-suite failures (not touched by this change): app-server-daemon.test.ts timeout and app-server-protocol.test.ts manifest mismatch.

Note

A currently running old senpi process must be restarted to load this fix; after restart, manual /compact bypasses 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

    • Apply the existing Anthropic payload sanitizer via onPayload when the model API is anthropic-messages, ensuring no orphan tool_result reaches Anthropic during compaction’s direct stream() call.
    • Add a wire-level regression test that rejects orphan results and verifies the summarization request is clean.
  • Migration

    • Restart any running process to load the fix; after restart, manual /compact bypasses cooldown and sends a sanitized request.

Written for commit f7d92c0. Summary will update on new commits.

Review in cubic

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.
@code-yeongyu
code-yeongyu merged commit 928e900 into main Jul 23, 2026
9 of 10 checks passed
@code-yeongyu
code-yeongyu deleted the fix/compaction-anthropic-tool-pair branch July 23, 2026 06:04
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