Skip to content

fix(server,web,mobile): agent-launched chats keep their launcher as parent - #179

Merged
jayleaton merged 1 commit into
mainfrom
fix/agents-board-child-parenting
Oct 31, 2026
Merged

jayleaton merged 1 commit into
mainfrom
fix/agents-board-child-parenting

Conversation

@jayleaton

Copy link
Copy Markdown
Owner

Problem

Chats an agent launches sometimes land top-level on the Agents board instead of inside the launching chat's card, on every machine. An audit of real data on the Mac, desktop-pc and dev-box found these causes:

  • Cross-machine delegate_task without profileId (rows D1, V1, V2). Over a stored peer link, the other machine sees only the link session, never the calling thread, so the chat was created with no parent. t3_thread_launch sent over a peer link had the same gap.
  • Chats launched by a delegate_task helper (a subagent). Their parent is the helper, which is never a card, so the board showed them top-level.
  • Captain → Captain launches later detached (rows M1, V3). These were explicit detaches, which older Captain instructions asked for. The launched chat kept a task owned by its launcher while its parent link was null, so the two disagreed.
  • Invented parentThreadId (S1–S5). Already rejected since fix(mcp-gateway): chat creation rejects a parentThreadId that isn't a chat #175, which merged after those rows. One related gap remained: a chat naming itself as the parent of a chat on another machine was refused, because the parent was looked up on the target environment.

Explicit parentThreadId: null (S7–S9) stays as a supported opt-out. The tool text does not suggest it, so it is unchanged.

Change

 caller on Mac ──t3_thread_launch / delegate_task──▶ PeerForwarding ──link──▶ box
-                                                      (no caller)              parent: none
+                                                      delegatedFrom = caller   parent: caller @ Mac
  • PeerForwarding stamps the calling thread as delegatedFrom on every forwarded t3_thread_launch. The receiving t3_thread_launch handler nests a link-session launch under that thread (parentThreadId + parentEnvironmentId).
  • selectAgentSidebarThreads (packages/client-runtime, shared by the web, desktop and mobile boards) lists a chat whose parent is a subagent under that subagent's nearest non-subagent owner.
  • The parent link is canonical, and a task follows it.
    • ThreadTaskService reacts to thread.metadata-updated: a reparented child's task moves to the new parent, and a detached child's task is removed (the old owner's delta reads it as a departure).
    • Startup recovery fixes tasks that drifted earlier, such as M1.
    • ThreadTaskStore gains remove, and its cursor also counts departures, so cursors never repeat.
  • mcp-gateway t3_create_thread: a parentThreadId equal to the calling chat resolves to the caller's environment.

Scope and approval

Maintainer-requested bug fix (JJ via Captain). One problem: agent-launched chats losing their parent link.

Verification

Each new assertion failed on the base commit (fix reverted, test kept) and passes with the fix:

Test Before After
client-runtime agents.test.ts › nests a chat a subagent launched in the card of the subagent's owner ✗ doug-chat, deeper-chat were top-level cards ✓ 44/44 (+ threadRelationships 14/14)
server RemoteDelegation.test.ts › a task delegated to the box… (parent assertion) ✗ parentThreadId: undefined ✓ 27/27
server PeerForwarding.test.ts › launches in a linked environment… (parent assertion) ✗ no parent fields ✓ 42/42 with handlers.test
server ThreadTaskService.test.ts › a task follows its worker's parent… ✗ expected 'captain' to equal 'stranger' ✓ 26/26 (threadTask dir)
mcp-gateway tools.test.ts › parents chats a T3 chat creates on any environment ✗ self-parent on remote refused ✓ 114/114
  • Format and lint are clean on the changed files.
  • Typecheck passes for client-runtime, mcp-gateway and apps/server.
  • Format, lint and the apps/server typecheck ran locally because the Davit box ran out of memory. The tests ran in Davit.
  • Not done: a screenshot of the board in a real client. The board change is pure grouping logic, covered by the shared selector test above.

Merge Danger

Door: two-way for the parenting changes. A task removed on detach is not restored by a revert; the worker can be reassigned with t3_task_assign.

Blast Radius: consumers. Affected:

  • linked-environment launches and delegates (new parent fields on the remote chat);
  • Agents board grouping on web, desktop and mobile;
  • t3_task_read for owners whose children were moved or detached.

Model and harness: Claude Opus 5.5 in Claude Code (T3 agent Doug).

🤖 Generated with Claude Code

…arent

Chats an agent launched sometimes showed top-level on the Agents board:
a remote delegate_task or peer-link t3_thread_launch reached the other
machine with no calling thread, a chat a delegate_task helper launched
pointed at a parent that is never a card, and a detached child kept its
task with the launcher.

- A peer-forwarded t3_thread_launch always names its caller in
  delegatedFrom, and the receiving side nests the chat under it.
- The board lists a chat launched by a subagent under the subagent's
  owner, on web, desktop and mobile.
- A task follows its worker's parent: it moves with a reparent and goes
  with a detach, and restart reconciles tasks that drifted earlier.
- Naming the calling chat as parent of a chat on another machine
  resolves to the caller's environment instead of failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB +29 B (+0.6%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB +29 B (+2.3%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.6 KiB +41 B (+0.2%) 24.4 KiB ✅
Codex Live turn messages 1 2 +1 (+100.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.3 KiB 1.3 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 2 2 0 (0.0%) 8 ✅

Baseline: 8d1b3cf · PR result: 769a28c · 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: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

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

@jayleaton
jayleaton marked this pull request as ready for review October 31, 2026 07:48
@jayleaton
jayleaton merged commit 6502ba6 into main Oct 31, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 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