fix(orchestrator): Preserve claude/codex post-interrupt recovery state#4229
fix(orchestrator): Preserve claude/codex post-interrupt recovery state#4229mwolson wants to merge 2 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
12dfe4e to
2b5cda2
Compare
2b5cda2 to
2557fda
Compare
2557fda to
5e0f0d5
Compare
207e6fa to
ae99f40
Compare
cc5776d to
22db7bb
Compare
22db7bb to
8f2fb40
Compare
ApprovabilityVerdict: 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. |
8f2fb40 to
7a5217f
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ 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.
7a5217f to
6e96f42
Compare

Summary
notification arrives during session resume.
the command completion event.
terminals against each owning native thread during root Stop.
provider turns and threads have reached durable terminal state.
native turn without hiding a sole empty response or distinct later answer.
child timeline items when a stopped root turn leaves them open.
Stack
This draft targets
t3code/codex-turn-mappingand is stacked on #4193, the samepost-merge fixture-fix base used by #4218. This branch contains one unique
commit above that stack base.
Problem and Fix
task_notificationresult 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.item/completedfor an in-flight command, acceptturn/interruptwithout later sendingturn/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.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.Validation
vp check: pass with pre-existing warnings onlyvp run typecheck: passshell-state tests passed; full check and typecheck passed after refreshing
dependencies
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
turn_interrupt_mid_toolreplay and fixture-loading checks:pass
queued scheduler, projection, process ownership, and driver timeout gates
claude-interrupt-direct-stop: pass with one interrupted run and two distinctcompleted recovery runs
rendered as distinct assistant responses with no stuck Working state
during recovery 1; both recovery runs completed with their expected markers
in Claude and Codex and the stale Claude child root node
then reproduced two identical streamed final answers in one native turn
surviving root Stop and emitting a late final notification; the focused
descendant-turn replay now covers the exact native thread and process boundary
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.
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.
shell and sleep PIDs. The child provider turn became
interruptedand itsprovider thread became
idlebefore the root run terminalized. Recoverycompleted 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.
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
codex-interrupt-direct-stopon Codex 0.144.6: pass with run statuses[interrupted, completed, completed], no running command projection, and theforeground workload PID absent after Stop
The integration refresh also merged current
upstream/mainintoios-integrationand pushed that tested merge. Its merge intov2-baseisintentionally 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-terminallist 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/completedafter accepting an interrupt, locallyterminalize 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
completedduring that race, preserve theuser-requested
interruptedprojection.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 rootturn.terminaluntil descendant native turns settle. Stop now interrupts the full active lineage, terminates tracked background terminals (with list verification on ambiguousterminated: false), bounds wait with a 10s ceiling, and rescans for late-started descendants. Final-answeragentMessageitems are deduped (trailing empty or identical streamed text) while preserving sole-empty and distinct later answers. The message delta coalescer canemitEmpty: falseto drop empty completions.Provider turn start only includes routable subagents in related thread/provider IDs via
canRouteRelatedSubagent.CLI config tests expect state under
userdata-v2instead ofuserdata. 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
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.CodexAdapterV2, interrupting a turn now interrupts its entire descendant lineage, terminates associated background terminals viathread/backgroundTerminals/terminate, and emits terminal updates for in-flightcommandExecutionitems beforeturn.terminal.finalizeCodexTurnis serialized via a permit and defers root terminal events until descendant turns are inactive; duplicate or empty final answer emissions are suppressed.RunExecutionServicenow 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.Macroscope summarized 6e96f42.