Skip to content

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

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

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
  • Refreshed orchestrator-v2 snapshot: 58 Codex and run-execution tests plus 27
    shell-state tests passed; full check and typecheck passed after refreshing
    dependencies
  • 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.
  • Final rebuilt AppImage Codex probe: pass. Stop removed the exact descendant
    shell and sleep PIDs. The child provider turn became interrupted and its
    provider thread became idle before the root run terminalized. Recovery
    completed exactly once on a distinct run, with no late forbidden output or
    running projection through the original 90-second command deadline and an
    additional residual window. Cleanup deleted the provider child before the root
    and verified that no unrelated thread changed.
  • 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

The integration refresh also merged current upstream/main into
ios-integration and pushed that tested merge. Its merge into v2-base is
intentionally deferred because current main crosses a broad protocol and runtime
compatibility boundary with CTM. The packaged snapshot therefore uses the last
compatible B1 base plus the complete refreshed layer stack.

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.


Note

High Risk
Changes core Claude/Codex turn finalization, interrupt/stop containment, and root terminal ordering in the orchestrator; regressions could strand child work, drop valid assistant output, or mis-report run status.

Overview
Hardens orchestration-v2 provider adapters so Stop/interrupt and stale provider frames no longer leave runs stuck, empty, or duplicated.

Claude: SDK result messages with origin.kind === "task-notification" are skipped on normal user turns so stale background notifications after interrupt recovery cannot finalize the run or inject fallback assistant text; provider continuation turns still process those results when draining wake buffers.

Codex: Turn handling is reworked around shared finalizeCodexTurn—serializes completion vs timeout, terminalizes in-flight commandExecution items on interrupt/failure, and defers root turn.terminal until descendant native turns settle. Stop now interrupts the full active lineage, terminates tracked background terminals (with list verification on ambiguous terminated: false), bounds wait with a 10s ceiling, and rescans for late-started descendants. Final-answer agentMessage items are deduped (trailing empty or identical streamed text) while preserving sole-empty and distinct later answers. The message delta coalescer can emitEmpty: false to drop empty completions.

Provider turn start only includes routable subagents in related thread/provider IDs via canRouteRelatedSubagent.

CLI config tests expect state under userdata-v2 instead of userdata. Large replay/regression coverage was added for the above frame orderings.

Reviewed by Cursor Bugbot for commit 6e96f42. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Preserve post-interrupt recovery state for Claude and Codex adapters

  • In ClaudeAdapterV2, task-notification-origin result messages are now ignored during normal user turns and only consumed during provider continuation turns, preventing spurious assistant text or premature turn finalization.
  • In CodexAdapterV2, interrupting a turn now interrupts its entire descendant lineage, terminates associated background terminals via thread/backgroundTerminals/terminate, and emits terminal updates for in-flight commandExecution items before turn.terminal.
  • finalizeCodexTurn is serialized via a permit and defers root terminal events until descendant turns are inactive; duplicate or empty final answer emissions are suppressed.
  • RunExecutionService now tracks open run-owned subagents and cascades terminal events (subagent.updated, node.updated, turn_item.updated) before root run settlement when a run ends interrupted, failed, or cancelled.
  • Non-completed root terminals now stop event ingestion promptly; completed terminals continue until background-capable items drain.
  • Risk: Significant behavioral changes to turn finalization ordering and interrupt propagation across the Codex adapter and run execution service.

Macroscope summarized 6e96f42.

@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 Plus

Run ID: f86bd4fa-2c95-42e5-a2dc-772f670549ec

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
Comment thread apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts Outdated
@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
@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch from 22db7bb to 8f2fb40 Compare July 22, 2026 23:38
@mwolson
mwolson marked this pull request as ready for review July 23, 2026 00:02
@macroscopeapp

macroscopeapp Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces significant new interrupt handling machinery beyond a simple fix: cascade termination logic for subagents, new RPC calls for background terminal termination, deferred terminal events, and complex state coordination. These runtime behavior changes warrant careful review despite the comprehensive test coverage.

You can customize Macroscope's approvability policy. Learn more.

@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch from 8f2fb40 to 7a5217f Compare July 23, 2026 00:44

@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 7a5217f. Configure here.

Comment thread apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
@mwolson
mwolson force-pushed the fix/claude-codex-interrupt-recovery branch from 7a5217f to 6e96f42 Compare July 23, 2026 01:26
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