#20980: fix(guard): pair-atomic tool_use/tool_result commit — prevent orphaned tool_use in JSONL
cli
agents
size: L
Cluster:
Tool Result Compaction Fixes
## Summary
- `installSessionToolResultGuard` now holds assistant messages that contain `tool_use` blocks in a local pair buffer, committing the pair to JSONL only after all matching `tool_result` IDs are received.
- `flushPendingToolResults()` defaults to **discard** mode: on any interruption the incomplete pair is dropped without being written, so no orphaned `tool_use` block ever reaches JSONL.
- Legacy `OPENCLAW_TOOL_GUARD_ABORT_MODE=synthetic` preserves the previous synthesize-and-commit behavior.
## Why
Tool-use aborts (rate-limit, provider failover, user cancel) previously left the session JSONL in an inconsistent state: an assistant `tool_use` block without a corresponding `tool_result`. Strict providers (Anthropic, Cloud Code Assist, MiniMax) reject any subsequent API call that includes an orphaned `tool_use` in its history, causing a hard 400 error that survives across sessions until the JSON file is manually repaired.
## Changes
### `session-tool-result-guard.ts`
- Added `toolPairBuffer: AgentMessage[]` alongside the existing `pending` map.
- `guardedAppend`: assistant messages with `tool_use` are buffered rather than written to JSONL; each incoming `tool_result` advances the buffer; when `pending.size === 0`, `commitToolPairBuffer()` writes the whole pair atomically.
- `validatePairIntegrity()` guards the commit: if a buffer somehow arrives with mismatched IDs it is discarded instead of committed.
- `flushPendingToolResults`: new env-var-driven abort mode (see table below).
**Why discard on hook suppression?**
In the hook-suppressed path we intentionally discard the in-flight buffer instead of attempting a commit, because a suppressed `tool_result` would otherwise allow a lone `tool_use` to persist and corrupt the transcript. This keeps the pair-atomic contract strict. For environments that relied on legacy behavior, `OPENCLAW_TOOL_GUARD_ABORT_MODE=synthetic` preserves the prior "synthesize-and-commit" semantics (only when `allowSyntheticToolResults` is enabled).
### `attempt.ts`
- Added `FAILOVER COVERAGE` comment to the `finally` block documenting this as the single flush point for all exit paths (abort, rate-limit, provider failover).
## Abort mode
| `OPENCLAW_TOOL_GUARD_ABORT_MODE` | Behavior |
|----------------------------------|----------|
| unset / `discard` (new default) | buffer discarded; structured log emitted |
| `synthetic` | synthetic error `tool_result` synthesized and committed (legacy) |
## Tests
### New — `session-tool-result-guard.atomic-commit.test.ts`
5 cases:
- Case 1: abort during tool execution → 0 messages, no orphan
- Case 2: failover during tool execution → 0 messages, no orphan
- Case 3: rate-limit interruption → user message preserved, no orphan
- Case 4: normal pair → `[assistant, toolResult]` committed in order
- Case 5: first pair committed, second aborted → only first pair in JSONL
### Updated — `pi-embedded-runner.guard.waitforidle-before-flush.test.ts`
- Intermediate assertion: nothing in JSONL while pair is in flight (was incorrect: showed `assistant` prematurely).
- Added timeout / discard-mode test (expects 0 entries).
- Added timeout / synthetic-mode test (expects `[assistant, toolResult]` with synthetic error).
- Env var management via `beforeEach`/`afterEach`.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Implements pair-atomic `tool_use`/`tool_result` commit to prevent orphaned `tool_use` blocks in session JSONL when runs are interrupted (abort, rate-limit, provider failover). The guard now buffers assistant messages containing `tool_use` blocks and only commits the complete pair to JSONL after all matching `tool_result` IDs arrive. On interruption, the incomplete pair is discarded by default (new `discard` mode), ensuring no corrupt state reaches JSONL. Legacy `synthetic` mode preserves the previous behavior via `OPENCLAW_TOOL_GUARD_ABORT_MODE=synthetic`.
Key improvements:
- Introduced `toolPairBuffer` to hold assistant messages with `tool_use` until their results complete
- Added `validatePairIntegrity()` to ensure all `tool_use` IDs have matching `tool_result` IDs before commit
- New default `discard` mode drops incomplete pairs on any interruption (abort, failover, rate-limit, hook suppression)
- Comprehensive test coverage for all abort scenarios and pair commit edge cases
- Added `dropOrphanedToolPairs()` utility for session repair when loading corrupted transcripts
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk
- The implementation is well-designed with strong safeguards: pair-atomic commit ensures JSONL consistency, comprehensive test coverage validates all abort scenarios (5 new tests covering abort/failover/rate-limit/normal/sequential cases), backward compatibility is maintained via environment variable, and the discard-by-default behavior is conservative and safe. The logic is clean, well-commented, and follows the repository's coding standards.
- No files require special attention
<sub>Last reviewed commit: c34ea89</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#4852: fix(agents): sanitize tool pairing after compaction and history tru...
by lailoo · 2026-01-30
80.6%
#22011: fix(transcript): drop empty toolCallId toolResults during persisten...
by sauerdaniel · 2026-02-20
79.8%
#4844: fix(agents): skip error/aborted assistant messages in transcript re...
by lailoo · 2026-01-30
79.1%
#14328: fix: strip incomplete tool_use blocks from errored/aborted messages...
by Kropiunig · 2026-02-12
78.3%
#7525: Agents: skip errored tool calls during pairing
by justinhuangcode · 2026-02-02
78.2%
#12487: fix(agents): strip orphaned tool_result when tool_use is sanitized ...
by skylarkoo7 · 2026-02-09
77.9%
#15649: fix: sanitize tool_use IDs on session write path
by aldoeliacim · 2026-02-13
77.4%
#3622: fix(agents): drop orphan tool results
by mickobizzle · 2026-01-28
77.4%
#21195: fix: suppress orphaned tool_use/tool_result errors after session co...
by ruslansychov-git · 2026-02-19
77.4%
#9011: fix(session): auto-recovery for corrupted tool responses [AI-assisted]
by cheenu1092-oss · 2026-02-04
77.2%