fix(clients): native subagent threads show when they are working - #13614
Conversation
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>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 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.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Approved at 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>
Dismissing prior approval to re-evaluate 6a93fae
b64278c
into
t3code/codex-turn-mapping
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 runlessroot_turnnode whose status follows the subagent. Every working signal on web and mobile is derived fromprojection.runs, so the child always looked idle.What changed
packages/client-runtimederiveRunlessWorkStartedAt(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.ChatView):isWorkingandactiveWorkStartedAtalso come from that helper, which brings back the working header and its timer.phaseis unchanged, so stop, queue, steer, and interrupt stay run-only (a native subagent can't be stopped from its own thread).MessagesTimelinegetsrunlessWorkActive, 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.activeWorkStartedAtfalls back to the same helper, which brings back the floating working timer and the Thinking row.deriveThreadFeedPresentationtakes an explicitrunlessWorkActiveflag (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 nullactiveRunId.assertProviderNativeSubagentRootTurns, run after every fixture). For each provider-native subagent it checks that the child iscreationSource: "provider", is forked from the subagent node, has no runs, and has only runless root turns. It also checks that the root turn isrunningbefore the child's first non-prompt item, and that its active/terminal sequence matches the subagent's (including the ClaudeSendMessageresume inclaude_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_taskchildren 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 andidlegives 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 livework-liverow 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 therunlessWorkActivegate.apps/server:vp test run src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts: 87 passed with the new invariant.tsc --noEmiton client-runtime, web, mobile, server: no errors.vp linton touched files: no new findings.vp run knip:check: clean.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code