Skip to content

fix(clients): native subagent threads show when they are working - #13614

Merged
juliusmarminge merged 2 commits into
t3code/codex-turn-mappingfrom
v2/subagent-working-state
Sep 25, 2026
Merged

juliusmarminge merged 2 commits into
t3code/codex-turn-mappingfrom
v2/subagent-working-state

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Depends on #13619: merge it first or together. Without it, a server restart mid-subagent leaves the child's runless root turn running, and this PR would show that child as "Working 3h" forever.

Opening a provider-native subagent's thread (for example one of the three Claude Agent-tool subagents in an audit prompt) showed the task message and a work log, but no "Working for" timer, no live tool row, and no Thinking slot while the subagent was still running. The parent thread showed "3 working" at the same time.

Root cause. Provider-native subagent threads never get app runs (fixtures assert this on purpose via assertNoExtraAppRunsForProviderChildren). Their work is a single runless root_turn node whose status follows the subagent. Every working signal on web and mobile is derived from projection.runs, so the child always looked idle.

What changed

  • packages/client-runtime deriveRunlessWorkStartedAt(projection): the start time of the thread's active runless root turn, or null. It reads nodes the child projection already has, so it adds no websocket payload.
  • Web (ChatView): isWorking and activeWorkStartedAt also come from that helper, which brings back the working header and its timer. phase is unchanged, so stop, queue, steer, and interrupt stay run-only (a native subagent can't be stopped from its own thread). MessagesTimeline gets runlessWorkActive, so runless entries count as the live response while the root turn is active: a running tool becomes the live activity row, and the Thinking slot fills the gap between tools.
  • Mobile: activeWorkStartedAt falls back to the same helper, which brings back the floating working timer and the Thinking row. deriveThreadFeedPresentation takes an explicit runlessWorkActive flag (as web does): a runless tail group goes live only for runless subagent work, never for a normal thread in the optimistic-send window or with a queued latest run. Null-run activities were already matched against the null activeRunId.
  • Replay-fixture invariant (assertProviderNativeSubagentRootTurns, run after every fixture). For each provider-native subagent it checks that the child is creationSource: "provider", is forked from the subagent node, has no runs, and has only runless root turns. It also checks that the root turn is running before the child's first non-prompt item, and that its active/terminal sequence matches the subagent's (including the Claude SendMessage resume in claude_background_subagent_lifecycle, which goes running → completed → running → completed). All 23 native subagents recorded in 13 fixture/provider variants (Claude, Codex v1/v2/nested/continue, Cursor, Grok, OpenCode) satisfy it.

Delegated delegate_task children have real runs, so their behavior doesn't change.

Verification

  • packages/client-runtime: vp test run src/state/threadExecution.test.ts src/state/entities.test.ts: 39 passed. New cases: running root turn gives its start time with a null runtime (still no interrupt); each terminal status and idle gives null; a run-owned root turn is ignored.
  • apps/web: vp test run src/components/chat/MessagesTimeline.logic.test.ts src/session-logic.test.ts: 176 passed. New cases: a runless running tool becomes the live work-live row under the working header, and reads as settled history once the subagent stops; runless entries are not live on a thread that has a run. I reverted the logic change and confirmed the first case fails.
  • apps/mobile: vp test run src/lib/threadActivity.test.ts: 72 passed. The runless live-tail case fails without the fix; a second case checks that a normal thread in the optimistic window (no run, or a queued latest run) keeps its runless tail settled, and it fails without the runlessWorkActive gate.
  • apps/server: vp test run src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts: 87 passed with the new invariant.
  • tsc --noEmit on client-runtime, web, mobile, server: no errors. vp lint on touched files: no new findings. vp run knip:check: clean.
  • Not run: browser, desktop, or simulator checks (no dev servers, per maintainer rule), and repo-wide checks.

Model: Claude Opus 5.5 (Claude Code)

🤖 Generated with Claude Code


Devin Review

Provider-native subagent threads (Claude Agent tool, Codex/Cursor/Grok/
OpenCode native subagents) never get app runs; their work is a runless
root turn whose status follows the subagent. Every working signal on web
and mobile came from runs, so an open child thread looked idle: no
"Working for" timer, no live tool row, no Thinking slot.

Derive the working start from the child's active runless root turn in
client-runtime and feed it into the existing working state on web and
mobile. Runless timeline entries count as the live response only while
that root turn is active. Stop, queue, and steer stay run-only.

A replay-fixture invariant pins the server contract for every recorded
native subagent: the child hangs off the subagent node, has only runless
root turns, is running before its first item, and its root turn follows
the subagent's activity, including a Claude resume re-opening it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 4.9 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.1 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 20.4 KiB — 29.3 KiB ✅
Codex Live turn messages — 1 — 8 ✅
Claude Total thread wire — 4.9 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 20.8 KiB — 29.3 KiB ✅
Claude Live turn messages — 2 — 8 ✅

Baseline: unavailable · PR result: 6a93fae · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 106.1 KiB
  • Claude decoded thread snapshot: 106.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Sep 25, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 6a93fae

Macroscope's review found this PR approvable — This is a targeted client presentation fix that surfaces existing runless subagent activity in web and mobile without changing ordinary run-backed behavior or persistence contracts. The added tests cover active, terminal, and optimistic-send cases, while server changes are limited to replay-fixture validation.

You can add or adjust custom eligibility rules. Learn more.

Dropping the active-run check let any working thread without an unsettled
run (the optimistic-send window, or a queued latest run) mark a runless
tail group live with shimmer. Pass an explicit runlessWorkActive flag, as
the web timeline does, and live-mark a runless tail only for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@macroscopeapp
macroscopeapp Bot dismissed their stale review September 25, 2026 10:12

Dismissing prior approval to re-evaluate 6a93fae

@juliusmarminge
juliusmarminge merged commit b64278c into t3code/codex-turn-mapping Sep 25, 2026
24 of 25 checks passed
@juliusmarminge
juliusmarminge deleted the v2/subagent-working-state branch September 25, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant