Codex continues executing tool calls after SIGINT cancellation

Open 💬 3 comments Opened Jun 22, 2026 by N0zoM1z0

What version of Codex CLI is running?

@openai/codex 0.141.0

What subscription do you have?

N/A. The reproduction uses a local OpenAI-compatible mock provider and a fake API key.

Which model were you using?

gpt-4 in the local mock-provider replay.

What platform is your computer?

Linux x86_64 in Docker (node:24-bookworm-based target image).

What terminal emulator and version are you using?

Non-interactive Docker/CI shell.

Codex doctor report

Not available in this minimal Docker reproduction.

What issue are you seeing?

If SIGINT interrupts an in-flight provider stream, Codex can still consume a later tool call from that cancelled stream, execute it repeatedly, and submit tool results back to the provider until the bounded run kills the process.

What steps can reproduce the bug?

Prerequisites: Docker, Python 3, and the GitHub CLI (gh) for the clone command below. The linked reproducer is self-contained and uses only Python standard-library modules plus Docker. It builds the affected CLI version from the public package registry, starts a local mock provider, and runs the CLI in an isolated workspace. The Docker run is limited to 2 CPUs and 4 GiB RAM by default.

Complete self-contained reproducer: https://gist.github.com/N0zoM1z0/56a582c18dec7b72ac41a9225e668080

The Gist contains codex-late-effect-after-cancel.reproduce.py. Download and run:

gh gist clone 56a582c18dec7b72ac41a9225e668080 codex-late-effect-after-cancel-reproducer
cd codex-late-effect-after-cancel-reproducer
python3 codex-late-effect-after-cancel.reproduce.py

To reuse an already-built local image:

python3 codex-late-effect-after-cancel.reproduce.py --skip-build

Key output from a local run against the affected version:

process_exit=137
process_timed_out=True
provider_requests=24
tool_result_counts={'late_after_cancel': 276}
tool_result_after_signal: observed=276 expected>=1
side_effect_file_contains codex-late-effect-after-cancel.log 'codex-late-effect-after-cancel': observed=True
REPRODUCED

What is the expected behavior?

After SIGINT cancellation, Codex should stop consuming the in-flight provider stream and should not execute or submit any later tool calls from that cancelled stream.

Additional information

Public-upstream status: This reproduces against stock public @openai/codex@0.141.0 built from npm. This is separate from the existing interrupted-child cleanup report: the trigger here is a cancellation boundary followed by a late provider tool call.

Observed evidence

The mock provider delays its first tool call until after the reproducer sends SIGINT. Codex still executes the late late_after_cancel shell tool, writes the side-effect marker file, and submits many tool results before the bounded run times out.

View original on GitHub ↗

2 Comments

ofeliacode · 2 months ago

I investigated this and the likely race is in the turn stream loop.

stream.next().or_cancel(&cancellation_token) can still return a late provider event if the stream
item and cancellation are both ready around the same boundary. That late
ResponseEvent::OutputItemDone(FunctionCall/CustomToolCall) then reaches
handle_output_item_done, which records the item and queues a tool future using a child
cancellation token.

A minimal fix would be to add cancellation guards:

  • immediately after receiving a stream event and before processing it in try_run_sampling_request
  • inside handle_output_item_done before recording/queuing a model-emitted tool call

Expected behavior: once the turn cancellation token is cancelled, late stream events should resolve
as CodexErr::TurnAborted and must not enqueue tool execution.

I have a local proof-of-fix with a focused regression test that verifies a cancelled output item
does not queue a tool call. Happy to open a PR if this approach matches the intended direction.

d3v07 · 2 months ago

Building on @ofeliacode's analysis above, I put together a focused branch implementing this approach:

https://github.com/d3v07/codex/tree/fix/29439-sigint-late-tool-calls

The fix guards two points in the turn stream path:

  • after receiving a stream event, before telemetry or event handling side effects
  • inside handle_output_item_done, before recording or queueing a model-emitted tool call

That makes a late FunctionCall/tool-call item return TurnAborted instead of being dispatched after the turn cancellation token is already cancelled.

Validation run:

  • python3 ../scripts/format.py --check
  • just fix -p codex-core
  • RUST_MIN_STACK=8388608 cargo test -p codex-core stream_events_utils::tests
  • RUST_MIN_STACK=8388608 ./target/debug/deps/all-878c4e0ec6cfe0a6 abort_tasks --nocapture

Happy to open the PR if this approach matches the intended direction.

Showing cached comments. Read the full discussion on GitHub ↗