Skip to content

feat(threads): production child threads in Agents panel plus threads MCP toolkit (#8) - #10

Merged
lukemaj merged 9 commits into
mainfrom
feat/8-child-threads
Sep 25, 2026
Merged

lukemaj merged 9 commits into
mainfrom
feat/8-child-threads

Conversation

@lukemaj

@lukemaj lukemaj commented Sep 25, 2026 •

Copy link
Copy Markdown

Threads started by another thread are real T3 threads that stay out of the sidebar, appear in the parent thread's Agents panel with status, open on click, and can be messaged there and via the threads MCP toolkit (create, message, read, list). This is the production version of the spike from #3 (draft PR #4), and the fork capability Model Router send builds on.

How it works: spawn_thread creates a thread whose id carries the parent link (sub..), starts its first turn, and bridges its lifecycle into the parent's task.* activities (agentKind agent, taskType t3_thread), which the existing Agents panel fold renders as Direct-spawn rows. Child ids are filtered from both sidebars; Agents panel rows for child threads link to the thread, where the normal composer messages it. message_thread refuses a still-starting child with a retryable error.

Parent link decision: I kept the sub.. id convention instead of a real optional parentThreadId on thread.create. A real field would need coordinated changes to ThreadCreateCommand, ThreadCreatedPayload, OrchestrationThread, OrchestrationThreadShell, the decider, the projector, the ProjectionThread persistence schema plus SQLite layer plus a new migration, and the shell snapshot mapping, with backfill — too invasive for this thin fork. The id convention needs no contract, projector or migration change, survives restarts, and lets every client classify a thread from its shell alone. Known limit: the reportBack flag and bridge dedupe maps are process-local, so after a restart an already-idle child may re-report one turn; threads themselves persist and stay resumable and messageable (proven live across a restart below).

Per-provider decisions: spawn_thread accepts any provider instance. Effort maps to each driver's advertised option — codex/grok reasoningEffort, opencode variant, claudeAgent/cursor/antigravity effort (adapters ignore unknown option ids, so effort on Antigravity is a harmless no-op). Mid-turn messages steer the running turn at the orchestration layer uniformly for every provider; idle, failed and stopped children start a fresh turn. Mobile is untouched (explicit non-goal); desktop wraps web so it follows. No child-to-parent backlink UI: the parent link travels in the child's id and the bridge rows.

Upstream edits (new code otherwise lives in new files; line numbers against main c98dafd):

  • apps/server/src/mcp/McpHttpServer.ts lines 34-35 (import Threads toolkit), 613-616 (ThreadsToolkitRegistrationLive), 640 (merge into server layer): register the toolkit on the MCP server.
  • apps/web/src/components/Sidebar.tsx line 214 (import), 2536-2537 (exclude child ids from the visible list, which also feeds search, jump labels, drag and counts): hide children, no extra section.
  • apps/web/src/components/LegacySidebar.tsx line 26 (import), 1335-1338, 3405-3409, 3449-3450 (same exclusion in the project-group list, the main visible list and the jump-key list): the still-supported legacy sidebar hides children too.
  • apps/web/src/components/AgentsPanel.tsx line 28 (import), 565-567 (wrap Direct-spawn rows in AgentThreadLink): child rows open the child thread; native subagent rows stay plain.
  • scripts/build-desktop-artifact.ts lines 2080-2085, 2101: the bundle self-check probe runs under plain Node (drops an inherited ELECTRON_RUN_AS_NODE), which fixes the "skips the primary native probe for cross-architecture Windows payloads" test.
  • apps/server/src/entrypoint.test.ts lines 51-54: resolve the temp fixture dir, so the symlinked-entrypoint test holds under macOS /var -> /private/var. Assertion unchanged.
  • GLOSSARY.md (fork-owned, from docs: name the fork Chromeria and record the product family #11): table reformatted so vp fmt --check passes; no content change.

Test-suite fix (commit 7a94621): the threads toolkit handlers acquired engine.subscribeDomainEvents while their layer built. That layer builds with the HTTP routes, and the route-layer engine in apps/server/src/server.test.ts does not implement subscribeDomainEvents, so every server.test.ts case failed at startup with UnimplementedError: ... Unimplemented method "subscribeDomainEvents" (193 tests). The bridge now consumes the hot engine.streamDomainEvents, as the upstream reactors (CheckpointReactor, ProviderRuntimeIngestion, AgentAwarenessRelay) do. No test or assertion changed.

Proof (local; fork CI cannot run on Blacksmith runners), on a7de0cd rebased onto main c98dafd:

  • vp run -r typecheck exit 0; vp lint exit 0 (only pre-existing warnings in untouched files); vp fmt --check exit 0.
  • Tests, run per package on this branch and on main in the same environment and compared with comm: no test fails on this branch that passes on main. The branch fails a strict subset of main's failures. apps/server has 50 failures on both, identical names: all ThreadSettlementReactor "retains protected worktrees (...)" cases plus a few UsageService, CodexDriver npm-prefix, project-skill discovery and sandbox-symlink cases. They are environment-dependent on this Mac and unrelated to this change. scripts: 312 passed, 0 failed. apps/desktop: 1353 passed, 0 failed. Every other package passes.
  • Focused tests: apps/server/src/mcp/toolkits/threads/childThreads.test.ts, subagentThreadId.test.ts, apps/web/src/components/subagentThreads.test.ts.

Live verification on an isolated instance (T3 home /tmp/t3-child-verify, server port 14768, user instance on 3773 and ~/.t3/userdata untouched): parent thread on codex/gpt-6-astra drove the real MCP toolkit. Codex child sub..4bc655f4f138 ran to idle and replied CODEX_CHILD_OK, reported back into the parent as a message; opencode child sub..9323f8a5abef (opencode-go/gpt-6-luna) ran to idle and replied OPENCODE_CHILD_OK; a first opencode child failed with 'Insufficient account funds' and its Failed status plus error surfaced in the parent's Agents rows. The starting guard fired live with the exact error 'Thread sub..8a075f2ffeee is still starting. Retry in a few seconds.' on an immediate message_thread, and the retry after 60s was accepted. list_child_threads returned all three children with correct statuses; read_thread polling tracked running to idle. A direct thread.turn.start dispatch on the idle codex child (the composer path for an opened child) returned COMPOSER_OK. Shell snapshots contained the sub.* ids (the sidebar filter input); restart of the isolated server mid-run kept all threads addressable.

UI verification after the fix (isolated dev instance, T3 home /tmp/t3-p10-live copied from the run above, server port 14768, web port 6728, headless Chromium; the user's instance on 3773 and ~/.t3/userdata untouched): the sidebar lists only the parent thread, not its four children. The parent's Agents panel lists every child under Direct spawns with status (Idle, Failed with "Insufficient account funds", Idle). Clicking the "codex probe" row navigated to //sub..4bc655f4f138. A message typed in that thread's normal composer ("Reply with exactly: UI_COMPOSER_OK") got the reply UI_COMPOSER_OK within 10 s. A new child spawned from the parent on the fixed build ("post-fix probe", codex) appeared in the Agents panel, reached Idle with POSTFIX_CHILD_OK, and reported back into the parent. That exercises the streamDomainEvents bridge end to end.

Built with OpenCode (Muse Spark); test-suite fix, rebase and UI verification by Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

lukemaj and others added 9 commits September 25, 2026 14:00
…eads (#3)

Adds a threads toolkit (spawn_thread, message_thread, read_thread,
list_child_threads) next to device, preview and pullRequests. A bridge
mirrors each child thread's lifecycle into its parent's activities as the
task.* rows the Agents panel already folds, and can send each finished
turn back to the parent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…elf (#3)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… a starting child (#3)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e Agents panel (#3)

Replaces the Subagents shelf: child threads are filtered out of the
sidebar, and their Agents panel rows link to the child thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… toolkit guards (#8)

Child threads spawned through the threads MCP toolkit are real T3
threads whose sub.<parent>.<suffix> id carries the parent link, hidden
from both sidebars and listed with status in the parent's Agents panel.

Production hardening over spike/3-threads-toolkit: documented the
id-convention tradeoff against a real parentThreadId (contract,
projector, persistence migration too invasive for this fork), extracted
the steer/new-turn delivery rule, pinned per-driver effort options, and
refused messages to still-starting children with a retryable error.

Built with OpenCode (Muse Spark).
verifyPackagedBundleIsSelfContained spread ambient process.env into
the probe, so a shell with ELECTRON_RUN_AS_NODE=1 leaked the flag into
the validation command and broke the no-Electron-probe invariant the
Windows payload test asserts. Scrub the flag like the neighboring fff
probe curates its own env.

Built with OpenCode (Muse Spark).
)

- desktop: run the bundle self-check probe without ELECTRON_RUN_AS_NODE
  so validation does not depend on the orchestrator's ambient runtime flag.
- server: resolve the entrypoint fixture dir, matching the fully-resolved
  module URLs Node reports (macOS symlinks TMPDIR through /private/var).

Built with OpenCode (Muse Spark).
…kit (#8)

The toolkit handlers layer builds with the HTTP routes. Acquiring
subscribeDomainEvents at build time failed every route-layer test whose
engine does not implement it (193 server.test.ts cases). Use
streamDomainEvents, as the upstream reactors do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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