fix(mobile): fold subagent lifecycle rows into one batch per spawn#10211

merged2
t3code/mobile-work-row-expand-only-with-extra
t3code/mobile-subagent-spawn-batches
openedSep 5, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago

Stacked on #10210.

Problem

Each subagent's progress and completion rendered as its own timeline row on mobile (three agents → three "Running …" rows plus their completions), while web folds a turn's spawns into one "Kicked off N subagents" row via collapseDerivedWorkLogEntries in session-logic.ts.

Fix

Mirror web's batching in the mobile work log:

  • Same spawn-group keys (wf:<coordinator> for workflows, direct:<turn> for direct spawns), decided at the first row seen per taskId so later rows under synthetic turns cannot splinter the batch.
  • Agent lifecycle rows pass the agent-internal filter even when bypassed or owned by another agent, as on web; that is how Codex children (all bypassed) and Claude workflow members reach the batch row.
  • The batch row keeps the anchor's identity so it renders where the run launched.
  • Background tasks (agentKind !== "agent") keep one row per task, as before.
  • Web reads live agent state from its Agents panel; mobile has no such surface, so the row carries each member's latest lifecycle state, derives its own lifecycle from them (live while any member is, then the worst terminal state), and lists the members when opened. A Codex child's idle turn end counts as finished, and a finished coordinator settles members that never reported their own end. That lifecycle feeds the group summary and shimmer from #10173 unchanged.

Verification

  • vp test run apps/mobile/src/lib/threadActivity.test.ts — 98 passed, including new tests that fold two direct spawns into one row and track it through running → one done → one failed, fold bypassed workflow members into the coordinator's batch and settle them with it, and finish a Codex child on idle; plus updated expectations for the resumed-agent and Antigravity batch cases.
  • tsc --noEmit for apps/mobile, targeted lint and format.
  • Simulator, same seeded turn (three agents: two running, one finished) on main and this branch.

Before: three separate subagent rows. After: one "Kicked off 3 subagents · 1 working" row.

Implemented by Claude Fable 5 in the Claude Code harness.

Note

Fold subagent lifecycle rows into one batch row per spawn in mobile threadActivity

  • Replaces one-row-per-agent-task rendering with one aggregated row per workflow or direct-spawn batch in the mobile work log; shell, monitor, and plan tasks remain ordinary per-task rows.
  • Agent task rows that were previously filtered as internal now reach feed derivation so they can be grouped by workflow, turn, or task ID into spawn batches.
  • Adds helpers for batch lifecycle precedence (in-progress > failed > stopped > completed), member merging by task ID, label generation with live/failed/stopped member counts, and expanded-body formatting listing each member's title, status, and detail.
  • Workflow coordinators are excluded from displayed member counts and details; a terminal coordinator settles unfinished workflow members to its own terminal state.
  • Updates tests in threadActivity.test.ts to assert batch-row grouping, anchor identity, member counts, terminal states, and idle-as-completed behavior.
  • Behavioral Change: isAgentInternalActivity now retains agent task rows with task IDs (including bypassed rows) for batch folding instead of filtering them; consumers of collapseDerivedWorkLogEntries will see fewer rows and new structured spawn metadata on WorkLogEntry.

Macroscope summarized a83379d.

Added 1 commit

  • fix(mobile): fold subagent lifecycle rows into one batch per spawneb8fb60
Synced from GitHub

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

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

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire—13.6 KiB—15.1 KiB✅
CodexThread snapshot wire—7.0 KiB—7.3 KiB✅
CodexLive turn WebSocket wire—6.6 KiB—7.8 KiB✅
CodexLive turn WebSocket decoded—57.1 KiB—66.4 KiB✅
CodexLive turn messages—10—21✅
ClaudeTotal thread wire—13.5 KiB—15.1 KiB✅
ClaudeThread snapshot wire—7.0 KiB—7.3 KiB✅
ClaudeLive turn WebSocket wire—6.5 KiB—7.8 KiB✅
ClaudeLive turn WebSocket decoded—57.8 KiB—66.4 KiB✅
ClaudeLive turn messages—9—21✅

Baseline: unavailable · PR result: a83379d · 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: 113.8 KiB
  • Claude decoded thread snapshot: 114.5 KiB

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

apps/mobile/src/lib/threadActivity.tsline 688Outdated · eb8fb60Open thread
……@@ -589,13 +613,97 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo
685+ agents: NonNullable<WorkLogEntry["agentSpawn"]>["agents"],
686+): WorkLogToolLifecycleStatus {
687+ const statuses = agents.map((agent) => agent.status);
688+ if (statuses.some((status) => status === undefined || status === "inProgress")) {
MmacroscopeappBoton line 688Sep 5, 2026

🟡 Medium lib/threadActivity.ts:688

Direct subagent batches that finish with Codex's status: "idle" remain labeled inProgress instead of completing. extractWorkLogToolLifecycleStatus returns undefined for this task.updated terminal update, and agentSpawnLifecycleStatus treats every undefined member status as running at line 688; map this terminal idle status to completed before aggregation so finished batches stop showing as working.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/mobile/src/lib/threadActivity.ts around line 688:
2
3Direct subagent batches that finish with Codex's `status: "idle"` remain labeled `inProgress` instead of completing. `extractWorkLogToolLifecycleStatus` returns `undefined` for this `task.updated` terminal update, and `agentSpawnLifecycleStatus` treats every undefined member status as running at line 688; map this terminal `idle` status to `completed` before aggregation so finished batches stop showing as working.

Confirmed. Fixed in a83379df0: a task row whose payload status is idle now maps to completed for the batch (Codex's turn-completed is resumable, not terminal, but for the batch row it is a finished member). Covered by the new "treats a Codex child's idle turn end as a finished batch member" test.

apps/mobile/src/lib/threadActivity.tsline 725Outdated · eb8fb60Open thread
……@@ -604,13 +712,32 @@ function collapseDerivedWorkLogEntries(
722+ collapsed.push(entry);
723+ continue;
724+ }
725+ const groupKey = spawnGroupByTaskId.get(entry.taskId) ?? agentSpawnGroupKey(entry);
MmacroscopeappBoton line 725Sep 5, 2026

🟡 Medium lib/threadActivity.ts:725

Workflow batches on mobile omit synthesized :wf: child agents, so the agentSpawn row contains only the coordinator and cannot show child progress or details. deriveWorkLogEntries filters nonterminal timelineBypass tasks before this loop, but Claude emits every workflow-member task.progress with timelineBypass: true; preserve those member rows (or otherwise pass them through) so mergeAgentSpawnEntries can add them.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/mobile/src/lib/threadActivity.ts around line 725:
2
3Workflow batches on mobile omit synthesized `:wf:` child agents, so the `agentSpawn` row contains only the coordinator and cannot show child progress or details. `deriveWorkLogEntries` filters nonterminal `timelineBypass` tasks before this loop, but Claude emits every workflow-member `task.progress` with `timelineBypass: true`; preserve those member rows (or otherwise pass them through) so `mergeAgentSpawnEntries` can add them.

Confirmed. isAgentInternalActivity dropped bypassed non-terminal task rows before batching, so a workflow batch only ever held its coordinator. Fixed in a83379df0: agent lifecycle rows (agentKind: "agent") now pass the internal filter the way web's session-logic does, so bypassed members fold into the batch. Covered by the new "folds bypassed Claude workflow members" test.

apps/mobile/src/lib/threadActivity.tsline 647Outdated · eb8fb60Open thread
……@@ -589,13 +613,97 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo
644+ ...anchorWithoutDetail,
645+ // The row's own lifecycle is the batch's: live while any member is, then
646+ // the worst terminal state, so the group summary and shimmer follow it.
647+ toolLifecycleStatus: agentSpawnLifecycleStatus(agents),
MmacroscopeappBoton line 647Sep 5, 2026

🟡 Medium lib/threadActivity.ts:647

When a workflow coordinator completes, the batch remains inProgress if a member such as wf-1:wf:0 never emits its own terminal event, so mobile permanently shows “1 working” instead of a completed batch. agentSpawnLifecycleStatus(agents) retains that member's stale inProgress status; settle unfinished members to the coordinator's terminal state before calculating the batch status, matching the runtime workflow fold.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/mobile/src/lib/threadActivity.ts around line 647:
2
3When a workflow coordinator completes, the batch remains `inProgress` if a member such as `wf-1:wf:0` never emits its own terminal event, so mobile permanently shows “1 working” instead of a completed batch. `agentSpawnLifecycleStatus(agents)` retains that member's stale `inProgress` status; settle unfinished members to the coordinator's terminal state before calculating the batch status, matching the runtime workflow fold.

Confirmed. Fixed in a83379df0: when the coordinator is in a terminal state, agentSpawnRow settles any member still inProgress/unknown to the coordinator's state before aggregating. The new workflow test has one member that never reports its own end and asserts the batch reads "Ran 2 subagents · completed".

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR changes the existing mobile work-log behavior by replacing individual agent lifecycle rows with stateful batches across direct spawns and workflows. The implementation spans filtering, aggregation, lifecycle derivation, and expanded rendering, making the change broader than a straightforward isolated fix.

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

Added 1 commit

  • fix(mobile): fold bypassed members and idle children into their batcha83379d