Skip to content

feat(orchestrator): Surface waiting background work#4378

Draft
mwolson wants to merge 1 commit into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:feat/orchestrator-v2-background-waiting
Draft

feat(orchestrator): Surface waiting background work#4378
mwolson wants to merge 1 commit into
pingdotgg:t3code/codex-turn-mappingfrom
mwolson:feat/orchestrator-v2-background-waiting

Conversation

@mwolson

@mwolson mwolson commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Relationship to #4415

#4415 owns the frontend
Waiting direction on main: an idle provider session renders as Waiting in the
web and mobile thread lists. This PR supplies the orchestrator-v2 counterpart.
It projects the provider-thread background roster, keeps late roster clears
observable, and presents a nonempty post-settlement roster as runtime idle.

This PR intentionally does not carry its former sidebar or mobile status
precedence changes. The GitHub base remains t3code/codex-turn-mapping so the
diff contains only the orchestrator-v2 delta. Using #4415's branch as the
literal GitHub base would include the entire CTM stack in this PR. The intended
integration order is #4415 into main, CTM absorbs main, then this focused
delta completes Waiting for orchestrator v2.

Problem and Fix

Problem and Why it Happened Fix
A completed root turn can still own finite background work, but orchestrator v2 had no normalized post-settlement signal. Derive one pending list from provider-thread rosters and nonterminal command, dynamic-tool, and subagent items.
Closing provider event ingestion as soon as the root settled could miss the final roster clear. Keep ingestion open while that provider thread reports finite background work, then persist the late empty roster.
A session-wide pending probe could couple unrelated provider threads on a shared runtime. Use a thread-scoped probe for root-run ingestion while retaining the session-wide probe for provider idle release.
CTM runtime status can remain transiently running or checkpoint waiting after the authoritative post-settlement roster arrives. Normalize a nonempty roster to runtime idle in client presentation while preserving the original status in latestRun.
Existing stored provider threads and shells omit the new roster. Decode pendingBackgroundTasks with an empty-array default.

Defensive Fixes

Problem and Why it Happened Fix
Claude roster snapshots and completion edges can arrive in either order. Track the authoritative roster separately from wake eligibility and replay classification.
Replacing a Claude query kills the old process before it can clear its roster. Clear process-scoped state while preserving only explicitly classified buffered Bash notifications.
A Grok subagent can outlive the root turn through adapter carryover. Include live carryover subagents in the pending-work probe.
Persisted background state can survive a restart without a provider capable of completing it. Clear stale rosters and terminalize stale background-capable items during recovery.

Validation

  • Focused tests pass for Claude and ACP adapters, run execution, provider
    recovery, contracts, shared pending-work derivation, client presentation, and
    the web timeline.
  • Affected package typechecks pass for contracts, shared, client-runtime,
    server, and web.
  • Targeted vp check passes for every changed file, and git diff --check is
    clean.
  • The final branch is exactly one commit on live
    t3code/codex-turn-mapping.
  • The client presentation regressions cover stale running and checkpoint
    waiting shell states without restoring direct sidebar roster precedence.

@coderabbitai

coderabbitai Bot commented Jul 23, 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: a7c4ec12-315e-444c-a41c-c3a18653b967

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:XXL 1,000+ changed lines (additions + deletions). labels Jul 23, 2026

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

const recoveredNonterminalRunIds = new Set(runs.map((run) => run.id));

isBackgroundCapableTurnItemType causes replayable waiting runs to have their background-capable turn items (command_execution, dynamic_tool, subagent) cancelled, even though those runs were intentionally skipped from terminalization because a pending/running checkpoint effect still exists. When a waiting run has a replayable checkpoint, reconcileProjection omits it from runs, so its ID is absent from recoveredNonterminalRunIds. The later stale-item loop then matches that run's items via isBackgroundCapableTurnItemType + isNonterminalTurnItemStatus and emits turn-item.updated cancellation events, destroying items that belong to a still-recoverable run and checkpoint. The stale-item loop needs to also exclude turn items whose runId belongs to a replayable waiting run, not just those already in recoveredNonterminalRunIds.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts around line 304:

`isBackgroundCapableTurnItemType` causes replayable `waiting` runs to have their background-capable turn items (command_execution, dynamic_tool, subagent) cancelled, even though those runs were intentionally skipped from terminalization because a pending/running checkpoint effect still exists. When a `waiting` run has a replayable checkpoint, `reconcileProjection` omits it from `runs`, so its ID is absent from `recoveredNonterminalRunIds`. The later stale-item loop then matches that run's items via `isBackgroundCapableTurnItemType` + `isNonterminalTurnItemStatus` and emits `turn-item.updated` cancellation events, destroying items that belong to a still-recoverable run and checkpoint. The stale-item loop needs to also exclude turn items whose `runId` belongs to a replayable `waiting` run, not just those already in `recoveredNonterminalRunIds`.

@mwolson
mwolson force-pushed the feat/orchestrator-v2-background-waiting branch from e46af0a to f318877 Compare July 24, 2026 04:04
// After the root turn terminals the run leaves "running", so
// writeIfRunCurrent would drop late provider_thread.updated
// roster clears. Only gate pre-terminal root-thread updates.
const rootTerminalAlreadySeen = yield* Ref.get(rootTerminalSeen);

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 orchestration-v2/RunExecutionService.ts:738

After the root terminal is observed, writeIfRunCurrent is stripped from all provider_thread.updated events for the root thread — including events from a superseded attempt whose subscription stays open for background work. When that stale subscription receives a late provider_thread.updated and persists the old providerThread snapshot, it overwrites the new attempt's thread state (for example reverting status or roster cleared by the new attempt). The gate should use a settled-run-safe conditional write or field-specific merge rather than bypassing attempt ownership entirely based on rootTerminalAlreadySeen.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/RunExecutionService.ts around line 738:

After the root terminal is observed, `writeIfRunCurrent` is stripped from all `provider_thread.updated` events for the root thread — including events from a superseded attempt whose subscription stays open for background work. When that stale subscription receives a late `provider_thread.updated` and persists the old `providerThread` snapshot, it overwrites the new attempt's thread state (for example reverting status or roster cleared by the new attempt). The gate should use a settled-run-safe conditional write or field-specific merge rather than bypassing attempt ownership entirely based on `rootTerminalAlreadySeen`.

@juliusmarminge
juliusmarminge force-pushed the t3code/codex-turn-mapping branch 2 times, most recently from 1e58e65 to a286c60 Compare July 24, 2026 13:37
@mwolson
mwolson force-pushed the feat/orchestrator-v2-background-waiting branch from f318877 to 2bf4cdf Compare July 24, 2026 15:32
if (providerInstanceId === undefined) {
continue;
}
events.push({

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 orchestration-v2/ProviderRuntimeRecoveryService.ts:319

A stale subagent turn item on an already-settled run gets only a turn-item.updated event, leaving the matching projection.subagents entry in pending/running/waiting indefinitely while its turn item becomes cancelled. The existing nonterminal-run cancellation loop above emits both subagent.updated and turn-item.updated, so the new settled-run path produces contradictory state — downstream code reading the subagent entity will treat a dead subagent as active. The settled-run loop should also emit a subagent.updated event (status cancelled) for matching subagent entries.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts around line 319:

A stale `subagent` turn item on an already-settled run gets only a `turn-item.updated` event, leaving the matching `projection.subagents` entry in `pending`/`running`/`waiting` indefinitely while its turn item becomes `cancelled`. The existing nonterminal-run cancellation loop above emits both `subagent.updated` and `turn-item.updated`, so the new settled-run path produces contradictory state — downstream code reading the subagent entity will treat a dead subagent as active. The settled-run loop should also emit a `subagent.updated` event (status `cancelled`) for matching subagent entries.

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