Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e52216fa5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3730311a98
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9ba9fefa94
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d93e3bf76a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c6e6470a2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b09d8d14a2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
I don’t think (type, call_id) is collision-free across the Session. Custom model providers can reuse a call ID on a later turn; if an older matching call is still in this tail window, present suppresses the current deferred call and its output can again be persisted without its call. Could this key include a response/turn identity, or otherwise scope the match to the current response instead of treating call_id as globally unique?
|
@sylvesterkaczmarek Reproduced before answering, and it fails exactly as you describe. With
Scoping the match to the current response is the right direction, but I couldn't find a reliable way to do that from Session history alone. A Session persists a flat sequence of I also tested the narrower alternative of matching the entire converted prefix as an ordered block instead of matching individual items. That fixes the collision case, but breaks partial writes: if an earlier attempt persisted the calls but failed before persisting the output, the full prefix no longer matches and the calls are appended again. That seems to be the recurring signal from the edge cases on this PR: we're trying to answer "was this batch already written?" from Session history, but Session history doesn't contain enough provenance to answer that reliably. So I think the cleaner direction is to stop inferring it.
I prototyped changing the deferred park so that it records the withheld batch as the pending session write rather than dropping it and reconstructing it later. On resume, we then reconcile a declared batch instead of guessing from history. That removes the id-reuse, partial-write, detached-resume and cancellation cases I was able to construct, and actually deletes a fair amount of the reconciliation logic added by this PR. There are two semantics I don't want to choose on behalf of the maintainers, though:
Full write-up and reproducer are in #4827. I can push the prototype to this PR, open it separately, or hand the approach over if you'd rather own that shape. If you'd prefer to keep #4828 narrow and land the deeper persistence change separately, I can also just adjust the key here. |
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
I think the non-streaming loop still has the deferred-prefix loss case. On NextStepRunAgain, it clears deferred_session_prefix even when turn_session_items is empty and therefore nothing was persisted; the streamed loop now guards that case. Could the non-streaming path keep the deferred prefix until at least one resolved turn item is actually saved?
|
@sylvesterkaczmarek I reproduced this before answering, and the result is clearer than I expected. I built the empty-resolved-turn case end to end: park a gated call, approve it, then resolve into a turn whose session items are emptied by a handoff I then traced the non-streaming loop for that exact run. The I also couldn't construct a Finally, as a mutation test, I deleted the clear entirely and re-ran the eight reproducers from this PR plus the full suite. The outcomes were byte-identical. The variable only lives for a single resume pass: that branch is entered while So my earlier comment on that line ("later turns of this run must not re-send it") was incorrect. The loss your review points out is real, but it isn't reachable through that clear, and retaining the variable can't bridge it. Within the pass there is no later consumer; across runs, the only carrier is reconciliation against Session history, which your collision finding already showed can't be made reliable. The one place the batch survives all of this is the serialized checkpoint. That's the What I did push is a regression test ( I left the clear itself alone. Deleting it changes nothing measurable, and changing dead code as if it fixed this would be misleading. If a maintainer weighs in on #4827, I'll finish the declarative version, which makes this whole family of cases unreachable. |
seratch
left a comment
There was a problem hiding this comment.
The ordinary orphan-output case is real, but the current prefix-inference approach still has supported resume failures: the lookup happens after approved tool execution, the streamed lookup drops the context wrapper, legacy no-argument Sessions fail, and finalization/reconnect can duplicate or lose the batch. I recommend redesigning around the deferred response's durable ownership instead of adding another inference branch. Reusing pending-write recovery must also preserve the output-guardrail persistence gate; eagerly writing the held prefix before that gate is not a safe replacement.
2312428 to
47bfebe
Compare
|
Thanks for the direction, it was the right call. I have replaced the reconciliation The park now registers the withheld batch on the existing Each of your four points now fails by construction rather than by inference: there is The PR description is rewritten with the full design, the serialized-state note |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 47bfebebf9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
openai-agents-python/src/agents/run_internal/session_persistence.py
Lines 795 to 799 in fe5790e
When a streamed approval resume resolves directly to NextStepFinalOutput, _save_resumed_stream_items has already removed the held batch via take_held_session_write, but this condition excludes the final step from resumed_write_state. A Session append failure or lost acknowledgement therefore leaves no pending write in the checkpoint; retrying skips the completed tool while its call/output exchange remains absent from the Session. Route final settlement through the pending-write recovery path before clearing the held record.
AGENTS.md reference: AGENTS.md:L104-L104
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f007612498
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b470baec5c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
openai-agents-python/src/agents/run_internal/session_persistence.py
Lines 703 to 710 in 90868a8
When a held multi-approval checkpoint is resumed with the persistence gate off but without a new approval decision, the approval placeholders trigger settlement but convert to zero new_items, while the held calls are passed through original_input; consequently saved_run_items_count is zero and this pending record also stores a zero persisted count. If the append fails or loses its acknowledgement, resume_pending_session_write() reconciles the held calls but restores that zero count, so a later gate-enabled resume can pass the output-guardrail safety check and append those calls again. Fresh evidence beyond the prior successful-settle counting thread is that save_resumed_turn_items() adds len(held_input) only after success, leaving this failure-recovery metadata uncorrected.
AGENTS.md reference: AGENTS.md:L104-L104
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 821afdc3f7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
I don't think the current head is ready to approve yet. There are still unresolved correctness/compatibility issues on the current diff: the new pending_session_write.held serialization needs schema-version treatment consistent with the released reader contract; held-write reattachment bypasses the Responses compaction bookkeeping used by canonical persistence; and the streaming max-turn-handler terminal path can retain a held write after reporting completion. The post-tool callback failure window documented in the open thread is also a real durability gap at the commit boundary. Please resolve the remaining current-head threads, or narrow/document the compatibility contract sufficiently, before re-requesting review.
|
Thanks, that was a precise list. All four are addressed on the current head, each with a regression test proven red against the previous commit. Schema. You and Codex were right and my reasoning was wrong: I leaned on 1.17 being unreleased, but the 1.17 reader validates the pending write by exact key set, so a checkpoint written under that label with the extra keys is not loadable by a 1.17 reader, and my own regression called the four-key form released behaviour. Compaction bookkeeping. Fixed at the root rather than patched: the entry settle no longer appends behind the canonical path, it goes through Max-turn terminal path. Both runners now discard a still-standing held batch when a max-turn handler ends the run, so the finished run's checkpoint stays loadable and the streaming result no longer reports terminal output while carrying a resumable pending write the non-streaming result had already dropped. Post-tool callback window. Fixed rather than documented: the resumed turn's output committer folds the committed output into the held batch as it commits it, so a callback that raises afterwards cannot leave a retry that skips the completed invocation and drops the executed call and its result. One more round, self-inflicted. Reviewing my own diff afterwards turned up four defects it had introduced, all fixed on this head with a test proven red against the previous commit:
Verification on this head: full suite green (9471 passed) except one pre-existing sandbox failure that also fails here with this branch stashed, |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 14c8315506
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
When a partial-approval turn settles a held call/output batch into an OpenAIResponsesCompactionSession, this local-output branch returns only saved_run_items_count, omitting the settled_batch_items that were appended. The current persisted count can therefore remain zero; if the caller later re-enables the output-guardrail gate and approves the remaining call, the resumed-safety check permits the final sweep to append the already-stored calls again, corrupting Session history. Fresh evidence beyond the earlier count thread is that this compaction-only return bypasses the corrected common return on line 790. Return the combined count here as well.
AGENTS.md reference: AGENTS.md:L102-L102
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…ized contract The acceptance battery drives park, approve, reject, re-park, detached carry, tripwire, guardrail crash, emptied turn, legacy and context-aware sessions, and a failed settle recovered on the next resume, each through both runners and a serialized checkpoint. The resume-path suite pins the held marker's validation and that a checkpoint without the marker keeps its released eager-settle meaning.
…fer signature The non-streamed runner always builds a RunState for a fresh run, so the interruption result reads the held record from the state itself; the extra carrier parameter could never be exercised.
…red held calls A held checkpoint resumed against a different Session must fail at boot like an ordinary pending write, before the approved tool can execute and the batch can settle into the wrong conversation. And a handoff input_filter may drop a subset of the resolved outputs, so the settling batch being non-empty does not make it safe: a held call settles only when its output survived, which honors the filter's decision symmetrically in both directions.
…pprovals, and completions Four holes the adversarial pass over the final diff surfaced, each reproduced in both runners before fixing. A fresh park during a detached resume now folds the new call into the standing held batch instead of losing it. The pairing guard exempts calls whose approvals are still open on the current step: their outputs are missing because they have not run yet, not because a filter removed them, so they settle like a non-deferred park writes a call before its output. The entry settle applies the same pairing contract against the batch alone. And a detached completion discards the batch at the fresh final exit too, so a completed run's checkpoint stays loadable and the runners agree. The streamed resume test double now forwards the settling batch.
…ettles, and restore Conversations sanitization at entry Three findings from the third automated review round. The sandbox runtime defers the pre-turn input save, so a deferred park used to fold the Session's only copy of the accepted input into the held batch, where a tripwire discard would take it along: the deferred arm now persists any unsaved input exactly as the non-deferred arm does, and the batch carries only the withheld response. The final-output settle now registers the claimed batch before appending, so a crash inside that append fails closed with the batch recorded instead of silently losing it. And the attached entry settle re-applies the Conversations-specific sanitization a detached extension could not, restoring the backend invariant before the direct append.
…d pair every approval family Two more findings from the same review round. With output guardrails the final sweep rebuilds the whole current response, held batch included, and the item deduplication cannot key the batch's unkeyed companions, so an assistant preamble landed twice: when the final items already carry every held request the batch is redundant and is dropped, in both runners. And the pairing guard now speaks every supported approval identity, hosted MCP requests and responses included, instead of recognizing only the function-call pair.
…counting rules Four more findings from the same review round. The held pairing guard now delegates to the canonical drop_orphan_function_calls, so every tool-call family in _TOOL_CALL_TO_OUTPUT_TYPE pairs (shell and apply-patch included) and a reasoning item riding before a dropped call is pruned with it, as the Responses API requires. A Conversations-backed registration forces the reasoning-id policy to None like the normal save, so a server-identified reasoning item stays persistable. And settled held items count toward the turn's persisted count, so a later gate-enabled resume fails fast on the persisted-items refusal instead of re-appending the stored calls.
… cover the held batch The guardrail rebuild deduplicates the held batch out of the append to avoid doubling its unkeyed companions, but the append still lands the approved call and output, so the recovery registration must stay armed. Arming now keys off whether a held batch was claimed at all, captured before the dedup empties the payload, in both the resumed-turn helper and the zero-count final save; a crash inside the append leaves the batch recorded to reconcile on retry instead of silently losing it.
…ttle it through the canonical path, and close the terminal and commit-boundary gaps Recovered work addressing the four blocking review points: - The held variant adds keys the released 1.17 reader rejects by exact key set, so it now has its own schema version. 1.17 keeps its four-key form and its original summary; held and response_id are gated to 1.18, with corpus fixtures, sources, README and the version-boundary test updated. - The entry settle no longer appends behind the canonical persistence path: it goes through save_result_to_session like every other settle, inheriting the Conversations sanitization, the ordered dedup, the pending-write registration and the compaction bookkeeping for the response the batch belongs to, which the park now records. - A max-turn handler ends the run, so both runners discard a held batch there. - The resumed turn's output committer folds a committed tool output into the held batch, so a post-output callback that raises cannot leave a retry that skips the completed invocation and drops the executed call and its result.
…ool output The consolidated settle hands the held batch to the canonical path through the original_input slot, but the deferral decision only inspected new_items, so a batch containing the approved tool's output reported no local tool output and compacted the very response whose output had just landed. The decision now asks whether the append persists a local tool output at all, whichever slot carried it, and the batch records the store setting of the turn it was withheld in so the deferral resolves the same compaction mode the ordinary path would.
…e held-only keys The corpus entries claimed a 1.18 writer for a commit that emits 1.17 and has no response_id, and the generator had no 1.18 scenario, so regenerating the corpus would have dropped them. Both fixtures are now what the recorded 1.17 writer emits with only the schema label changed, the generator carries the matching scenarios, and the README says the same thing. The reader also refuses response_id and store on an ordinary pending write, where they describe nothing, and the schema rationale no longer claims 1.17 shipped in a release: its readers are on main, which is reason enough not to rewrite what they already emit.
A handoff input_filter can drop every resolved item, but by then the approved tool has run and its output was folded into the held batch. Emptiness of the turn was the wrong predicate: pairing is. The executed call and output now settle through the canonical path and only the unpaired requests drop, so the Session keeps the only record that the tool ran and a later run does not re-issue its side effect. Three defects the same review surfaced go with it: - The compaction deferral read the whole ``original_input`` slot, which carries the caller's own input on an ordinary save. Only a settling batch reads it now. - The settled count added the batch's raw length, overcounting whatever the dedup dropped; the append reports what it actually wrote, and that count slices a later save of the same turn. - The max-turns discard ran before ``validate_handler_final_output``, so a wrongly typed handler output lost the batch that the streamed runner keeps. Each is pinned by a test proven red against the previous behaviour.
…its last step is chosen Choosing a terminal step is not the same as ending the run. Validation, the final-output hooks, the output guardrails and the final save all run after that choice, any of them can raise, and a run that raises may still be retried or reattached with the approved tool's call and output reachable only through the held batch. Consuming the batch at the choice threw it away on every one of those failures. Four sites carried that ordering, and they are the whole class: the max-turn handler finalization, the detached final output in both runners, and the detached final output on the resumed streamed loop. Each now disposes of the batch once finalization has completed, with a tripwire handled separately as the decided blocked outcome it is. The five remaining disposal sites are deliberate ones that follow an outcome already decided, and they are unchanged. Also: a re-interruption no longer overwrites the storage setting the parked response was produced under. Presence of the key decides, not its truthiness, so an ordinary ``store=None`` park keeps its own setting and the settle resolves that response's compaction mode from the right turn. ``response_id`` follows the same rule for the same reason. Tests pin the failure of each finalization stage in both runners, and the park storage settings across None, False and True.
…n path Two defects in how a settling held batch meets a compaction-aware session: - The compaction-deferral branch returned the run-item count alone, and it is the branch every held settle with outputs takes on such a backend, so exactly the sessions that defer were the ones whose settled turns undercounted. The count gates the resumed-safety refusal and slices later saves of the same turn, so it must equal what the append wrote. The branch now returns the combined count. - The local-continuation classification knew the mapped tool outputs but not the hosted MCP approval response, which is the locally produced half of its approval pair and must stay associated with the response chain that carried the request. Compacting that response before the model consumes the approval drops it in previous_response_id mode. The constant is now _LOCAL_CONTINUATION_OUTPUT_TYPES and covers both carriers: the settled dict and the MCPApprovalResponseItem the non-deferred resume commits, because classifying one and not the other would defer or compact the same response depending on which path persisted it. The four-stage scenario behind the count (partial approval, held settlement with a lapsed gate, gate re-enable, remaining approval) is pinned end to end: it must end in the documented fail-fast refusal with nothing duplicated. Each fix is also pinned at the unit boundary and proven red by mutation.
A detached re-park cannot see the Session backend, so it folded new items under the resuming run's own reasoning-id policy. For a Conversations-origin batch that strips the server id at the one point where nothing can restore it, and the reattach then drops the reasoning item as unpersistable. The park now records the conversion policy it actually used (None for a Conversations backend, the run's policy otherwise) on the held record, and a fold converts under the record's policy instead of the caller's. The key is gated and validated with the other held keys under the unreleased 1.18 schema, refused on ordinary pending writes and on unknown values; an absent key falls back to the caller's policy. Pinned at the fold and at the park, plus the validator rejection, each guard proven red by mutation.
A reattached detached carry can reach the final exit with a zero persisted count, so the batch settles through the final sweep's direct save. That call armed the recovery registration but not the settle marking, so the compaction deferral could not see the batch's outputs when the final turn carried none of its own, and the returned count excluded what the settle wrote. One flag closes both, pinned at the helper boundary and proven red by mutation. Also repairs the held-record docstring, whose conversion-policy paragraph had split a sentence in two.
…ered session view ``HandoffInputData.new_items`` is the session-history axis by contract, and ``input_items`` exists precisely so a filter can shape model input while preserving history. The held settle defeated that: an output the commit boundary folded into the batch settled on the batch's own evidence, so a filter that removed a complete pair from ``new_items`` — the library's own ``remove_all_tools`` included — found it persisted anyway. The commit boundary now records the call ids it folds in a deliberately transient, per-turn set on the run state. A settling or folding exit whose ``run_items`` are the resolved session view drops the batch's copy of those outputs: kept ones arrive through the view itself, removed ones must not land. The set resets at each turn boundary, because the filter's authority covers one turn, and it does not serialize, because an output folded by an earlier process is carried prior-turn history that a later turn's filter is not entitled to remove, exactly as the eager path cannot unpersist earlier turns. No schema change. The regression pair Sylvester asked for is pinned in both runners, plus the library-filter secret case, the detached filtered handoff riding a checkpoint, and the carried-pair boundary. Two prior tests asserted the settled-from-batch behaviour and now pin the corrected contract.
…tract survives a serialized retry A post-output callback crash leaves the folded output on the checkpoint, and the supported retry path serializes and reloads that state. The fold's ownership lived in process memory, so the reloaded retry could not tell the crashed turn's own outputs from carried history: a handoff filter that removed the pair found the batch settling it anyway, the library's own ``remove_all_tools`` included. The commit boundary now records the call ids it folds on the held record itself (``folded_tool_outputs``, gated and validated with the other held keys under the unreleased 1.18 schema), together with the turn that owns them, and the in-memory set is gone. A settle or fold on an exit where a ``Handoff.input_filter`` actually ran (``SingleStepResult`` now says so) drops the batch's copies of the owned outputs: kept ones arrive through the resolved view in the same save, removed ones must not land, and the pairing prune takes their calls with them. Exits without a filter are untouched, so a partial approval's second resume still settles the earlier approval's pair from the batch, and the marker expires with its turn, so carried prior-turn history still settles at the reattaching entry. Pinned by the serialized-retry regression in both runners alongside the existing filter-contract pair, the partial-approval flow, and the carried-pair boundary; the marker's validator shape has its own rejection case; each guard proven red by mutation.
…anions too The held batch carries the parked response's unkeyed companions (an assistant preamble, an id-less reasoning item), and a handoff filter that removed them from the session view found them persisted from the batch anyway: the gate covered the owned outputs and nothing else. On a filtered current-turn claim or detached fold, an unkeyed batch item now survives only if the resolved view kept it, matched by the same fingerprint the dedup uses. Calls and request kinds stay with the pairing rule, owned outputs keep their unconditional drop, and carried prior-turn history remains outside the filter's reach. Pinned by the filtered-preamble regression in both runners, each branch proven red by mutation.
…r reads them Main bumped the schema to 1.18 for agent-scoped approvals and MCP recipient bindings while this branch was in review, so the held pending write now shares the unreleased 1.18 label instead of taking a number of its own, the same way 1.17 absorbed the pending Session write before it. The two tests that relabel a current checkpoint as 1.17 must therefore also strip the 1.18-only context and response fields, exactly as main's own relabeling tests do, or the reader rejects the payload before the pending-write validator ever runs.
e222d67 to
46599c9
Compare
|
Rechecked current head |
This pull request fixes #4827 by giving the interruption park durable ownership of the
session batch it withholds, replacing the history-reconciliation approach that the
previous revision of this PR used and that review rejected.
The bug
_should_defer_interrupted_session_itemswithholds the interrupted turn's sessionwrite when the agent has output guardrails and a non-default
tool_use_behavior. Thepark leaves no record of what it withheld, so no later code can prove it happened.
When the approval resume resolves into
next_step_run_again, only the resolved turn'sitems are written: the Session ends up holding a
function_call_outputwhosefunction_callwas never persisted, and the Responses API rejects every later runover that Session with
No tool call found for function call output. Theconversation is permanently dead. This is reproducible with
ScriptedModelalone(see
tests/test_deferred_interrupted_session_write.py, red on main).The fix: registering is not writing
RunState._pending_session_writealready is the durable single-slot primitive for"one canonical resumed append awaiting acknowledgement", with digest-based crash
recovery. The park now registers the withheld batch on that slot with a
heldmarker:
persistence gate is preserved exactly: nothing reaches the Session until the gate
stops applying to the parked response.
turn's items in one ordered
save_result_to_sessionappend (call before output byconstruction) and re-registers as the one ordinary pending write, inheriting the
existing digest reconciliation for a crash mid-append.
after_turncancellation of a detached resumeleaves behind) settles at resume entry: the parked response's outputs already went
back to the model, which only happens after the gate stopped applying, and the
run-again turn's saves never arm
resumed_write_state, so entry is the only settlepoint that checkpoint will ever reach.
resume settles call and output together.
from the batch itself.
HandoffInputData.new_itemsis the session-history axis bycontract (
input_itemsexists precisely to filter model input while preservinghistory), so an output the commit boundary folded into the batch this turn settles
only if the filter's view kept it; a removed output stays out of the Session and
takes its call with it. Carried prior-turn history (a detached carry riding a
checkpoint) still settles, because a later turn's filter is not entitled to
unpersist earlier turns, exactly as the eager path cannot.
the declaration once the blocked outcome is decided, and persists only the
sanitized rebuild.
The history-reconciliation machinery from the previous revision
(
deferred_interrupted_session_prefix,_identity_key,_PREFIX_MATCH_LOOKBACK) isdeleted.
How this addresses each review point
lookup anymore. The batch is on the checkpoint before the resume starts.
every settle flows through the existing save and settle machinery, whose call
sites carry
wrapper=context_wrapper. Pinned bytest_settle_reaches_a_context_aware_session_through_the_wrapper.limit=read is deleted withthe lookup. The settle inherits the pre-existing
limit=reads ofresume_pending_session_write; a companion change makes_session_get_itemsprobe the signature and fall back to a full read with the released latest-N
semantics applied locally. Pinned by
test_a_session_without_optional_kwargs_survives_a_deferred_resume.survives serialization and the step flip to run-again, and every exit either
settles, extends, or discards it deliberately.
to_state()on a completed runnever carries a stale record. Pinned by
test_after_turn_cancel_keeps_the_held_batch_for_the_next_attachand thestate._pending_session_write is Noneassertions.Serialized-state note (per
.agents/references/runstate-schema.md)The held variant adds keys the 1.17 reader rejects by exact key set, and 1.17 readers
are released (v0.22.x), so writing it under that label would emit checkpoints they cannot
load. The held keys are therefore gated to 1.18, the unreleased label main introduced for
MCP recipient bindings and agent-scoped approvals while this PR was in review, and the
1.18 summary now names both. This is the same move 1.17 made when it absorbed the pending
Session write before release, and it leaves no gap in the corpus that a private 1.19 would
have opened. 1.17 keeps the four-key form and the summary it shipped with, and 1.17
payloads still restore and settle eagerly as before. Corpus fixtures (
minimal/v1_18.json,features/v1_18_held_pending_session_write.json),sources.json, the corpus README and the supported-version boundary test are updated.Pinned by
test_pending_session_write_without_the_held_key_keeps_its_meaning(the1.17 form still loads and settles) and by the
held-under-1-17case oftest_pending_session_write_rejects_invalid_serialized_checkpoint(the held variantunder the older label is refused).
Contract-surface inventory for
heldpending_session_write.helddefer_interrupted_session_write)from_jsonvalidation, resume-entry settle rules, exit settles (take_held_session_write), detached folds (extend_held_session_write),to_jsonand both checkpoint copies (opaque deepcopy), the non-streamed result bridgefalsekeeps the released meaning: eager settle at resume entryheldwith recordedbeforedigests rejectedAwait-boundary inventory for the settle
from the slot (
take_held_session_writefrees the slot first). Suspension point:the composed
save_result_to_sessionappend. While suspended: the write isregistered as the one ordinary pending write with
beforedigests, so a crash orcancellation mid-append is recovered by the existing committed-versus-unchanged
reconciliation on the next resume (pinned by
test_a_failed_settle_of_the_held_batch_is_recovered_on_the_next_resume, 16interleavings across both runners and serialization).
itself; a second batch while one is unresolved fails fast with the existing
UserError.copies of the same checkpoint have no cross-object interlock
(
resume_pending_session_writedocuments that the application must serializeaccess), so the held variant inherits exactly the same contract.
Why the field could not travel before this change
The non-streamed result rebuilds a
RunStatefrom_*_for_stateshadow attributesdeclared on the result (
result.py), and_pending_session_writehad no shadowdeclared at all, unlike
_pending_input_for_state. The original omission is amissing shadow declaration, not a missed assignment in the copy block, which is why
the field mechanically could not ride that path; this PR declares the shadow and
reads it back. Credit for this framing goes to an independent reader of #4827 who
verified the report from source.
Decisions taken with a stated default
rather than extending 1.17 in place (the released 1.17 reader validates the pending
write by exact key set and would reject a checkpoint written under its own label) or
taking a private 1.19 (which would leave 1.18 without a corpus entry the moment it
stopped being the current version).
deliberately transient, per-turn set of folded call ids on the run state (no schema
change; same lifecycle precedent as
_session_write_in_progress). After acheckpoint the set is empty on purpose: an output folded by an earlier process is
carried history, and the reattaching entry settle keeps the carried batch whole. An
earlier revision settled the batch's paired part regardless of the filter; the
documented
new_items/input_itemssplit is the controlling contract and thisrevision follows it.
checkpoint shape only exists after the parked response's outputs went back to the
model and no later settle point exists on that path.
get_itemssignature probe is a companion fix in this PRbecause the settle machinery it protects predates this change; without it a
pre-
limitstructural Session hard-fails any pending-write recovery.existing same-session error, before the approved tool can execute; a detached
resume still rides.
input_filterthat drops one resolved output takes exactly that call with it,while calls whose approvals are still open settle unpaired on purpose: their
outputs have not run yet, matching how a non-deferred park persists a call before
its output. The entry settle applies the same contract against the batch alone.
standing held batch (the declaration carries the session identity), and a detached
completion discards the batch at the final exit so a completed run's checkpoint
stays loadable in both runners.
input. The gate withholds model output, not the user's accepted input, so a
deferred park persists any still-unsaved input (the sandbox runtime defers the
pre-turn save) exactly as the non-deferred arm does, and a tripwire discard can
never take the Session's only copy of the input with it.
its append, regardless of the current step, so a crash inside the settle fails
closed with the batch recorded. The resulting mid-settle checkpoint is rejected
on load on purpose: the run ended mid-settle, and failing closed beats replaying
an approved side effect as if nothing happened.
sanitization before the direct append, restoring the invariant for items a
detached extension added without a Session in hand.
is dropped as redundant at the shared settle choke point, because the item
deduplication cannot key its unkeyed companions (an assistant preamble, an id-less
reasoning item) and feeding it again would duplicate them.
compaction. The batch reaches the canonical path through the
original_inputslot,so a decision that only inspected
new_itemswould compact the very response whoseoutput had just landed; the record also carries the store setting of the turn it was
withheld in, so the deferral resolves the mode the ordinary path would have.
response_idandstoreare refused on an ordinary pending write, wherethey describe nothing, and the 1.18 corpus entries are what the recorded 1.17 writer
emits with only the schema label changed, with matching generator scenarios so the
corpus stays reproducible.
drop_orphan_function_calls,so every tool-call family in
_TOOL_CALL_TO_OUTPUT_TYPEpairs (shell and apply-patchincluded) and a reasoning item riding before a dropped call is pruned with it; hosted
MCP approvals pair alongside through the canonical request identity.
Nonelike the normal save, and settled held items count toward the turn's persistedcount so a later gate-enabled resume fails fast instead of re-appending.
output-commit boundary, so a post-output callback that raises cannot leave a retry
that skips the completed invocation and drops the executed call and its result.
discarded in both runners; the finished run's checkpoint stays loadable and the two
runners report the same terminal state. The discard sits below
validate_handler_final_output, because only past that validation does the handleractually end the run.
original_inputslot only on thecalls that settle a batch through it. That slot carries the caller's own input on
an ordinary save, and an earlier tool output sitting there says nothing about
whether this response produced one.
the batch's raw length. The resolved turn re-delivers the outputs the batch already
folded in, they dedup away, and the count slices a later save of the same turn. The
compaction-deferral branch returns the same combined count: it is the branch every
held settle with outputs takes on a compaction-aware backend, so returning the
run-item count alone there undercounted exactly the sessions that defer.
response in both of its carriers (the settled dict and the run item the
non-deferred resume commits). The approval response is the locally produced half
of its pair and must stay associated with the response chain that carried the
request; classifying one carrier and not the other would defer or compact the same
response depending on which path persisted it.
(
reasoning_item_id_policy, gated and validated with the other held keys). Adetached fold cannot see the Session backend, so converting under the resuming
run's own policy could strip a Conversations server reasoning id at the one point
where nothing can restore it; the record carries the policy the park actually
used, and folds convert under it.
response, not the parked one. At every exit a newer response is the session's
compaction frontier and the batch lands inside its append; the recorded
response_idandstoreare for the entry settle, the one place where theparked response still is the frontier.
Test plan
tests/test_deferred_interrupted_session_write.py: park, approve, reject,re-park, detached carry with
after_turncancellation, tripwire (with a preamblepinning that the redaction's drops are not resurrected), guardrail crash, emptied
turn, context-aware and legacy sessions, single ordered write, and failed-settle
recovery; each scenario runs both runners and a serialized checkpoint round trip.
tests/test_run_impl_resume_paths.py:heldvalidation (non-bool, held withbefore) and the no-marker compatibility pin.extend, each discard, the pairing guard and its pending-approval exemption, the
session-identity check, the bridge, the slot release, the signature probe, the
emptied-turn settle, the compaction predicate, the settled count and the max-turn
discard ordering, every one proven to turn at least one test red.
make format,make lint,make typecheck,make testsvia.agents/skills/code-change-verification/scripts/run.sh.