Skip to content

fix(runtime): bound the interrupt write so cancelling never waits on a stalled provider - #42

Merged
dviejokfs merged 2 commits into
mainfrom
fix/background-command-draining
Oct 12, 2026
Merged

dviejokfs merged 2 commits into
mainfrom
fix/background-command-draining

Conversation

@dviejokfs

Copy link
Copy Markdown
Contributor

What

Bound the adapter-encoded interrupt write so cancelling a turn never waits on a provider that has stopped reading its stdin.

A provider busy inside a tool call can leave its stdin pipe full. cancel_running_process (one-shot turns) and settle_retained_interrupt (retained turns) wrote the interrupt frame with no bound, so cancellation, and the process-tree cleanup after it, waited until the tool ended on its own. The write now gives up after INTERRUPT_WRITE_TIMEOUT (1 s) through one helper, write_interrupt_frame. A timeout is treated exactly like an interrupt the provider did not accept: the writer is dropped and the provider is terminated, as before.

No public API changes. Behaviour on a healthy provider is unchanged.

Where this came from

It is the leftover of the uncommitted work in the fix/windows-runtime-compatibility worktree (whose PR #16 had already merged). Those edits were written against a base before #36, #37, #40 and #41, and are mostly superseded or in conflict with deliberate design. Each change was checked against current main, with live runs against real CLIs (Claude Code 2.1.296, codex-cli 0.162.1) where the behaviour is not visible in a unit test.

Leftover change Result Why
Bound the interrupt write Ported (this PR) Failing tests on main: both cancellation tests below time out.
INTERRUPT_GRACE 5 s to 250 ms Dropped No demonstrated gap. With a real Codex app-server running a foreground sleep 90, cancel to return took 49 ms on main; the grace is only a ceiling for a provider that ignores the interrupt, and the shorter value risks truncating session persistence.
Claude: keep stdin open until a result with no live tasks (drop result_seen-driven close on drain) Dropped Not reproducible: a live chained flow (background command, completion launches a second command, then ALL_DONE) ran to completion on main with both markers written. It also removes the one-shot close-on-drain that background_work_drained documents on purpose, and with no follow-up grace in one-shot mode would hold a turn to its timeout whenever Claude does not answer the notification. Retained turns already handle follow-ups (#36).
Codex app-server: hold turn/completed until started commandExecution items finish (cap 256) Dropped Conflicts with the existing retention design (#22): background terminals are kept by a retained app-server (background_terminal_retention_smoke on main: listening after the turn, after 4 idle timeouts and after a follow-up turn). The change was not scoped to one-shot turns, so a retained turn that starts a dev server would be held open forever, which is what #36 removed for Claude. For one-shot turns the app-server exiting with its children is documented behaviour; this PR documents it more precisely.
docs/background-commands.md, examples/background_command_probe.rs Dropped They describe the dropped Claude/Codex behaviour; on this base the probe reports a documented limitation as a failure.

Tests

Both written first and failing on main (8 s outer timeout elapsed):

  • cancellation_does_not_wait_for_a_provider_that_stopped_reading_stdin: one-shot cancel with a 1-byte stdin pipe the fixture never reads; asserts Cancelled and that the provider was terminated.
  • interrupting_a_retained_turn_does_not_wait_for_a_stalled_provider: the same through the retained interrupt path.

New fixture Script::StalledInterrupt in tests/codex_app_server.rs.

Gates run locally: cargo fmt --all -- --check, cargo clippy --locked --all-targets --all-features -- -D warnings, cargo test --locked --all-features (all pass).

Docs and changelog

  • CHANGELOG.md: Fixed entry.
  • docs/how-to/keep-tool-processes-running.md: new section "Background commands of a one-shot Codex turn" (observed with codex-cli 0.162: a sleep 15 started by a turn that had ended never wrote its finish marker) pointing at process retention and ManagedProcessSupervisor.

🤖 Generated with Claude Code

…a stalled provider

A provider busy inside a tool call can stop reading its stdin and leave
the pipe full. The adapter-encoded interrupt was written without a bound
in both the one-shot cancel path and the retained interrupt path, so
cancellation (and the process-tree cleanup after it) waited until the
tool ended by itself. Give the write 1 second and treat a timeout like
an interrupt the provider did not accept: stop using the writer and
terminate the provider.

Also document that a one-shot Codex turn takes its background commands
with the app-server and point at process retention.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; both previous findings are fixed and no new blocking issue remains.

Summary

The PR limits interrupt writes to one second so a provider that stops reading stdin cannot hold up cancellation.

  • Turn cancellation moves on when a provider stops reading stdin.
  • The guide explains why one-shot Codex background commands end.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Cancel turn] --> B[Write interrupt frame]
  B --> C{Written within one second?}
  C -->|Yes| D[Continue existing interrupt cleanup]
  C -->|No| E[Stop using the writer]
  E --> F[Terminate provider]
Loading

Reviews (2) · Last reviewed commit: "test(runtime): assert the retained stall..." · Reviewed by Greptile

Comment thread tests/codex_app_server.rs Resolved
Comment thread CHANGELOG.md Resolved
…lation

Also remove a duplicated phrase the previous changelog edit introduced
in the neighbouring entry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@dviejokfs
dviejokfs merged commit a88ed14 into main Oct 12, 2026
7 checks passed
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.

1 participant