Repository navigation
fix(runtime): keep tool-use/tool-result pairs paired in Trident compaction - #3367
Conversation
…ction
Trident stage 1 dropped the assistant message that carried a file tool
use while keeping the tool result that answered it, so the compacted
session ended up with orphaned `tool_result` blocks. Providers reject
such requests with HTTP 400 ("tool_result blocks must be a response to
a preceding tool_use"), which breaks the auto-compact retry path that
runs Trident before resending a request.
Stage 1 also counted a tool result as an independent file operation,
which pushed `last_write` past the write the result belonged to and made
the newest write look obsolete.
Only tool uses count as file operations now, and a tool result is
dropped together with the tool use that produced it.
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Code Review: PR #3367
✅ Fix Correctness
Issue: Trident stage 1 ("supersede") was dropping assistant messages containing tool_use blocks while leaving their corresponding tool_result blocks in surviving messages. This creates orphaned tool_result blocks that lack matching tool_use predecessors, causing providers to reject the session with HTTP 400.
Root Cause:
extract_file_operation()treatedtool_resultblocks as independent file operations, causing the logic to calculate obsolescence indices without considering the semantic pairing of tool_use ↔ tool_result- No validation ensured that dropping a
tool_usealso dropped its correspondingtool_result
Fix Verification:
- New test
stage1_keeps_tool_use_and_result_paired()explicitly validates that compacted messages have no orphaned tool_results - Adds pre-emptive logic: collects all dropped tool_use IDs, then marks any surviving message with a tool_result for a dropped tool_use as obsolete (unless it also carries a surviving tool_use)
- Removes tool_results from
extract_file_operation()since they're answer pairs, not independent operations - All 7 trident tests pass, all 740 runtime crate tests pass
No Regressions: Existing stage1 tests (stage1_removes_obsolete_file_reads, stage1_keeps_standalone_reads) pass unchanged; test coverage expanded.
✅ Code Quality
- Format: ✓ (no changes needed)
- Clippy: Pre-existing needless_borrow warnings in stage3 tests unrelated to this PR (present on main at different line numbers)
- Comment quality: Clear explanation of why tool_results must not be independent file operations
✅ Workflow Actions
Fork workflow approval gates successfully advanced to queued/in_progress:
Rust CI(40742767960): queuedRust(40742767808): in_progress
[repo owner's gaebal-gajae (clawdbot) 🦞]
|
Merged as — |
|
确认这个修复很有必要:stage1 supersede 丢弃 tool_use 时同步清理对应的 tool_result,避免孤儿 tool_result 触发 provider HTTP 400 从而破坏 auto-compact 重试路径;同时把 tool_result 从 extract_file_operation 独立文件操作中剔除,逻辑更正确。回归测试 stage1_keeps_tool_use_and_result_paired 覆盖了配对场景,740 个 runtime 测试全绿,可以放心合并。 |
1 similar comment
|
确认这个修复很有必要:stage1 supersede 丢弃 tool_use 时同步清理对应的 tool_result,避免孤儿 tool_result 触发 provider HTTP 400 从而破坏 auto-compact 重试路径;同时把 tool_result 从 extract_file_operation 独立文件操作中剔除,逻辑更正确。回归测试 stage1_keeps_tool_use_and_result_paired 覆盖了配对场景,740 个 runtime 测试全绿,可以放心合并。 |
Summary
Trident stage 1 ("supersede") removed the assistant message that carried a file tool use, but left the
tool_resultmessage that answered it behind. The compacted session then containedtool_resultblocks with no matchingtool_use, and providers reject such requests with HTTP 400 (tool_resultblocks must be a response to a precedingtool_use). That breaks the auto-compact retry path inmain.rs, which runs Trident before resending a request that failed with a context-window error.Stage 1 also registered a tool result as an independent file operation. A result always follows its tool use, so this pushed
last_writepast the newest write and made that write's own tool use look obsolete — splitting its pair as well.Changes
rust/crates/runtime/src/trident.rs:extract_file_operationnow only recognisesToolUseblocks. AToolResultis the answer half of a tool use, not an independent file operation.stage1_supersedecollects the ids of the tool uses it drops and drops the tool result messages that answer them, so notool_resultsurvives without itstool_use. A message that also carries a surviving tool use is kept rather than traded for a different orphan.stage1_keeps_tool_use_and_result_paired, which uses realisticread_file/write_fileoutput shapes and asserts both that no tool result is orphaned and that the newest write survives with its result.Testing
cd rust && cargo test -p runtime --lib trident— the new test fails on the unpatched tree (orphaned tool_result for tool_use 1) and passes with the fix; the existingstage1_*,stage2_*,stage3_*and full-pipeline tests still pass.cd rust && cargo test --workspace— all green except the pre-existingtools::tests::powershell_runs_via_stub_shell, which needspwsh/powershellinPATHand fails identically on an unmodifiedmain.scripts/fmt.sh --check— clean.cd rust && cargo clippy --workspace --all-targets— the set of warnings reported fortrident.rsis unchanged before and after the patch (no new ones).Anti-slop triage
stage1_keeps_tool_use_and_result_pairedis a regression test that fails before the change and passes after; the orphaned-tool_resultfailure mode is the same one the CLI already guards against inabsorb_tool_result_message(rust/crates/rusty-claude-cli/src/main.rs) and inruntime/src/compact.rs.Verification
git diff --checkpasses.Resolution gate
tool_resultblocks one message too late, so the next request is rejected with 400 #3361 is about a different tool-result pairing problem (parallel tool-call turns sending results one message too late) that was already fixed separately.Submitted with gh CLI by @bougieL