fix(context): orphaned tool_result HTTP 400 — three-layer defense (ISSUE #171) - #174
Merged
Conversation
Three-layer defense for tool_use/tool_result pairing: - Layer 1: MemoryStore::recent_history round-aware window + orphan expand/drop - Layer 2: ContextPipeline hard trim round-aware deletion - Layer 3: ContextSerializer sanitize_orphans safety net References mainstream Agent implementations (Claude Code, Codex, OpenCode) and includes three-layer regression test plan.
Three-task TDD plan: - Task 1: Layer 3 safety net (ContextSerializer sanitize_orphans) - Task 2: Layer 1 source fix (MemoryStore recent_history round-aware) - Task 3: Layer 2 hard trim round-aware deletion - Task 4: Final verification Each task: failing test → CMake registration → implement → rebuild → regression check → commit.
Drops orphan tool_result at head and orphan tool_use at tail before serialization. Prevents Anthropic API HTTP 400 errors when upstream windowing or trimming produces unpaired tool messages. Layer 3 of ISSUE #171 fix.
…esults Rewrite recent_history to slice on user-message boundaries instead of naive max_turns*2 estimate. Adds adjust_for_orphan_tools helper that expands the window to cover a parent assistant when nearby, or drops leading orphan tool messages when the parent is too far. Layer 1 of ISSUE #171 fix.
Previous Test 4 and Test 6 passed trivially because the window never started on a tool message. Rewrote scenarios to force start onto a tool_result, triggering the drop path (Test 4) and expand path (Test 6).
…sult pairing Replace per-message erase in hard trim with whole-round deletion (user-led boundaries). Prevents orphaned tool_result blocks when token-budget trim removes an assistant message containing tool_use. Re-scans round starts each iteration to avoid index drift. Layer 2 of ISSUE #171 fix.
…riggered - Per-message token truncation in hard trim matches initial computation, eliminates negative tokens_after artifact in logs. - Add pipeline.stats().hard_trims > 0 assertion to all 3 test cases so they verify hard trim actually fired (not just that output is clean). - Strengthen Test 2 to also assert front message is a user (round boundary).
…/catch, spec update - Add Test 4: DoesNotDeleteSystemMessages to test_pipeline_hard_trim.cpp (spec-required test case, was missing). - Wrap sanitize_orphans in try/catch per spec line 318: safety net must not crash AgentLoop on unexpected exceptions. - Document the serialize() move in spec: Layer 2 fix moves serialize() after hard trim to fix latent no-op bug where hard trim mutated bound.provider_messages after payload was already built.
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.
Summary
Fixes #171 — Anthropic API HTTP 400 caused by orphaned
tool_resultblocks lacking matchingtool_useblocks.Three-layer defense, each independently revertable:
6a31f9b):ContextSerializer::sanitize_orphans()drops orphantool_resultat head and orphantool_useat tail before serialization. Pure function, try/catch-wrapped so it can never crash the AgentLoop. Applies to both OpenAI and Anthropic formats.001da48+b1afb05):MemoryStore::recent_history()slices on user-message boundaries instead of naivemax_turns*2. Addsadjust_for_orphan_toolshelper that expands the window backward to cover a parent assistant (when withinmax_turns*2+4) or drops leading orphan tool messages (when parent is too far).3ea265d+a16aeb0):ContextPipeline::planned_assemble()hard trim deletes whole rounds (user-led boundaries) instead of per-message erase. Also fixes a latent bug:serialize()moved from after hard trim to before it — the old position made hard trim a no-op on the actual payload.Commit
cddc570closes spec gaps: addsDoesNotDeleteSystemMessagestest, wrapssanitize_orphansin try/catch per spec line 318, documents theserialize()move in the spec.Design:
docs/superpowers/specs/2026-06-22-issue-171-orphaned-tool-result-design.mdPlan:
docs/superpowers/plans/2026-06-22-issue-171-orphaned-tool-result.mdTest Plan
merak-context-serializer-test— 5 cases (orphan head/tail Anthropic, paired preserved, multiple orphans, OpenAI head)merak-memory-history-test— 6 cases (no tools, tool boundary, well-formed, far-parent drop, empty, no-user fallback expand)merak-context-pipeline-test— 4 cases (round boundaries, preserve 1 round, end-to-end ISSUE LLM API 400: orphaned tool_result blocks in Anthropic request — tool_use_id mismatch #171 repro, system message preserved)merak-context-test— existing context tests still passmerak-agent-loop-test— 26/26 existing loop tests still pass