Skip to content

fix: preserve completed tool outputs when a sibling fails - #5332

Open
jbeckwith-oai wants to merge 9 commits into
mainfrom
codex/fix-completed-tool-history
Open

jbeckwith-oai wants to merge 9 commits into
mainfrom
codex/fix-completed-tool-history

Conversation

@jbeckwith-oai

@jbeckwith-oai jbeckwith-oai commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This pull request fixes completed tool outputs disappearing when another tool in the same turn fails. Client-managed runs retain accepted call/output pairs, required reasoning and replay ancestry in Session history, error details, streamed results, and existing RunState recovery. The run still reports the failure, and rejected input or tool output is not published.

Accepted output is recorded after tool output guardrails and provider conversion, before optional custom-data extraction and end hooks. Failed runs may therefore retain an output with incomplete optional metadata. Local recovery snapshots detach replay payloads and SDK metadata from late callbacks while preserving stream occurrence markers and provider-owned response identities. Successful runs retain their existing metadata behavior. Existing Session reconciliation handles failed writes; there is no new public API or persisted schema.

Scope and limitations:

  • Server-managed continuation (conversation_id / response chaining) keeps its existing recovery path. The known local-output gap on follow-up failures is separate work.
  • Cleanup remains bounded. Application callbacks that suppress cancellation cannot be forcibly terminated by asyncio; this change does not guarantee their termination or bounded aggregate resource use.
  • Retained history does not guarantee exactly-once external side effects across crashes, failed persistence, or new model-issued calls. Application idempotency remains necessary where required.

Test plan

  • Required verification stack passed: format, lint, Mypy, Pyright, and 12,266 tests (69 skipped).
  • 521 focused tests covering completed siblings, admission, metadata, native/function tools, approval resume, Session reconciliation, hosted events, ToolSearch and Program replay.
  • 10 focused Python 3.10 metadata/cancellation regressions passed.
  • Two consecutive clean fresh-context adversarial review rounds on the final content.
  • Native macOS sandbox tests are skipped only in the Codex sandbox and run in CI.

Issue number

Closes #5327

Checks

  • Added regression coverage
  • Ran .agents/skills/code-change-verification/scripts/run.sh
  • All local verification steps pass
  • Independent adversarial review completed on the final content

@jbeckwith-oai
jbeckwith-oai requested review from a team, rm-openai and seratch as code owners October 7, 2026 14:26
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T20:21:14.756016Z 57a2c71 New commits
🔒 Security Review ✅ Completed 2026-10-07T20:23:32.212363Z 57a2c71 New commits

Security findings

Advisory findings (3)

ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: af69e3739e

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

@markstuart-oai markstuart-oai 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.

Reviewed 39324dd. I found a passing-input timing case that still loses completed side effects; the inline comment describes the trigger and regression test.

I also confirmed two existing findings against this commit:

  • Supplied non-streamed RunState is not synchronized. After an approved interruption plus accepted state.add_input(...), the generated/session arrays are copied before the next turn. This handler appends completed pairs and the response only to local arrays, so serializing the supplied state after the tool error loses them. Even without added input, the session and raw-response copies lag. Synchronize the recovery state before rethrowing.
  • Streamed replay loses the tool-use snapshot. The new error branch bypasses the normal tracker snapshot. A first turn with tool_choice="required" can retain completed outputs but restore an empty tracker, forcing another tool call on resume instead of resetting tool choice.

Please make partial-turn acceptance update the complete recovery checkpoint, including transcript, response and tool-use state, through the existing state owners. The two runners currently update different subsets of that state. The earlier non-streamed input-admission comment does not reproduce here: resumed turns set all_input_guardrails to an empty list.

Validation: source review of the four-file diff, lifecycle/state owners and regression tests. GitHub reports 23 successful checks and two skipped readiness checks at this head. I did not run repository workloads. The added tests cover the basic failure modes, but not the delayed passing input verdict or these resume-state cases.

Comment thread src/agents/run_internal/run_loop.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: 4e334fbc96

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/run_internal/turn_resolution.py Outdated
Comment thread src/agents/run_internal/run_loop.py
Comment thread src/agents/run_internal/turn_resolution.py Outdated
Comment thread src/agents/run_internal/turn_resolution.py Outdated
Comment thread tests/test_native_tool_failure_history.py
Comment thread tests/test_tool_batch_failure_history.py
Comment thread src/agents/run_internal/tool_execution.py
Comment thread src/agents/run_internal/run_loop.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 Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 8493f588e1

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/agents/run_internal/tool_execution.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: 8493f588e1

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/run_internal/turn_resolution.py
Comment thread src/agents/run_internal/tool_actions.py
Comment on lines +929 to +932
def _publish_completed_tools() -> None:
# Accepted server responses already have their own resumable checkpoint.
if on_tool_execution_error is not None and not server_manages_conversation:
retained_items = _completed_tool_step_items(new_step_items, completed_outputs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Publish local successes in server-managed follow-up turns

When conversation_id or response chaining is active on a follow-up turn, pending_input_admission_items is empty, so the accepted response does not receive the special interruption checkpoint referenced by this comment. This unconditional server-managed exclusion then prevents a successful local sibling's output from reaching error details or RunState; the server owns the tool call in its response chain but does not own the client-executed output, so resuming cannot submit that completed result and may repeat the local side effect. Publish local completed outputs on these turns or checkpoint every accepted server response before excluding this path.

AGENTS.md reference: AGENTS.md:L115-L117

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Leaving this thread open as a separate scope item. This PR deliberately preserves existing server-managed recovery and limits its new partial-history publication to client-managed turns. The follow-up path described here needs a server-conversation checkpoint/replay contract: the server owns response calls, while local outputs still need submission. Reusing the client-managed publication path or checkpointing every server response without that analysis risks duplicate replay. The current change does not claim to fix that path; the PR description now states the boundary. This remains unresolved, not a false positive or an accepted-risk dismissal.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The reviewed scope is now approved: this PR fixes client-managed history and preserves the existing server-managed recovery path. The follow-up-turn gap remains separate work and is stated in the PR description. A server already owns failed sibling calls, so reusing the pruned client transcript is insufficient; the follow-up needs explicit unresolved-invocation and unsent-output recovery semantics. Leaving this thread open because 57a2c71 does not fix that gap.

Comment thread src/agents/run_internal/run_loop.py
Comment thread src/agents/run_internal/turn_resolution.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: 959ebbb16a

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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

@dpiet-oai dpiet-oai 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.

The non-streaming runner still loses completed tool history when a default parallel input guardrail is pending; see the inline finding.

Comment thread src/agents/run.py
Comment thread tests/test_tool_batch_failure_history.py Fixed
Comment thread tests/test_tool_batch_failure_history.py Fixed

@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 Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 5a2bc6993d

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

if pending:
task.cancel()
# Bound cancellation cleanup too: application hooks may suppress cancellation.
await asyncio.wait((task,), timeout=_FUNCTION_TOOL_POST_INVOKE_WAIT_SECONDS)

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 Security Review · Automatically triggered

P2 Badge Security: Ensure canceled native finalizers terminate before returning

When a prompt-exposed host configures a native tool whose custom_data_extractor or on_tool_end catches CancelledError, a model response can pair that tool with a failing function-tool category. This branch calls task.cancel() but ignores whether the second timed asyncio.wait() still returns the task in pending; the run then raises while the finalizer remains live with its run context. Repeating such requests accumulates detached tasks and enables service-level exhaustion. Fresh evidence beyond the prior finalizer comment is that the new cancellation wait still has no terminal-state check. Keep ownership until termination or use an isolation boundary that can be killed.

SECURITY.md reference: SECURITY.md:L29-L35


Dismiss this finding: Reply with @codex security dismiss <reason> [context]. Codex will resolve this conversation automatically; GitHub may require a page refresh to show the result.

Valid reasons: false-positive, duplicate, out-of-scope, compensating-control, risk-accepted, or other. Example: @codex security dismiss duplicate Already flagged by another review

What each reason means
  • false-positive — Not a vulnerability
  • duplicate — Already tracked elsewhere
  • out-of-scope — Outside this review's scope
  • compensating-control — Mitigated by another control
  • risk-accepted — Risk intentionally accepted
  • other — Another reason; context required

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed the specific cancellation-resistant case with a synthetic public Runner probe: the callback receives cancellation, suppresses it, and remains pending after the bounded cleanup returns. The probe explicitly releases it afterward to avoid leaving work behind. This is not fixed by the earlier cancel-then-wait change.

The existing tool lifecycle contract requires bounded draining and prompt parent cancellation; it does not guarantee forcible termination of application callbacks that suppress cancellation. An asyncio Task cannot forcibly terminate such a coroutine. Waiting until it exits can hang the run indefinitely, and guaranteeing termination through isolation or a new ownership/supervision policy is a broader design change. Checking pending again alone would not terminate it.

Leaving this thread open, without dismissal or accepted-risk status, and asking the human for the scope decision. The independent Python 3.10 cancellation-marker correction is continuing through review and verification; it does not claim to remediate this limitation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The reviewed scope now explicitly defers hard termination of cancellation-resistant application callbacks. Commit 57a2c71 seals client recovery snapshots against late callback mutation, but it does not stop a coroutine that suppresses cancellation or bound aggregate retained tasks across repeated requests. Cleanup remains bounded to preserve prompt cancellation. This limitation is explicit in the PR description; this thread remains open and is not being represented as remediated.

Comment thread tests/test_tool_batch_failure_history.py
Comment thread tests/test_tool_batch_failure_history.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: 6c177ae685

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/run_internal/turn_resolution.py

@dpiet-oai dpiet-oai 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.

A completed side effect can still be forgotten when custom-data finalization exceeds the bounded wait; see the inline finding.

Comment thread src/agents/run_internal/turn_resolution.py Outdated
while not release.is_set():
try:
await release.wait()
except asyncio.CancelledError:

@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: 57a2c7122e

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/run.py
on_response_accepted=_commit_pending_server_response,
on_response_hooks_started=_mark_response_hooks_started,
run_state=run_state,
on_tool_execution_error=partial_tool_results.append,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve selected tool errors in non-streaming guardrail races

When a parallel input guardrail trips after a tool failure has been selected but while that tool batch is still draining post-invocation work, the non-streaming invocation wires only the partial-output callback and never receives the on_tool_error_selected signal used by the streamed path. The guardrail task can therefore finish first, causing the dedicated tripwire branch to cancel model_task and raise InputGuardrailTripwireTriggered, whereas the same ordering in run_streamed() preserves the selected tool error; this hides the actual tool failure and makes error handling depend on runner mode. Wire the selection signal through this path and apply the same precedence before handling a late tripwire.

AGENTS.md reference: AGENTS.md:L116-L116

Useful? React with 👍 / 👎.

Comment on lines +2803 to +2808
except BaseException:
accepting_outputs = False
if run_state is not None and not server_manages_conversation:
run_state._generated_items = _snapshot_tool_outputs(
run_state._generated_items, committed_tool_outputs
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Publish resumed-turn outputs in failure diagnostics

When an interrupted response resumes multiple approved tools and one completes before another raises an AgentsException, this failure path snapshots the completed output only into run_state._generated_items. The non-streaming runner still builds RunErrorDetails.new_items from its pre-resume session_items, and the streaming runner updates _model_input_items but not new_items, so both public error details omit the accepted output even though the supplied RunState contains it. Synchronize the public/session view from the checkpoint before re-raising so diagnostics accurately expose the side effect that recovery will skip.

AGENTS.md reference: AGENTS.md:L115-L115

Useful? React with 👍 / 👎.

@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 Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 57a2c7122e

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/agents/result.py
while not self._input_guardrail_queue.empty():
guardrail_result = self._input_guardrail_queue.get_nowait()
if guardrail_result.output.tripwire_triggered:
if guardrail_result.output.tripwire_triggered and not self._tool_error_selected:

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 Security Review · Automatically triggered

P2 Badge Security: Raise late tripwires after tool-local cancellation

Fresh evidence beyond the earlier stream-completion thread: when streaming with parallel input guardrails, a model-selected tool that propagates CancelledError sets _tool_error_selected; this condition then consumes a late tripwire without storing InputGuardrailTripwireTriggered. Because _check_errors() also ignores a cancelled run-loop task, stream_events() ends normally. A host relying on its documented exception to reject untrusted input can treat the request as a clean completion. Preserve the tripwire whenever the selected tool failure has no reportable stream exception.

SECURITY.md reference: SECURITY.md:L38-L38


Dismiss this finding: Reply with @codex security dismiss <reason> [context]. Codex will resolve this conversation automatically; GitHub may require a page refresh to show the result.

Valid reasons: false-positive, duplicate, out-of-scope, compensating-control, risk-accepted, or other. Example: @codex security dismiss duplicate Already flagged by another review

What each reason means
  • false-positive — Not a vulnerability
  • duplicate — Already tracked elsewhere
  • out-of-scope — Outside this review's scope
  • compensating-control — Mitigated by another control
  • risk-accepted — Risk intentionally accepted
  • other — Another reason; context required

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Finished tool call is dropped when a parallel tool's guardrail trips

3 participants