fix(core): persist approved tool results immediately after execution
Summary
Fixes #1538. Stacked on #1540, whose approval-state changes are included. This branch has additional persistence/replay commits; it is no longer a single commit on top of #1540.
Approved tools execute before the next model step completes. Previously an interrupted turn could leave a persisted approval without its executed result, so resending it ran the tool again.
- Flush successful results immediately when the buffer contains real
approval.approved === trueevidence for that call. A function-valuedneedsApprovalis not itself approval evidence. - Seed explicit approval input even when limited or semantic context omits the request (#1540).
- Reconcile approval-bearing UI input against stored terminal results independently of model context limits, preserving terminal calls omitted from an older message.
- Order background input reconciliation/writes with result writes through a manager-local queue, so a stale input snapshot cannot overwrite the result within that manager.
- On preservation-read failure, do not write the stale input. Ordinary inputs without explicit approval decisions do not trigger this extra history read.
Limits and tradeoffs
This narrows the post-execution persistence window. It does not provide exactly-once execution, distributed locking, or atomicity across multiple managers/processes. A crash after the tool's own side effect but before storage, a storage failure, or concurrent requests on different managers can still require tool-side idempotency.
The current Memory API has no public ID-targeted message lookup. Explicit approval-bearing input therefore reads full conversation history for reconciliation, even with contextLimit: 0; ordinary UI input does not. This favors retaining execution evidence over treating truncated context as proof no result exists. An ID-targeted adapter lookup would be a separate API improvement.
Successful outputs retain the existing SDK model-output wrapping, including toModelOutput. The approved tool error path remains outside this PR. #1537's whole-generation retry semantics are also separate. The regression coverage below uses generateText; no claim of complete streaming-path coverage.
Validation
All tests are local mocks, with no real payment/refund/email side effects.
- Interrupted approved execution persists before the next model call completes; a resent approval must resolve successfully and must not re-execute.
- Same behavior with
contextLimit: 0. - Normal completion and ordinary tools retain their step persistence behavior.
- Manager tests force an input write to pause while a result write is queued, verify preservation-read failure cannot overwrite output, preserve omitted terminal calls, and avoid history reads for ordinary input.
- Ordering/read-failure tests both fail against the prior implementation and pass after the fix.
- A direct helper test verifies a dynamically non-gated tool has no inferred approval.
- Six related suites: 190 tests pass.
tsc --noEmitpasses. Biome has only the three pre-existing agent.ts complexity warnings. - Patch changeset included.
Summary by CodeRabbit
- Bug Fixes
- Approval decisions for tools remain visible in conversation history, including when a conversation is resumed.
- Approved tool results are saved after execution, so interrupted turns and later message updates retain completed results.
- Resending an approval no longer causes an already completed tool action to run again.
- Previously completed tool results are preserved when older messages are resubmitted, and denied decisions remain recorded.
Corrected omitted-context coverage
Merged the complete-owner hydration fix from #1540. Earlier contextLimit-zero fixtures accidentally passed an ignored Memory constructor property. Both now pass memory.options.contextLimit: 0 at generation time. The zero-context approved-result interruption/retry test passes on this actual path, and partial-input persistence retains stored text and sibling tool parts. Six related suites now pass 191 tests; production typecheck remains clean. This still does not establish cross-manager exactly-once execution.