Skip to content

fix(orchestrator): Preserve claude/codex post-interrupt recovery state#4229

Draft
mwolson wants to merge 2 commits into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/claude-codex-interrupt-recovery
Draft

fix(orchestrator): Preserve claude/codex post-interrupt recovery state#4229
mwolson wants to merge 2 commits into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:fix/claude-codex-interrupt-recovery

Conversation

@mwolson

@mwolson mwolson commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Preserve the first Claude user turn after Stop when a stale background task
    notification arrives during session resume.
  • Terminalize Codex command projections when interrupted or failed turns omit
    the command completion event.
  • Interrupt active Codex descendant turns and terminate tracked background
    terminals against each owning native thread during root Stop.
  • Delay interrupted or failed Codex root terminal events until descendant
    provider turns and threads have reached durable terminal state.
  • Suppress redundant empty or identical Codex final-answer items in the same
    native turn without hiding a sole empty response or distinct later answer.
  • Terminalize run-owned subagent projections, linked child nodes, and linked
    child timeline items when a stopped root turn leaves them open.
  • Add deterministic regressions for the observed Claude and Codex frame orders.

Stack

This draft targets t3code/codex-turn-mapping and is stacked on #4193, the same
post-merge fixture-fix base used by #4218. This branch contains one unique
commit above that stack base.

Problem and Fix

Problem and Why it Happened Fix
Claude can emit a background task_notification result between the first post-Stop prompt and its real response. The adapter treated that result as the active root result, completing the recovery run with no assistant output. Detect task-notification-origin results and ignore them for normal user turns before fallback text or terminalization. Provider continuation turns still consume the same result shape while draining buffered wakes.
Codex app-server can omit item/completed for an in-flight command, accept turn/interrupt without later sending turn/completed, and keep descendant native turns running after the root stops. Its interrupt response also does not contain provider-owned background terminals. The projected run could look interrupted while a child shell survived Stop and later emitted a final notification. A root terminal arriving before its child provider terminal events also stopped run ingestion and stranded the durable child provider turn and thread as running. Track command snapshots and process IDs per native turn. Interrupt the active root lineage, terminate each tracked terminal against its owning native thread immediately and after settlement, bound the native completion wait, and terminalize projections before turn.terminal. Defer non-completed root terminal events until active descendant turns have terminalized, including timeout and interrupt-request failure paths. Verify ambiguous false results against the paginated terminal list.
Codex app-server can emit multiple final-answer items in one native turn, including a trailing empty item or a second streamed item with text identical to the first. The UI selected the empty item as terminal or rendered the same recovery response twice. Track completed final texts per native turn, buffer later final streams, and discard later empty or identical completions. Preserve sole empty answers, distinct later finals, commentary, unknown-phase compatibility, buffered non-empty text, and subagent results.
Claude and Codex can stop the root run while its subagent entity, node, turn item, or linked child timeline remains running. Cascade interrupted, failed, or cancelled status to open run-owned subagent records, linked child nodes, and projected child turn items before root finalization, while preserving child linkage and partial result text.

Validation

  • vp check: pass with pre-existing warnings only
  • vp run typecheck: pass
  • Codex adapter tests: 40 passed, including descendant-turn interruption,
    owning-thread terminal containment, bounded interrupt fallback, late native
    completion rejection, redundant empty and identical-final suppression,
    overlapping streams, sole-empty preservation, unknown phases, zero-length
    deltas, subagent result safety, and child provider terminal ordering after
    native completion, timeout, interrupt-request failure, and a descendant that
    starts after the initial Stop snapshot
  • Targeted Codex turn_interrupt_mid_tool replay and fixture-loading checks:
    pass
  • Parent interrupt-safety fixtures: pass, including simulated Darwin cleanup,
    queued scheduler, projection, process ownership, and driver timeout gates
  • claude-interrupt-direct-stop: pass with one interrupted run and two distinct
    completed recovery runs
  • Isolated paired browser verification: both Claude and Codex recovery messages
    rendered as distinct assistant responses with no stuck Working state
  • Packaged AppImage Claude race: an empty task-notification result arrived
    during recovery 1; both recovery runs completed with their expected markers
  • Packaged AppImage foreground-subagent probes: reproduced the stale parent row
    in Claude and Codex and the stale Claude child root node
  • Packaged AppImage Codex recovery probe: confirmed containment and recovery,
    then reproduced two identical streamed final answers in one native turn
  • Manual packaged Codex child-Stop probe: reproduced the native child command
    surviving root Stop and emitting a late final notification; the focused
    descendant-turn replay now covers the exact native thread and process boundary
  • Final packaged Codex descendant-Stop probe: pass. Root Stop interrupted the
    child native turn, terminated its tracked foreground process, kept the child
    command and parent subagent interrupted, and completed one same-thread
    recovery response. No forbidden child output appeared through the original
    command deadline or the additional late-event polling window.
  • Follow-up manual Codex Stop probe: process containment and visible recovery
    passed, but the root terminal reached the run stream before the child
    provider terminal events. The durable child provider turn remained running
    and its provider thread remained active, which the new ordering regressions
    reproduce.
  • Shared run-execution tests: 18 passed, including interrupt cascade,
    failed/cancelled mapping, supersede protection, linked-child cleanup,
    linked-child timeline cleanup, unreferenced-child exclusion, suppressed
    streaming exclusion, partial result preservation, and same-attempt and
    cross-attempt late-event rejection
  • Strict codex-interrupt-direct-stop on Codex 0.144.6: pass with run statuses
    [interrupted, completed, completed], no running command projection, and the
    foreground workload PID absent after Stop

Defensive Fixes

If Codex returns terminated: false, query the paginated background-terminal
list before deciding whether containment succeeded. Treat an absent process as
already contained, fail if it remains listed, attempt every tracked process ID,
and always clear interrupt bookkeeping.

If Codex never emits turn/completed after accepting an interrupt, locally
terminalize after the same 10-second ceiling used by sibling adapters. A shared
finalization permit prevents the timeout and a racing native completion from
publishing duplicate terminal events; late native events cannot reopen the
settled turn.

When root Stop has active native descendant turns, install completion waiters
for the full active lineage before sending interrupts. Treat a target that
settled during waiter setup as already complete. Run the first terminal sweep
concurrently with the single completion deadline, then sweep again after
settlement so commands observed during the interrupt race are also contained. If
a marked native target reports completed during that race, preserve the
user-requested interrupted projection.

For non-completed Codex roots, retain the root terminal event while any native
descendant turn remains active. Descendant finalization publishes its provider
turn and provider-thread terminal updates first, then releases the root event.
Interrupt RPC failures locally terminalize remaining targets before release,
and the existing finalization permit serializes native completion, timeout, and
failure cleanup.

Before Stop returns, rescan the interrupted native lineage and interrupt plus
terminalize descendants that started after the initial snapshot. Native child
starts that arrive after an ancestor has terminalized non-completed are
interrupted without creating a new running provider projection.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e857dabf-8715-48df-893e-e3657ab12aaf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 21, 2026
@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch from 12dfe4e to 2b5cda2 Compare July 21, 2026 12:55
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 21, 2026
@mwolson mwolson changed the title fix(claude): Preserve recovery turns across task notifications fix(orchestration): Preserve post-interrupt recovery state Jul 21, 2026
@mwolson mwolson changed the title fix(orchestration): Preserve post-interrupt recovery state fix(orchestrator): Preserve post-interrupt recovery state Jul 21, 2026
@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch from 2b5cda2 to 2557fda Compare July 21, 2026 14:28
Comment thread apps/server/src/orchestration-v2/RunExecutionService.ts
@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch from 2557fda to 5e0f0d5 Compare July 22, 2026 01:03
@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch 2 times, most recently from 207e6fa to ae99f40 Compare July 22, 2026 17:11
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 22, 2026
@mwolson mwolson changed the title fix(orchestrator): Preserve post-interrupt recovery state fix(orchestrator): Preserve claude/codex post-interrupt recovery state Jul 22, 2026
@mwolson
mwolson marked this pull request as ready for review July 22, 2026 18:04
@mwolson
mwolson marked this pull request as draft July 22, 2026 18:08

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ae99f40. Configure here.

threadId,
turnId: activeTurn.nativeTurnId,
});
yield* Deferred.await(turnCompleted);

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.

Interrupt awaits completion indefinitely

Medium Severity

interruptTurn now blocks on Deferred.await(turnCompleted) with no timeout after turn/interrupt. If Codex never emits turn/completed, the provider-turn.interrupt outbox effect never finishes, leaving interrupt bookkeeping and the worker fiber stuck even though the user already saw an interrupted projection.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ae99f40. Configure here.

@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch 4 times, most recently from cc5776d to 22db7bb Compare July 22, 2026 22:42
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Jul 22, 2026
Comment on lines +58 to 66
const completedIndex = transcript.entries.findIndex(
(entry) => entry.type === "emit_inbound" && protocolMethod(entry.frame) === "turn/completed",
);
const terminateIndex = transcript.entries.findIndex(
(entry) =>
entry.type === "expect_outbound" &&
protocolMethod(entry.frame) === "thread/backgroundTerminals/terminate",
);
assert.isAtLeast(commandIndex, 0, "Codex interrupt fixture must record command execution start");

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.

🟡 Medium turn_interrupt_mid_tool/codex_output.ts:58

assert.isAbove(terminateIndex, completedIndex) passes even when completedIndex is -1, so this fixture can accept a thread/backgroundTerminals/terminate request that was sent without a corresponding turn/completed frame. That defeats the regression check — termination should be verified to occur only after completion is confirmed. Add an explicit nonnegative assertion for completedIndex before the comparison.

  const completedIndex = transcript.entries.findIndex(
    (entry) => entry.type === "emit_inbound" && protocolMethod(entry.frame) === "turn/completed",
  );
+ assert.isAtLeast(
+   completedIndex,
+   0,
+   "Codex interrupt fixture must record turn completion before termination",
+ );
  const terminateIndex = transcript.entries.findIndex(
    (entry) =>
      entry.type === "expect_outbound" &&
      protocolMethod(entry.frame) === "thread/backgroundTerminals/terminate",
  );
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt_mid_tool/codex_output.ts around lines 58-66:

`assert.isAbove(terminateIndex, completedIndex)` passes even when `completedIndex` is `-1`, so this fixture can accept a `thread/backgroundTerminals/terminate` request that was sent without a corresponding `turn/completed` frame. That defeats the regression check — termination should be verified to occur only after completion is confirmed. Add an explicit nonnegative assertion for `completedIndex` before the comparison.

@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch from 22db7bb to 8f2fb40 Compare July 22, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant