fix(sessions): recover failed resumed Session writes on a renewed interruption - #4650
Merged
seratch merged 1 commit intoAug 25, 2026
Conversation
…erruption A resumed turn that resolves one approval but leaves another pending advances to NextStepInterruption. save_resumed_turn_items() and the RunState pending write validator both gated recovery on NextStepRunAgain, so that transition recorded no pending write. A failed append was dropped and the completed tool output never reached the durable Session, while RunState kept it. Widen both gates to accept NextStepInterruption so the existing pending write mechanism covers the transition.
4 tasks
4 tasks
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
Resuming a turn that resolves one approval while another stays pending advances the run to
NextStepInterruption. Both halves of the pending-write recovery added in #4630 are gated onNextStepRunAgain:save_resumed_turn_items()only passesresumed_write_statewhen the step isNextStepRunAgain, so this transition records no pending write.RunStatedeserialization rejects a restored pending write unless the step isNextStepRunAgain.So when the resumed
Sessionappend raises on that transition, the failed batch has no recovery marker. The approved tool already ran and its guardrails and hooks already completed, but thefunction_call_outputnever reaches the durableSession, whileRunStateandRunResult.to_input_list()both keep it. Resolving the second approval then continues into another model call on top of aSessionthat holds afunction_callwith no matching output, and a later fresh run prunes the orphan, so the completed action disappears from future context.This is the same durable divergence #4630 fixed for
NextStepRunAgain, on a transition that PR did not cover. Its description scopes out handoff and terminal recovery, not this one.Measured on an eight row matrix (sync and streamed, live and JSON round trip, atomic failure and commit-then-raise), comparing the durable
Sessionagainst the replay:The change widens both gates to accept
NextStepInterruptionso the existing pending-write mechanism covers the transition. No new machinery, no schema change:pending_session_writealready carries everything recovery needs, andresume_pending_session_write()already reconciles it before the next model call.Scope note: #4646 also reports that the completed tool's input/output guardrail results are dropped from
RunStateand every later result on this path. That is a separate ordering problem inrun.py, where the guardrail results are published after the append that raises, and it is deliberately not touched here.Test plan
tests/test_run_impl_resume_paths.py::test_renewed_interruption_recovers_failed_resumed_session_append, parametrized over streamed and non streamed and over a live state and a JSON round trip. It pauses on two approval-gated calls in one response, approves only the first, fails the resumed append, then resolves the second approval and asserts the durableSessionand the replay agree.Verified it fails without the source change by reverting
src/and rerunning: all four cases fail with['function_call'] != ['function_call', 'function_call_output']..agents/skills/code-change-verification/scripts/run.shpasses end to end: format, lint, typecheck and the full test suite, including the existing #4630 coverage.Issue number
Partially addresses #4646
Checks
.agents/skills/code-change-verification/scripts/run.sh/reviewbefore submitting this PRI'm a freshman in college learning this codebase by chasing real bugs in it, so please double check my reasoning on the transaction boundary here. I picked the smallest change that made the durable state agree with the replay rather than proposing a new contract, since the pending-write mechanism already existed and only the gate looked too narrow. Happy to fold in the guardrail-result half or drop this entirely if you'd rather handle the whole transition in one go.