fix(core): persist approved tool results immediately after execution

TO
5h ago
+1070-5

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 === true evidence for that call. A function-valued needsApproval is 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 --noEmit passes. 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.