Skip to content

fix(sessions): declare the withheld interrupted write as a held pending Session write - #4828

Open
dixso wants to merge 34 commits into
openai:mainfrom
dixso:fix-deferred-interrupted-session-write
Open

dixso wants to merge 34 commits into
openai:mainfrom
dixso:fix-deferred-interrupted-session-write

Conversation

@dixso

@dixso dixso commented Sep 2, 2026 •

Copy link
Copy Markdown

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_items withholds the interrupted turn's session
write when the agent has output guardrails and a non-default tool_use_behavior. The
park 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's
items are written: the Session ends up holding a function_call_output whose
function_call was never persisted, and the Responses API rejects every later run
over that Session with No tool call found for function call output. The
conversation is permanently dead. This is reproducible with ScriptedModel alone
(see tests/test_deferred_interrupted_session_write.py, red on main).

The fix: registering is not writing

RunState._pending_session_write already 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 held
marker:

  • Registering touches only the checkpoint, never the Session, so the output-guardrail
    persistence gate is preserved exactly: nothing reaches the Session until the gate
    stops applying to the parked response.
  • At a gate-legal exit of a later resume, the batch lands ahead of the resolved
    turn's items in one ordered save_result_to_session append (call before output by
    construction) and re-registers as the one ordinary pending write, inheriting the
    existing digest reconciliation for a crash mid-append.
  • A run-again checkpoint (what an after_turn cancellation of a detached resume
    leaves 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 settle
    point that checkpoint will ever reach.
  • A detached exit folds the resolved items into the standing batch so the reattaching
    resume settles call and output together.
  • Held settlement derives its pairing evidence from the resolved session view, never
    from the batch itself. HandoffInputData.new_items is the session-history axis by
    contract (input_items exists precisely to filter model input while preserving
    history), 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 blocked-output redaction never sees the batch raw: the tripwire path discards
    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) is
deleted.

How this addresses each review point

  1. "Looking up the deferred prefix after the approved tool executed": there is no
    lookup anymore. The batch is on the checkpoint before the resume starts.
  2. "The streamed lookup drops the context wrapper": the wrapper-less read is deleted;
    every settle flows through the existing save and settle machinery, whose call
    sites carry wrapper=context_wrapper. Pinned by
    test_settle_reaches_a_context_aware_session_through_the_wrapper.
  3. "Legacy no-argument Sessions fail": the per-resume limit= read is deleted with
    the lookup. The settle inherits the pre-existing limit= reads of
    resume_pending_session_write; a companion change makes _session_get_items
    probe 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.
  4. "Finalization or reconnect can drop or duplicate the batch": the declaration
    survives serialization and the step flip to run-again, and every exit either
    settles, extends, or discards it deliberately. to_state() on a completed run
    never carries a stale record. Pinned by
    test_after_turn_cancel_keeps_the_held_batch_for_the_next_attach and the
    state._pending_session_write is None assertions.

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 (the
1.17 form still loads and settles) and by the held-under-1-17 case of
test_pending_session_write_rejects_invalid_serialized_checkpoint (the held variant
under the older label is refused).

Contract-surface inventory for held

surface producers consumers default/missing invalid
pending_session_write.held the four park sites: fresh and re-park, both runners (defer_interrupted_session_write) from_json validation, resume-entry settle rules, exit settles (take_held_session_write), detached folds (extend_held_session_write), to_json and both checkpoint copies (opaque deepcopy), the non-streamed result bridge absent or false keeps the released meaning: eager settle at resume entry non-bool rejected; held with recorded before digests rejected

Await-boundary inventory for the settle

  • Operation: settling the held batch at a resume exit. Snapshot: the batch claimed
    from the slot (take_held_session_write frees the slot first). Suspension point:
    the composed save_result_to_session append. While suspended: the write is
    registered as the one ordinary pending write with before digests, so a crash or
    cancellation 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, 16
    interleavings across both runners and serialization).
  • Ownership mechanism: the existing single-slot rule. A settling write registers
    itself; a second batch while one is unresolved fails fast with the existing
    UserError.
  • Known pre-existing limitation, unchanged by this PR: two independently restored
    copies of the same checkpoint have no cross-object interlock
    (resume_pending_session_write documents that the application must serialize
    access), so the held variant inherits exactly the same contract.

Why the field could not travel before this change

The non-streamed result rebuilds a RunState from _*_for_state shadow attributes
declared on the result (result.py), and _pending_session_write had no shadow
declared at all, unlike _pending_input_for_state. The original omission is a
missing 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

  • Decision: the held variant rides the unreleased 1.18 label main already introduced
    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).
  • Decision: the filter's session authority is scoped to one turn and tracked by a
    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 a
    checkpoint 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_items split is the controlling contract and this
    revision follows it.
  • Decision: a run-again checkpoint settles the batch at entry, because that
    checkpoint shape only exists after the parked response's outputs went back to the
    model and no later settle point exists on that path.
  • Decision: the legacy get_items signature probe is a companion fix in this PR
    because the settle machinery it protects predates this change; without it a
    pre-limit structural Session hard-fails any pending-write recovery.
  • Decision: a held resume with a different attached Session fails at boot with the
    existing same-session error, before the approved tool can execute; a detached
    resume still rides.
  • Decision: a held call settles only when paired. Pairing is per call, so a handoff
    input_filter that 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.
  • Decision: a fresh park during a detached resume folds the new call into the
    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.
  • Decision: the held batch carries only the withheld response, never the run
    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.
  • Decision: a settling held batch always registers as the one pending write before
    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.
  • Decision: the attached entry settle re-applies the Conversations-specific
    sanitization before the direct append, restoring the invariant for items a
    detached extension added without a Session in hand.
  • Decision: when the guardrail rebuild already carries every held request, the batch
    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.
  • Decision: the settling batch's tool outputs count when deciding whether to defer
    compaction. The batch reaches the canonical path through the original_input slot,
    so a decision that only inspected new_items would compact the very response whose
    output 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.
  • Decision: response_id and store are refused on an ordinary pending write, where
    they 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.
  • Decision: the pairing guard 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; hosted
    MCP approvals pair alongside through the canonical request identity.
  • Decision: a Conversations-backed registration forces the reasoning-id policy to
    None like the normal save, and settled held items count toward the turn's persisted
    count so a later gate-enabled resume fails fast instead of re-appending.
  • Decision: a committed tool output folds into the held batch at the resumed turn's
    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.
  • Decision: a max-turn handler ends the run, so a held batch still standing there is
    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 handler
    actually end the run.
  • Decision: the compaction deferral inspects the original_input slot only on the
    calls 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.
  • Decision: the settled count is reported by the append itself rather than added as
    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.
  • Decision: the local-continuation classification covers the hosted MCP approval
    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.
  • Decision: the held record owns the conversion policy of its items
    (reasoning_item_id_policy, gated and validated with the other held keys). A
    detached 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.
  • Decision: exit-based settles run the compaction bookkeeping for the resume's
    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_id and store are for the entry settle, the one place where the
    parked response still is the frontier.

Test plan

  • tests/test_deferred_interrupted_session_write.py: park, approve, reject,
    re-park, detached carry with after_turn cancellation, tripwire (with a preamble
    pinning 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: held validation (non-bool, held with
    before) and the no-marker compatibility pin.
  • A mutation pass over the new logic: each registration site, each settle, each
    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 tests via
    .agents/skills/code-change-verification/scripts/run.sh.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_internal/blocked_output.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_internal/run_loop.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_internal/session_persistence.py Outdated
Comment thread src/agents/run_internal/run_loop.py Outdated
Comment thread src/agents/run_internal/session_persistence.py Outdated
Comment thread src/agents/run.py Outdated
Comment thread src/agents/run_internal/session_persistence.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_internal/session_persistence.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_internal/session_persistence.py Outdated
Comment thread src/agents/run_internal/run_loop.py Outdated

@sylvesterkaczmarek sylvesterkaczmarek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@dixso

dixso commented Sep 3, 2026

Copy link
Copy Markdown
Author

@sylvesterkaczmarek Reproduced before answering, and it fails exactly as you describe.

With call_1 already present in the Session tail from an earlier turn, and the current deferred response emitting call_1 again, deferred_interrupted_session_prefix returns []. The new call is suppressed while its output can still be persisted, recreating the exact orphan this PR is meant to prevent, reached through a different path.

tests/test_tool_approval_call_id_reuse.py also suggests the repo already treats call ID reuse as a real case, so I don't think we can rely on (type, call_id) being unique enough for reconciliation.

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 function_call / function_call_output items, with no marker identifying which model response or turn produced them. So adding response or turn identity to the key doesn't help unless that identity is persisted too, which feels like a broader Session contract change rather than something this fix should introduce implicitly.

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.

RunState._pending_session_write already represents almost exactly what we need: a canonical session append that is pending acknowledgement. It's serialized by to_json, restored by from_json, and reconciled in order by resume_pending_session_write.

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:

  1. RunResult.to_state() creates a fresh RunState through _populate_state_from_result, and _pending_session_write only survives when the result already carries a _state. So the first park currently loses it. We'd need to propagate it through the result, similar to _current_turn_persisted_item_count.

  2. Settling that deferred batch makes _current_turn_persisted_item_count > 0, which then triggers Cannot resume an approval checkpoint with output guardrails after current-turn items were persisted. Whether settling the deferred batch should count toward that guard is really a question about the intended invariant.

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 sylvesterkaczmarek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@dixso

dixso commented Sep 4, 2026

Copy link
Copy Markdown
Author

@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 input_filter. I ran it on both runners. They already behave identically, and identically badly: neither writes a dangling call, but both lose the deferred batch completely. The approved tool executes, yet neither its call nor its output reaches the Session.

streamed  items=[('user',''), ('message','')]  call_PARKED present: False
nostream  items=[('user',''), ('message','')]  call_PARKED present: False

I then traced the non-streaming loop for that exact run. The NextStepRunAgain check at run.py:1264 is evaluated, but the clear at :1267 never executes because the emptied turn resolves into NextStepHandoff, not RunAgain.

I also couldn't construct a RunAgain case with empty turn_session_items: approving or rejecting an interruption always produces an output item, while the only path I found that empties the turn is a handoff filter, which takes the other branch.

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 _current_step is an interruption, and update_run_state_after_resume replaces it before the loop continues. Nothing reads the variable after that pass, and a later resume recomputes it from state.

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 _pending_session_write direction in #4827, pending the two maintainer decisions listed there.

What I did push is a regression test (23124284) pinning what we can verify: an emptied resolved turn writes no dangling call and no orphaned output in either runner, and both runners produce identical Session contents in that shape. It's proven red against 9ba9fefa, where the streamed path wrote both calls dangling.

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 seratch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dixso
dixso force-pushed the fix-deferred-interrupted-session-write branch from 2312428 to 47bfebe Compare September 5, 2026 10:03
@dixso dixso changed the title fix(sessions): persist deferred interrupted-turn items when the approval resume continues the run fix(sessions): declare the withheld interrupted write as a held pending Session write Sep 5, 2026
@dixso

dixso commented Sep 5, 2026

Copy link
Copy Markdown
Author

@seratch

Thanks for the direction, it was the right call. I have replaced the reconciliation
approach entirely with durable ownership on the checkpoint, as you suggested.

The park now registers the withheld batch on the existing
RunState._pending_session_write slot with a held marker. Registering touches only
the checkpoint, so the output-guardrail persistence gate is fully preserved; the batch
settles at a gate-legal exit of a later resume, landing ahead of the resolved turn's
items in one ordered append and inheriting the existing digest-based crash recovery.
The lookup, the identity heuristics, and the lookback constant are deleted.

Each of your four points now fails by construction rather than by inference: there is
no post-execution lookup, every settle flows through wrapper-carrying machinery, the
legacy-session read shape is gone (plus a signature probe for the settle's inherited
limit= reads), and the declaration survives serialization, the run-again flip, and
detached reconnects, with explicit settle, extend, or discard at every exit.

The PR description is rewritten with the full design, the serialized-state note
(held extends unreleased 1.17, following the same pattern as the commit that
introduced pending_session_write), and the decisions I took with their rationale.
The new tests drive every scenario through both runners and a serialized round trip,
and I ran a mutation pass proving each new guard turns at least one test red.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_internal/session_persistence.py
Comment thread src/agents/run.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

resumed_write_state=(
run_state
if run_state is not None
and isinstance(run_state._current_step, NextStepRunAgain | NextStepInterruption)
else None

P1 Badge Arm recovery before settling a final held batch

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".

Comment thread src/agents/run_internal/session_persistence.py
Comment thread src/agents/run_internal/session_persistence.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run.py
Comment thread src/agents/run_internal/agent_runner_helpers.py
Comment thread src/agents/run_internal/session_persistence.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_internal/session_persistence.py Outdated
Comment thread src/agents/run_internal/session_persistence.py
Comment thread src/agents/run_internal/session_persistence.py
Comment thread src/agents/run_internal/session_persistence.py
Comment thread src/agents/run.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

resumed_write_state._pending_session_write = {
"session_id": session.session_id,
"items": copy.deepcopy(items_to_save),
"before": None,
"persisted_count": (
resumed_write_state._current_turn_persisted_item_count + saved_run_items_count
),
}

P1 Badge Include held items in the pending write recovery count

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".

Comment thread src/agents/run_internal/agent_runner_helpers.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_state.py Outdated
Comment thread src/agents/run_internal/session_persistence.py Outdated
Comment thread src/agents/run_internal/run_loop.py

@sylvesterkaczmarek sylvesterkaczmarek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dixso

dixso commented Sep 7, 2026

Copy link
Copy Markdown
Author

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. CURRENT_SCHEMA_VERSION is now 1.18, 1.17 keeps its four-key form and its original summary, the held keys are gated to 1.18, and the corpus fixtures, sources, README and version-boundary test are updated. Both directions are pinned: a 1.17 payload still restores and settles eagerly, and the held variant under a 1.17 label is refused.

Compaction bookkeeping. Fixed at the root rather than patched: the entry settle no longer appends behind the canonical path, it goes through save_result_to_session like every other settle, so it inherits the Conversations sanitization, the ordered dedup, the pending-write registration and the compaction bookkeeping. That needed the response the batch belongs to, which the park now records (the second key the version bump covers). Pinned by a test that asserts a compaction-aware backend receives the bookkeeping for the parked response.

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:

  • The resumed turn's committer folds the executed output into the held batch (the post-tool fix above), which made a wholesale discard on an emptied resolved turn wrong: it threw away a fully paired call and output, so the Session lost its only record that the approved tool ran and the next run would re-issue the side effect. The emptied turn now settles the batch's paired part and drops only the unpaired requests, which is the predicate every other settle already uses.
  • Widening the compaction check to see the settling batch made it read the whole original_input slot, which carries the caller's own input on an ordinary save. It now reads that slot only on the calls that actually settle a batch.
  • The settled count added the batch's raw length, overcounting whatever the dedup dropped. The append reports what it wrote, because that count slices a later save of the same turn positionally.
  • The max-turn discard ran before validate_handler_final_output, so a wrongly typed handler output lost a batch the streamed runner keeps. It now sits below the validation.

Verification on this head: full suite green (9471 passed) except one pre-existing sandbox failure that also fails here with this branch stashed, mypy and mypy --platform win32 clean, pyright clean, and a mutation pass in which every new guard was proven to turn a test red.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agents/run_internal/run_loop.py Outdated
Comment thread src/agents/run_internal/run_loop.py Outdated
Comment thread src/agents/run_internal/session_persistence.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review


P1 Badge Include settled items in the compaction return count

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".

Comment thread src/agents/run.py
Comment thread src/agents/run_internal/session_persistence.py Outdated
…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.
@dixso
dixso force-pushed the fix-deferred-interrupted-session-write branch from e222d67 to 46599c9 Compare September 26, 2026 06:58
@dixso
dixso requested review from a team and rm-openai as code owners September 26, 2026 06:58
@sylvesterkaczmarek

Copy link
Copy Markdown
Contributor

Rechecked current head 46599c9f. The only branch-specific change since my last clean review is the schema-relabel compatibility update: production behavior is unchanged apart from a run-state summary line wrap, while the tests now strip the current 1.18-only approval/MCP fields before exercising 1.17 reader semantics. The held-write/filter behavior I reviewed remains unchanged. No remaining blocker from my review.

@github-actions github-actions Bot removed the stale label Sep 27, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants