Skip to content

feat(mobile): stream swarm subagent task cards from the host record stream - #3327

Open
YodonTan wants to merge 6 commits into
GCWing:mainfrom
YodonTan:feat/mobile-subagent-cards
Open

YodonTan wants to merge 6 commits into
GCWing:mainfrom
YodonTan:feat/mobile-subagent-cards

Conversation

@YodonTan

@YodonTan YodonTan commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

What

In 极致 (Ultimate) harness mode the agent delegates to swarm subagents, but a remote mobile client saw only two plain tool rows — "I spawned an agent" and "it answered" — while every intermediate step (94 thinking items / 172 tool calls in one measured worker) slept in hidden child sessions the phone could not even name. This PR makes the subagent process visible end-to-end:

  • Host (2 commits, reviewable in isolation — bfe626e63 + 513453276): the async AgentSpawn path now stamps subagent_session_id / subagent_dialog_turn_id / subagent_model_id onto the parent turn's persisted tool item before the started result returns (the tool-completion record resync is the discovery trigger; SubagentSessionLinked itself never resyncs records). SessionManager persists through the same materialized-rounds read the relay uses, so the link survives. Zero protocol change — the field already existed on ToolItemData and the record-stream serializer; it was simply never written on the async path. Old hosts without the field keep working (clients degrade to plain tool rows).
  • Client: a new SubagentStreamCoordinator discovers child ids from parent records, subscribes each child session's record stream (initial page backfills full history, then live records push), and groups steps into the owning task card by subscription identity — never by is_subagent markers, because live verification on a real Ultimate session proved child items carry none. Owner detection now accepts AgentSpawn alongside Task.
  • Three wiring bugs fixed after device verification: branch expansion survives row rebuilds (keyed @ObservedV2 disclosure state, not a card-local boolean); the card status tracks the worker's lifecycle derived from the child stream (the host completes a spawn record when the worker starts); child streams are capped at 8 with steal-oldest + lazy re-attach, and child transcripts persist through the phone's transcript cache so a cold start restores the steps.
  • Contract honesty: the previously shipped subagent-process-visibility tests asserted a host contract that never existed (child records carrying subagentSessionId inside the parent stream). They are rewritten to pin the real, live-verified contract, and the preview fixture now replays real shapes (async spawn with the started result table, unmarked child items, real tool names).

Verification

  • Client: 395 node tests / 394 pass / 1 pre-existing unrelated failure; new subagent-branch-lifecycle 12/12, subagent-process-visibility 31/31; harmony:architecture and mobile:ui:check pass; clean assembleHap compile + install.
  • Device (emulator, mock transport shaped from a live Ultimate session): card expanded with real-shape child steps mid-progress, derived status flipping running → completed/failed while spawn records are already completed, 10 workers under the 8-stream cap with all cards reachable, cold-start restore of card steps from the transcript cache with child streams muted.
  • Host: coordinator test asserting the persisted parent item + its serialized subagentSessionId, and a relay contract test asserting the published item/... record; remote_connect_contracts 75/75.

Not verified

  • Live host end-to-end (desktop build with the host change + a real paired phone session) — the mock replays the shipped contract shape, so the remaining risk is only the stamping site's behavior on a real coordinator (single call-site, mirrored from the sync path).
  • Continuation re-attach (a settled child named again by a later launch) is test-verified only — the fixture has no continuation script.
  • Wide/fold postures not exercised; live progress push remains poll/record-driven as before.

Tant added 6 commits October 9, 2026 13:03
An asynchronous AgentSpawn returns a started result while the child Session runs on, so the parent Turn's completion writer never saw the link that synchronous delegation stamps in memory. Remote record-stream consumers read persisted Turns only, which left the spawning tool item without any child Session fact.

Link the child Session on the parent Turn's persisted tool item before the spawn start returns, materializing the running generation exactly like a relay read so the stamped item keeps the round and item identity the later completion merge preserves. The existing tool-completion record resync then publishes the link mid-Turn, with no protocol or client change.
…elay item records

The record stream serializes the persisted tool item as it is, so a stamped parent AgentSpawn item must publish subagentSessionId, subagentDialogTurnId and subagentModelId under their camelCase wire names.
An Ultimate session delegates work to child agents, and the host keeps each
worker in its own hidden session: the parent stream only carries the launch
item, a child's steps exist on the child's own stream and carry no marker of
their own. The client already knew how to draw a delegation card's branch; the
records it drew came from a contract the host never published.

The launching item now names its child session (`subagentSessionId`, the field
the host stamps on the item data, camelCase as the record stream serializes
it). A small coordinator follows the parent's records, opens that child session
through the same `subscribeSession` call the session on screen uses, reduces its
records with the same durable reducer, and attaches them as the branch of the
item that launched it. Grouping comes from the subscription a record arrived on
and never from a marker on the record, so a worker's steps cannot be mistaken
for the parent's own rows.

The coordinator publishes only through the controller's existing path, so the
child's records reach the screen through the reducer, timeline store,
projection, grouping policy and card that a parent record already uses. A
launch whose item carries no child session id opens nothing: an older host
keeps the plain tool rows it has always sent.

A card is keyed by what changed in its message, and a launch item settles as
soon as its worker starts — a background worker's launch settles while its
stream says nothing new. The version this coordinator publishes therefore
counts the launch's own record as well as its branch, or exactly that card
would keep drawing a settled worker as running.
The visibility suite pinned a contract no host produces: child records carrying
the child session id inside the parent stream, a nested shape the record stream
has never sent. It is rewritten around what a live Ultimate session publishes.

Pinned now: the launching tool item (`AgentSpawn`/`Task`) is the only place a
child session id appears, a child stream's own records are unmarked and are
grouped by the subscription they arrived on, a launch without that field opens
no stream and stays a plain tool row, a settled launch repaints its card even
when its worker says nothing new, and a host that still inlines a marked child
in the parent stream is not drawn twice.

The fixture drives the production chain — stream reader, controller, durable
reducer, timeline store, keyed rows, projection, grouping policy — so the
assertions cover the path the app runs rather than a preview-only seam. The
three suites that load `ChatSessionController` from source now resolve the
coordinator the controller constructs.
- keep branch expansion across row rebuilds via a keyed observed
  disclosure state instead of the card's local boolean
- derive the card status from the child stream (worker lifecycle),
  not the launch record the host completes at spawn time; fall back
  to the launch status when the subscription failed before data
- cap concurrent child streams at 8 with steal-oldest/waiters and
  lazy re-attach on expand; persist child branch transcripts through
  the phone transcript cache so a cold start restores the steps, and
  re-attach children named again by a later launch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant