You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Feature]: Provider-agnostic orchestration ("ultracode for every model") — a fork-owned MCP toolkit that lets any adapter fan out into real T3 threads #43
I searched existing issues and did not find a duplicate.
I am describing a concrete problem or use case, not just a vague idea.
Area
apps/server
Problem or use case
What "ultracode" is, for a reader who has never used it
"Ultracode" is shorthand for a cluster of behaviours people expect from a high-effort coding agent, none of which are a single feature:
Parallel fan-out. The agent decomposes a task and runs several workers at once (explore three call sites, write tests while another writes the implementation) instead of doing everything serially in one context.
Structured output. Workers return machine-readable results (a verdict, a file list, a JSON summary) rather than prose the parent has to re-read and re-parse.
Adversarial verification. A second agent is asked to break the first one's work — find the bug, find the missing test — rather than to agree with it.
Judge panels / best-of-N. The same task is attempted N ways and a separate adjudicator picks a winner or merges the outputs.
Loop-until-dry. The agent keeps iterating (fix → run tests → fix) until a stopping condition is met, rather than stopping after one pass.
What T3 Code actually has today
In this repo, ultracode is only a Claude effort level, not any of the above. It is a dropdown value that normalises to --effort xhigh plus a settings.ultracode: true flag handed to the Claude Agent SDK:
apps/server/src/provider/Layers/ClaudeProvider.ts:75 (and :106, :142) — { value: "ultracode", label: "Ultracode" }, listed only for Claude models
apps/server/src/provider/Layers/ClaudeProvider.ts:406 — if (effort === "ultracode") { return "xhigh"; }
So every one of the five ultracode behaviours, when it happens at all, happens inside the Claude CLI process, where T3 cannot inspect, interrupt, checkpoint, or steer it. That is the direct cause of "ultracode does not always work well in T3 Code": T3 renders provider-internal fan-out as flat, opaque rows.
Subagent activity is a display heuristic, not a model: tool names containing agent/task/subagent get bucketed into the canonical item type collab_agent_tool_call, titled "Subagent task" (ClaudeAdapter.ts:596, :865). The same heuristic exists in OpenCodeAdapter.ts and CodexAdapter.ts and is absent from CursorAdapter.ts and GrokAdapter.ts.
task.started / task.progress / task.completed exist in the canonical union (packages/contracts/src/providerRuntime.ts:177) and are ingested (apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts), but only ClaudeAdapter.ts ever emits them — verified: grep -rl 'task\.started' apps/server/src packages/contracts/src returns only ClaudeAdapter.ts, ProviderRuntimeIngestion.ts, and the contracts file. They are read-only telemetry about work T3 does not own.
Why this cannot be fixed inside the adapters
ProviderAdapterCapabilities has exactly one field, and all five adapters set it to the same value — the declarative capability surface differentiates nothing today:
ClaudeAdapter.ts:3934, CodexAdapter.ts:1705, CursorAdapter.ts:1167, GrokAdapter.ts:1449, OpenCodeAdapter.ts:1703 — all sessionModelSwitch: "in-session".
There is no spawn/subagent primitive anywhere in ProviderAdapterShape, and there is no parent/child thread concept in T3's own domain: grep -rn parentThreadId apps/server/src packages/contracts/src returns nothing (the identifier exists only in the vendored Codex app-server generated schema). OrchestrationThread (packages/contracts/src/orchestration.ts:352) has no parent field.
Building fan-out inside adapters would mean five implementations against five unrelated protocols (Claude Agent SDK, Codex app-server JSON-RPC, ACP ×2, OpenCode HTTP), and two of them (Cursor, Grok) have no subagent concept at all. The repo already documents what happens when capability is under-declared: the web app hardcodes a per-driver allowlist because there is no flag to read —
// apps/web/src/outbox/composerSteering.logic.ts:13*Thecatchisthatthereisnosteeringcapabilityflaganywhereinthe*contracts,sosupport is implicitperadapterandhastobeassertedhere.// :43constSTEERABLE_PROVIDER_DRIVER_KINDS: ReadonlySet<string>=newSet([...]);
Proposed solution
Shape: one orchestrator thread + N child threads, driven entirely above the adapter layer
The orchestrator is an ordinary T3 thread on any provider. Its children are ordinary T3 threads. The only new machinery is a fork-owned MCP toolkit the model calls, plus a fork-owned store that tracks runs. Nothing touches ProviderAdapterShape, and no provider-native subagent feature is used or required.
Why this is provider-agnostic for free
Every one of the five adapters already mounts a T3-hosted MCP server into its provider session, with a per-thread bearer credential issued by T3:
all calling McpProviderSession.readMcpProviderSession(input.threadId). The invocation scope carries the calling thread's id, so a tool handler natively knows who the parent is:
Adding tools here means every provider gets them on the same day, with zero adapter edits.
New files (all fork-owned, zero upstream conflict)
apps/server/src/t3x/orchestrate/
tools.ts # Effect Tool.make definitions — copy apps/server/src/mcp/toolkits/preview/tools.ts
handlers.ts # copy apps/server/src/mcp/toolkits/preview/handlers.ts
RunStore.ts # JSON persistence in stateDir, modelled on t3x-auto-resume.json
Runner.ts # dispatches thread.create + thread.turn.start, watches for completion
index.ts # exports OrchestrateToolkitRegistrationLive + OrchestrateLayerLive
apps/server/src/t3x/mcp/index.ts # single aggregator for all future fork MCP toolkits
apps/server/src/mcp/toolkits/preview/ is a complete, shipping template (tools.ts, handlers.ts, plus tests for both) — it is currently the only toolkit (ls apps/server/src/mcp/toolkits → preview).
The three tools (v1)
Tool
Behaviour
subagent_spawn
Takes { tasks: [{ label, prompt, providerInstanceId?, modelId?, runtimeMode? }] }, max 4. Creates one child thread per task, dispatches thread.turn.start with only the task prompt (no parent history), returns { runId, children: [{ childThreadId, label }] }immediately. Never blocks.
subagent_status
{ runId } → per-child { label, childThreadId, phase } where phase comes from projectThreadAwareness. Cheap to poll.
subagent_result
{ runId } or { childThreadId } → final assistant message text for each completed child, plus its phase.
Async-return + poll is deliberate: an MCP handler that blocks for minutes holds the parent's tool call open against adapter and CLI tool timeouts. Polling burns orchestrator turns but cannot deadlock.
Dispatch: reuse the proven fork precedent verbatim
AutoResumeReactor is already a server-side autonomous dispatcher that synthesises turns indistinguishable from a keystroke:
and it already writes its own audit trail with thread.activity.append (Reactor.ts:75; command declared at packages/contracts/src/orchestration.ts:865).
The orchestrator does the same: engine.dispatch({ type: "thread.create", ... }) then engine.dispatch({ type: "thread.turn.start", ... }) with commandId prefixed t3x-orchestrate:, and appends a breadcrumb to the parent thread for every spawn, phase change, and result read. That trail is provider-agnostic and shows up on Cursor/Grok/OpenCode identically — which is exactly why the design must not build on task.* (Claude-only, ClaudeAdapter.ts is the sole emitter).
Completion detection: already solved, do not reinvent
This reads only shell-projection fields — never provider internals — and is already consumed server-side by the fork's Web Push seam (apps/server/src/t3x/webPush/attention.ts). It returns waiting_for_approval / completed / null, which is precisely the "is this child done, stuck on an approval, or still working" distinction the orchestrator needs.
Do not gate on session.status alone. The existing code documents settled/teardown races, and fork issue #38 records the hazard that synthetic turns deadlock status-gated logic.
Result collection
OrchestrationLatestTurn (packages/contracts/src/orchestration.ts:335) carries assistantMessageId: Schema.NullOr(MessageId) at :341, and ProjectionSnapshotQuery.getThreadDetailById (apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts:168) returns the full thread including messages. subagent_result is a projection read, not a provider call.
Registration seam (the only upstream edit)
The MCP server builds its toolkit layer in apps/server/src/mcp/McpHttpServer.ts:206-225:
Add one merged layer sourced from apps/server/src/t3x/mcp/index.ts — the same aggregator discipline apps/server/src/t3x/index.ts:71/:100 already enforces for T3xLayerLive / T3xRoutesLive. Every future fork toolkit then costs zero additional upstream churn.
Deliberately avoid the capability gate in v1.McpCapability is a closed union with one member (apps/server/src/mcp/McpInvocationContext.ts:10 — export type McpCapability = "preview";) and it is issued from a hardcoded set (apps/server/src/mcp/McpSessionRegistry.ts:131 — capabilities: new Set(["preview"])). Extending both would add two more ledger rows for no v1 benefit. Gate the toolkit on a fork-local setting inside handlers.ts instead, and revisit if v2 needs per-thread scoping.
What degrades, per adapter, and how
Because the design uses no provider-native subagent feature, the capability does not degrade — the presentation and reliability do:
Concern
Claude
Codex
OpenCode
Cursor
Grok
Can call the tools (MCP mounted)
yes
yes
yes
yes
yes
Spawn tool call renders as "Subagent task"
yes (ClaudeAdapter.ts:596)
yes
yes
no — generic tool row
no — generic tool row
Native task.* telemetry
yes
no
no
no
no
Structured judge verdicts
native --json-schema
native --output-schema
prompt-instructed
prompt-instructed
prompt-instructed
Steering the orchestrator mid-run
yes
no (excluded from STEERABLE_PROVIDER_DRIVER_KINDS)
yes
yes
yes
Mitigations: emit the fan-out trail via thread.activity.append so it is legible everywhere (not task.*); accept prose results in v1 and defer structured verdicts; document that Codex orchestrators cannot be steered mid-run.
Fan-out cap and why it is low
Session startup is serialized. ensureSessionForThread (ProviderCommandReactor.ts:684) runs inside buildSendTurnRequestForThread, which runs inside a single-item-at-a-timeDrainableWorker (ProviderCommandReactor.ts:1323). Spawning N children means N sequential CLI process spawns before any of them runs. The turn itself is forked (ProviderCommandReactor.ts:1117-1119 — .pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped)), so once started they do run concurrently. Cap v1 at 4 and make the cap a fork-local constant.
Separately, ProviderSessionReaper.ts:17 sets DEFAULT_INACTIVITY_THRESHOLD_MS = 30 * 60 * 1000 — a parked child session is reaped after 30 minutes, invisibly to a polling orchestrator. subagent_status must report a reaped child as a distinct phase, not silently as "still working".
Restart durability
RunStore.ts persists { runId, parentThreadId, children: [{ childThreadId, label, phase }] } to stateDir, mirroring t3x-auto-resume.json. On boot, reconcile every non-terminal run against getThreadDetailById. Without this a server restart orphans every child thread with no trace.
Why this matters
Today "high-effort agent" in T3 Code means one Claude-only dropdown value that changes a flag on one provider's CLI, and whatever fan-out happens is invisible and unsteerable because it lives inside that CLI's process. This inverts that: each unit of parallel work becomes a first-class T3 thread, which means it inherits everything T3 already built — the activity timeline, approvals, checkpoints, per-turn diffs, interrupt, Web Push, auto-resume, mobile — with no per-feature work.
It is genuinely model-agnostic rather than nominally so: the orchestrator can be a Grok thread spawning Codex children, because the only interface it touches is OrchestrationEngineService.dispatch. That is a capability no single provider gives you, and it is the one thing a wrapper app is uniquely positioned to build.
It also removes a class of "why did nothing happen" confusion: a stalled child shows up as a thread with a phase and an approval card, not as a silent gap inside another process.
Smallest useful scope
v1 = async spawn + poll + collect, on shared cwd, no UI.
Ships:
apps/server/src/t3x/orchestrate/ with subagent_spawn, subagent_status, subagent_result
one aggregator seam line in apps/server/src/mcp/McpHttpServer.ts via apps/server/src/t3x/mcp/index.ts
fan-out cap 4, fork-local constant
RunStore JSON persistence + boot reconciliation
thread.activity.append breadcrumbs on the parent for spawn / phase-change / result
completion via projectThreadAwareness, results via getThreadDetailById + latestTurn.assistantMessageId
Explicitly out of scope for v1 (each is a follow-up issue):
Git worktrees per child. This is the big one. Worktree bootstrap is not in the engine — it is implemented inline in the WebSocket transport (apps/server/src/ws.ts:750 dispatchBootstrapTurnStart, :891thread.create, :908gitWorkflow.createWorktree, :940runSetupProgram()). A server-side dispatcher calling engine.dispatch directly gets none of it. ws.ts measured churn is 41 commits in the 60 days before merge-base 64bf01619 — the highest of any candidate file. So v1 children share the parent's cwd, which makes it safe for read-heavy fan-out (parallel investigation, review, test authoring in disjoint directories) and unsafe for parallel edits to the same files. State that limit in the tool description so the model does not attempt it.
Judge panels / best-of-N adjudication. Needs structured verdicts; see alternatives.
UI grouping. With no parent link in OrchestrationThreadShell, 4 children land flat in the shell snapshot and each completion fires the needs-attention / Web Push coordinator.
Extending ProviderAdapterCapabilities.
Generic structured output in TextGenerationService.
Alternatives considered
1. Wait for upstream orchestrator-v2. PR pingdotgg#3638 already merged delegate_task / task_status / task_cancel / create_threads / t3_thread_* as orchestrator MCP tools — but into the stack branch t3code/codex-turn-mapping, not main. Its gate, pingdotgg#2829, is still open against main. Verified: merge commit dac514e6b is not an ancestor of upstream/main, origin/main, or fork HEAD. This is the strongest alternative and the honest reason to keep v1 tiny — the whole toolkit is three tools in fork-owned files and is cheap to delete when v2 lands. Naming should deliberately not collide with delegate_task/task_status so both can coexist during a transition.
2. Cherry-pick the v2 toolkit onto today's main.git show dac514e6b:apps/server/src/mcp/toolkits/orchestrator/tools.ts is real, reviewed code. But it targets the v2 orchestrator's data model, so porting it means porting or stubbing that model — a much larger change than three fork-local tools, and it creates the "parallel paths" hazard the fork has already been bitten by (a fork path that duplicates an upstream capability silently bypasses new upstream guards).
3. A declarative t3x reactor instead of model-callable tools. Model AutoResumeReactor directly: a server-side supervisor that runs a user-configured plan (stages, providers, prompts) without the model deciding anything. Better for deterministic verification pipelines and judge panels; worse for the "ultracode" experience the user asked for, where the model authors its own fan-out. They share ~80% of the machinery (RunStore, Runner, awareness polling) so (a) can be added on top of (b) later. Picking the MCP toolkit first is the choice that matches the request.
4. createSdkMcpServer from the Claude Agent SDK. Exported and typed but unused anywhere in the repo. Strictly worse here: Claude-only, requires a ClaudeAdapter.ts edit (churn 12), and defeats the entire point of the issue.
5. Extend TextGenerationService with a generic generateStructured(schema, prompt) for judge verdicts. The interface is closed at four git-flavoured operations (apps/server/src/textGeneration/TextGeneration.ts:77-82), so this means five implementations, and only Claude and Codex get a hard schema constraint — Cursor/Grok/OpenCode scrape prompt-instructed JSON with extractJsonObject. Cheaper: run the judge as its own child thread and parse its final assistant message with the same lenient extractor those three adapters already use. Loses per-call model/effort control.
6. Do nothing and document that ultracode is Claude-only. Legitimate. The counter is that ultracode's value is mostly the orchestration pattern, not the effort flag, and that pattern is provider-independent.
Risks or tradeoffs
Seam cost (measured against merge-base 64bf01619, 60-day window)
docs/t3x/SEAMS.md:5 records 34 upstream-owned files, +1616 / -187 lines, and :21 carries an explicit tripwire: "Before adding row 35, re-isolate something instead." This feature adds one row.
File
Edit
Lines
Churn
Risk
apps/server/src/mcp/McpHttpServer.ts
one merged layer sourced from t3x/mcp/index.ts
~3
6
~18
Measured with git log --oneline --since="@$((MBTS-60*86400))" 64bf01619 -- <path>. For contrast, the paths this design deliberately avoids: apps/server/src/ws.ts = 41, apps/server/src/server.ts = 29, packages/contracts/src/orchestration.ts = 12, apps/server/src/provider/Layers/ClaudeAdapter.ts = 12, apps/web/src/components/ChatView.tsx = 63 (already the fork's worst row at risk 11466, SEAMS.md:41). Fork-owned apps/server/src/t3x/ has churn 0.
Rejected-but-tempting rows and their cost: McpSessionRegistry.ts (churn 7) + McpInvocationContext.ts (churn 3) for a capability gate — two rows for a v1 that has no per-thread scoping requirement. ProviderAdapter.ts has churn 0 and is cheap in isolation, but extending it forces edits to all five adapters (ClaudeAdapter.ts churn 12 among them).
The ledger must be updated in the same commit — header totals plus the new row — per the self-reference rule at SEAMS.md:22-28, and the t3x/mcp/index.ts aggregator should be documented there as the reason no further MCP rows will follow.
Parallel-paths hazard
This is the fork's known failure mode: a fork path that duplicates an upstream capability silently bypasses new upstream guards. Upstream is actively building the same thing (pingdotgg#3138, pingdotgg#4649, PRs pingdotgg#4663 / pingdotgg#5219 / pingdotgg#3638). Concretely: use distinct tool names from delegate_task/task_status, keep the entire implementation deletable in one rm -rf apps/server/src/t3x/orchestrate/, and re-check the SEAMS table every sync.
Machine and cost risks
Fan-out thrashes a 16GB Mac. Each child is a full provider CLI process, and startup is serialized through the DrainableWorker. Cap 4, and expect the first child to start well before the fourth.
No budget ceiling exists. Nothing in T3 bounds a run's token spend. A model that spawns 4 children, polls, and re-spawns can burn a lot unattended. v1 should hard-cap total spawns per parent thread.
The 30-minute idle reaper (ProviderSessionReaper.ts:17) silently kills parked children.
UNVERIFIED — do not present these as settled
Do Cursor and Grok's ACP clients actually expose T3's MCP tools to the model? The mcpServers wiring is verified present (CursorAdapter.ts:544, GrokAdapter.ts:582), but nobody has confirmed the tool reaches the model's tool list on those providers. Experiment: start a thread on each of the five providers and ask the model to call mcp__t3-code__preview_status; a tool result on all five proves the channel end-to-end before any orchestration code is written. Do this first — it is the load-bearing assumption of the whole design.
Does a child thread created via engine.dispatch({type:"thread.create"}) without the ws.ts bootstrap path actually start a working session? All existing evidence is that AutoResumeReactor dispatches thread.turn.start to threads that already exist. Experiment: dispatch thread.create then thread.turn.start from a scratch Effect script and confirm turn.completed.
Whether the reaper's 30-minute teardown produces a distinguishable phase from projectThreadAwareness, or looks identical to "completed". Experiment: let a thread idle past 30 minutes and diff the projected phase.
What settings.ultracode: true actually does inside the Claude binary. This repo only sets the flag (ClaudeAdapter.ts:3521); the semantics are undocumented here. The claim "ultracode means parallel fan-out" is inferred from observed behaviour, not from this codebase. Experiment:strings the bundled Claude platform binary for ultracode and read the surrounding tool-registry code.
Whether 4 concurrent children is survivable on this machine. Nobody has measured it.
Examples or references
Upstream issues this overlaps (this is NOT a clean-slate feature)
[Feature]: Add Orchestration/Delegation pingdotgg/t3code#3138 — [Feature]: Add Orchestration/Delegation (OPEN). Closest match: "implement dispatching cross provider agents and orchestration", smallest scope "a model can signal an another provider models to start implementation or start a review."How this differs:[Feature]: Add Orchestration/Delegation pingdotgg/t3code#3138 states the goal; this issue specifies the fork-local implementation (MCP toolkit above the adapter layer, projectThreadAwareness for completion, measured seam cost) and is scoped to land on today's main rather than on orchestrator-v2.
Searched all 1,615 upstream pingdotgg/t3code issues (open + closed — count confirmed against gh api search/issuestotal_count) by dumping the full title corpus locally and grepping ~80 term variants, plus body-level gh search issues per concept (orchestration, delegation, subagent, parallel agents, swarm, judge, voting, best of n, consensus, agent team, teammate, ultracode, fan-out, pipeline, workflow), plus a gh search prs sweep. Also searched all 21 radroid/t3code fork issues. Upstream Discussions are disabled (hasDiscussionsEnabled: false), so issues and PRs are the complete surface.
Result: this is not a duplicate, but it is a strong overlap and the body says so explicitly.
Most decisive finding: PR pingdotgg#3638 is MERGED — but into the stack branch t3code/codex-turn-mapping, gated behind still-open pingdotgg#2829. Verified git merge-base --is-ancestor dac514e6b upstream/main → NOT an ancestor of upstream/main, origin/main, or fork HEAD, and no scheduled_tasks/orchestrator toolkit files are tracked in this checkout (ls apps/server/src/mcp/toolkits → preview only). So the capability exists upstream on a branch but is not reachable from main today, which is the justification for filing this on the fork rather than upstream.
Zero hits in either repo for judge panels, best-of-N, adjudication, voting, consensus, or swarm — that slice is genuinely unclaimed, and is called out as a deferred follow-up rather than folded into v1.
No fork issue duplicates this. The only adjacent fork issue is #38 (Loop Watch), which is about supervising a single long-running thread and explicitly rules cron out of scope; it is referenced for its session.status hazard note, not as an overlap.
Before submitting
Area
apps/server
Problem or use case
What "ultracode" is, for a reader who has never used it
"Ultracode" is shorthand for a cluster of behaviours people expect from a high-effort coding agent, none of which are a single feature:
What T3 Code actually has today
In this repo,
ultracodeis only a Claude effort level, not any of the above. It is a dropdown value that normalises to--effort xhighplus asettings.ultracode: trueflag handed to the Claude Agent SDK:apps/server/src/provider/Layers/ClaudeProvider.ts:75(and:106,:142) —{ value: "ultracode", label: "Ultracode" }, listed only for Claude modelsapps/server/src/provider/Layers/ClaudeProvider.ts:406—if (effort === "ultracode") { return "xhigh"; }apps/server/src/provider/Layers/ClaudeAdapter.ts:3510/:3521—const ultracode = isClaudeUltracodeEffort(effort)→...(ultracode ? { ultracode: true } : {})So every one of the five ultracode behaviours, when it happens at all, happens inside the Claude CLI process, where T3 cannot inspect, interrupt, checkpoint, or steer it. That is the direct cause of "ultracode does not always work well in T3 Code": T3 renders provider-internal fan-out as flat, opaque rows.
agent/task/subagentget bucketed into the canonical item typecollab_agent_tool_call, titled "Subagent task" (ClaudeAdapter.ts:596,:865). The same heuristic exists inOpenCodeAdapter.tsandCodexAdapter.tsand is absent fromCursorAdapter.tsandGrokAdapter.ts.task.started/task.progress/task.completedexist in the canonical union (packages/contracts/src/providerRuntime.ts:177) and are ingested (apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts), but onlyClaudeAdapter.tsever emits them — verified:grep -rl 'task\.started' apps/server/src packages/contracts/srcreturns onlyClaudeAdapter.ts,ProviderRuntimeIngestion.ts, and the contracts file. They are read-only telemetry about work T3 does not own.Why this cannot be fixed inside the adapters
ProviderAdapterCapabilitieshas exactly one field, and all five adapters set it to the same value — the declarative capability surface differentiates nothing today:ClaudeAdapter.ts:3934,CodexAdapter.ts:1705,CursorAdapter.ts:1167,GrokAdapter.ts:1449,OpenCodeAdapter.ts:1703— allsessionModelSwitch: "in-session".There is no spawn/subagent primitive anywhere in
ProviderAdapterShape, and there is no parent/child thread concept in T3's own domain:grep -rn parentThreadId apps/server/src packages/contracts/srcreturns nothing (the identifier exists only in the vendored Codex app-server generated schema).OrchestrationThread(packages/contracts/src/orchestration.ts:352) has no parent field.Building fan-out inside adapters would mean five implementations against five unrelated protocols (Claude Agent SDK, Codex app-server JSON-RPC, ACP ×2, OpenCode HTTP), and two of them (Cursor, Grok) have no subagent concept at all. The repo already documents what happens when capability is under-declared: the web app hardcodes a per-driver allowlist because there is no flag to read —
Proposed solution
Shape: one orchestrator thread + N child threads, driven entirely above the adapter layer
The orchestrator is an ordinary T3 thread on any provider. Its children are ordinary T3 threads. The only new machinery is a fork-owned MCP toolkit the model calls, plus a fork-owned store that tracks runs. Nothing touches
ProviderAdapterShape, and no provider-native subagent feature is used or required.Why this is provider-agnostic for free
Every one of the five adapters already mounts a T3-hosted MCP server into its provider session, with a per-thread bearer credential issued by T3:
apps/server/src/provider/Layers/ClaudeAdapter.ts:3523apps/server/src/provider/Layers/CursorAdapter.ts:534apps/server/src/provider/Layers/CodexAdapter.ts:1397apps/server/src/provider/Layers/GrokAdapter.ts:572apps/server/src/provider/Layers/OpenCodeAdapter.ts:1217all calling
McpProviderSession.readMcpProviderSession(input.threadId). The invocation scope carries the calling thread's id, so a tool handler natively knows who the parent is:Adding tools here means every provider gets them on the same day, with zero adapter edits.
New files (all fork-owned, zero upstream conflict)
apps/server/src/mcp/toolkits/preview/is a complete, shipping template (tools.ts,handlers.ts, plus tests for both) — it is currently the only toolkit (ls apps/server/src/mcp/toolkits→preview).The three tools (v1)
subagent_spawn{ tasks: [{ label, prompt, providerInstanceId?, modelId?, runtimeMode? }] }, max 4. Creates one child thread per task, dispatchesthread.turn.startwith only the task prompt (no parent history), returns{ runId, children: [{ childThreadId, label }] }immediately. Never blocks.subagent_status{ runId }→ per-child{ label, childThreadId, phase }where phase comes fromprojectThreadAwareness. Cheap to poll.subagent_result{ runId }or{ childThreadId }→ final assistant message text for each completed child, plus its phase.Async-return + poll is deliberate: an MCP handler that blocks for minutes holds the parent's tool call open against adapter and CLI tool timeouts. Polling burns orchestrator turns but cannot deadlock.
Dispatch: reuse the proven fork precedent verbatim
AutoResumeReactoris already a server-side autonomous dispatcher that synthesises turns indistinguishable from a keystroke:and it already writes its own audit trail with
thread.activity.append(Reactor.ts:75; command declared atpackages/contracts/src/orchestration.ts:865).The orchestrator does the same:
engine.dispatch({ type: "thread.create", ... })thenengine.dispatch({ type: "thread.turn.start", ... })withcommandIdprefixedt3x-orchestrate:, and appends a breadcrumb to the parent thread for every spawn, phase change, and result read. That trail is provider-agnostic and shows up on Cursor/Grok/OpenCode identically — which is exactly why the design must not build ontask.*(Claude-only,ClaudeAdapter.tsis the sole emitter).Completion detection: already solved, do not reinvent
This reads only shell-projection fields — never provider internals — and is already consumed server-side by the fork's Web Push seam (
apps/server/src/t3x/webPush/attention.ts). It returnswaiting_for_approval/completed/null, which is precisely the "is this child done, stuck on an approval, or still working" distinction the orchestrator needs.Do not gate on
session.statusalone. The existing code documents settled/teardown races, and fork issue #38 records the hazard that synthetic turns deadlock status-gated logic.Result collection
OrchestrationLatestTurn(packages/contracts/src/orchestration.ts:335) carriesassistantMessageId: Schema.NullOr(MessageId)at:341, andProjectionSnapshotQuery.getThreadDetailById(apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts:168) returns the full thread including messages.subagent_resultis a projection read, not a provider call.Registration seam (the only upstream edit)
The MCP server builds its toolkit layer in
apps/server/src/mcp/McpHttpServer.ts:206-225:Add one merged layer sourced from
apps/server/src/t3x/mcp/index.ts— the same aggregator disciplineapps/server/src/t3x/index.ts:71/:100already enforces forT3xLayerLive/T3xRoutesLive. Every future fork toolkit then costs zero additional upstream churn.Deliberately avoid the capability gate in v1.
McpCapabilityis a closed union with one member (apps/server/src/mcp/McpInvocationContext.ts:10 — export type McpCapability = "preview";) and it is issued from a hardcoded set (apps/server/src/mcp/McpSessionRegistry.ts:131 — capabilities: new Set(["preview"])). Extending both would add two more ledger rows for no v1 benefit. Gate the toolkit on a fork-local setting insidehandlers.tsinstead, and revisit if v2 needs per-thread scoping.What degrades, per adapter, and how
Because the design uses no provider-native subagent feature, the capability does not degrade — the presentation and reliability do:
ClaudeAdapter.ts:596)task.*telemetry--json-schema--output-schemaSTEERABLE_PROVIDER_DRIVER_KINDS)Mitigations: emit the fan-out trail via
thread.activity.appendso it is legible everywhere (nottask.*); accept prose results in v1 and defer structured verdicts; document that Codex orchestrators cannot be steered mid-run.Fan-out cap and why it is low
Session startup is serialized.
ensureSessionForThread(ProviderCommandReactor.ts:684) runs insidebuildSendTurnRequestForThread, which runs inside a single-item-at-a-timeDrainableWorker(ProviderCommandReactor.ts:1323). Spawning N children means N sequential CLI process spawns before any of them runs. The turn itself is forked (ProviderCommandReactor.ts:1117-1119 — .pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped)), so once started they do run concurrently. Cap v1 at 4 and make the cap a fork-local constant.Separately,
ProviderSessionReaper.ts:17setsDEFAULT_INACTIVITY_THRESHOLD_MS = 30 * 60 * 1000— a parked child session is reaped after 30 minutes, invisibly to a polling orchestrator.subagent_statusmust report a reaped child as a distinct phase, not silently as "still working".Restart durability
RunStore.tspersists{ runId, parentThreadId, children: [{ childThreadId, label, phase }] }tostateDir, mirroringt3x-auto-resume.json. On boot, reconcile every non-terminal run againstgetThreadDetailById. Without this a server restart orphans every child thread with no trace.Why this matters
Today "high-effort agent" in T3 Code means one Claude-only dropdown value that changes a flag on one provider's CLI, and whatever fan-out happens is invisible and unsteerable because it lives inside that CLI's process. This inverts that: each unit of parallel work becomes a first-class T3 thread, which means it inherits everything T3 already built — the activity timeline, approvals, checkpoints, per-turn diffs, interrupt, Web Push, auto-resume, mobile — with no per-feature work.
It is genuinely model-agnostic rather than nominally so: the orchestrator can be a Grok thread spawning Codex children, because the only interface it touches is
OrchestrationEngineService.dispatch. That is a capability no single provider gives you, and it is the one thing a wrapper app is uniquely positioned to build.It also removes a class of "why did nothing happen" confusion: a stalled child shows up as a thread with a phase and an approval card, not as a silent gap inside another process.
Smallest useful scope
v1 = async spawn + poll + collect, on shared cwd, no UI.
Ships:
apps/server/src/t3x/orchestrate/withsubagent_spawn,subagent_status,subagent_resultapps/server/src/mcp/McpHttpServer.tsviaapps/server/src/t3x/mcp/index.tsRunStoreJSON persistence + boot reconciliationthread.activity.appendbreadcrumbs on the parent for spawn / phase-change / resultprojectThreadAwareness, results viagetThreadDetailById+latestTurn.assistantMessageIdapps/server/src/mcp/toolkits/preview/{tools,handlers}.test.tsExplicitly out of scope for v1 (each is a follow-up issue):
apps/server/src/ws.ts:750 dispatchBootstrapTurnStart,:891thread.create,:908gitWorkflow.createWorktree,:940runSetupProgram()). A server-side dispatcher callingengine.dispatchdirectly gets none of it.ws.tsmeasured churn is 41 commits in the 60 days before merge-base64bf01619— the highest of any candidate file. So v1 children share the parent's cwd, which makes it safe for read-heavy fan-out (parallel investigation, review, test authoring in disjoint directories) and unsafe for parallel edits to the same files. State that limit in the tool description so the model does not attempt it.OrchestrationThreadShell, 4 children land flat in the shell snapshot and each completion fires the needs-attention / Web Push coordinator.ProviderAdapterCapabilities.TextGenerationService.Alternatives considered
1. Wait for upstream orchestrator-v2. PR pingdotgg#3638 already merged
delegate_task/task_status/task_cancel/create_threads/t3_thread_*as orchestrator MCP tools — but into the stack brancht3code/codex-turn-mapping, notmain. Its gate, pingdotgg#2829, is still open againstmain. Verified: merge commitdac514e6bis not an ancestor ofupstream/main,origin/main, or fork HEAD. This is the strongest alternative and the honest reason to keep v1 tiny — the whole toolkit is three tools in fork-owned files and is cheap to delete when v2 lands. Naming should deliberately not collide withdelegate_task/task_statusso both can coexist during a transition.2. Cherry-pick the v2 toolkit onto today's
main.git show dac514e6b:apps/server/src/mcp/toolkits/orchestrator/tools.tsis real, reviewed code. But it targets the v2 orchestrator's data model, so porting it means porting or stubbing that model — a much larger change than three fork-local tools, and it creates the "parallel paths" hazard the fork has already been bitten by (a fork path that duplicates an upstream capability silently bypasses new upstream guards).3. A declarative t3x reactor instead of model-callable tools. Model
AutoResumeReactordirectly: a server-side supervisor that runs a user-configured plan (stages, providers, prompts) without the model deciding anything. Better for deterministic verification pipelines and judge panels; worse for the "ultracode" experience the user asked for, where the model authors its own fan-out. They share ~80% of the machinery (RunStore,Runner, awareness polling) so (a) can be added on top of (b) later. Picking the MCP toolkit first is the choice that matches the request.4.
createSdkMcpServerfrom the Claude Agent SDK. Exported and typed but unused anywhere in the repo. Strictly worse here: Claude-only, requires aClaudeAdapter.tsedit (churn 12), and defeats the entire point of the issue.5. Extend
TextGenerationServicewith a genericgenerateStructured(schema, prompt)for judge verdicts. The interface is closed at four git-flavoured operations (apps/server/src/textGeneration/TextGeneration.ts:77-82), so this means five implementations, and only Claude and Codex get a hard schema constraint — Cursor/Grok/OpenCode scrape prompt-instructed JSON withextractJsonObject. Cheaper: run the judge as its own child thread and parse its final assistant message with the same lenient extractor those three adapters already use. Loses per-call model/effort control.6. Do nothing and document that ultracode is Claude-only. Legitimate. The counter is that ultracode's value is mostly the orchestration pattern, not the effort flag, and that pattern is provider-independent.
Risks or tradeoffs
Seam cost (measured against merge-base
64bf01619, 60-day window)docs/t3x/SEAMS.md:5records 34 upstream-owned files, +1616 / -187 lines, and:21carries an explicit tripwire: "Before adding row 35, re-isolate something instead." This feature adds one row.apps/server/src/mcp/McpHttpServer.tst3x/mcp/index.tsMeasured with
git log --oneline --since="@$((MBTS-60*86400))" 64bf01619 -- <path>. For contrast, the paths this design deliberately avoids:apps/server/src/ws.ts= 41,apps/server/src/server.ts= 29,packages/contracts/src/orchestration.ts= 12,apps/server/src/provider/Layers/ClaudeAdapter.ts= 12,apps/web/src/components/ChatView.tsx= 63 (already the fork's worst row at risk 11466,SEAMS.md:41). Fork-ownedapps/server/src/t3x/has churn 0.Rejected-but-tempting rows and their cost:
McpSessionRegistry.ts(churn 7) +McpInvocationContext.ts(churn 3) for a capability gate — two rows for a v1 that has no per-thread scoping requirement.ProviderAdapter.tshas churn 0 and is cheap in isolation, but extending it forces edits to all five adapters (ClaudeAdapter.tschurn 12 among them).The ledger must be updated in the same commit — header totals plus the new row — per the self-reference rule at
SEAMS.md:22-28, and thet3x/mcp/index.tsaggregator should be documented there as the reason no further MCP rows will follow.Parallel-paths hazard
This is the fork's known failure mode: a fork path that duplicates an upstream capability silently bypasses new upstream guards. Upstream is actively building the same thing (pingdotgg#3138, pingdotgg#4649, PRs pingdotgg#4663 / pingdotgg#5219 / pingdotgg#3638). Concretely: use distinct tool names from
delegate_task/task_status, keep the entire implementation deletable in onerm -rf apps/server/src/t3x/orchestrate/, and re-check the SEAMS table every sync.Machine and cost risks
DrainableWorker. Cap 4, and expect the first child to start well before the fourth.ProviderSessionReaper.ts:17) silently kills parked children.UNVERIFIED — do not present these as settled
mcpServerswiring is verified present (CursorAdapter.ts:544,GrokAdapter.ts:582), but nobody has confirmed the tool reaches the model's tool list on those providers. Experiment: start a thread on each of the five providers and ask the model to callmcp__t3-code__preview_status; a tool result on all five proves the channel end-to-end before any orchestration code is written. Do this first — it is the load-bearing assumption of the whole design.engine.dispatch({type:"thread.create"})without thews.tsbootstrap path actually start a working session? All existing evidence is thatAutoResumeReactordispatchesthread.turn.startto threads that already exist. Experiment: dispatchthread.createthenthread.turn.startfrom a scratch Effect script and confirmturn.completed.projectThreadAwareness, or looks identical to "completed". Experiment: let a thread idle past 30 minutes and diff the projected phase.settings.ultracode: trueactually does inside the Claude binary. This repo only sets the flag (ClaudeAdapter.ts:3521); the semantics are undocumented here. The claim "ultracode means parallel fan-out" is inferred from observed behaviour, not from this codebase. Experiment:stringsthe bundled Claude platform binary forultracodeand read the surrounding tool-registry code.Examples or references
Upstream issues this overlaps (this is NOT a clean-slate feature)
[Feature]: Add Orchestration/Delegation(OPEN). Closest match: "implement dispatching cross provider agents and orchestration", smallest scope "a model can signal an another provider models to start implementation or start a review." How this differs: [Feature]: Add Orchestration/Delegation pingdotgg/t3code#3138 states the goal; this issue specifies the fork-local implementation (MCP toolkit above the adapter layer,projectThreadAwarenessfor completion, measured seam cost) and is scoped to land on today'smainrather than on orchestrator-v2.[Discussion] Configurable development pipelines(OPEN). Owns the multi-stage pipeline / per-stage provider+prompt half. Differs: that is user-configured stages; this is the model authoring its own fan-out at runtime. Complementary, not competing.[WIP] native subagent & workflow observability, OPEN, no v2 dependency) — the UI this feature should eventually render into rather than duplicate.feat: scheduled tasks (automations), MERGED intot3code/codex-turn-mapping, gated behind still-open feat(orchestrator): introduce new orchestrator pingdotgg/t3code#2829. Shipsdelegate_task/task_status/task_cancel/create_threads/t3_thread_start|list|read|send|wait|interrupt. This is the single most important reference: the capability exists upstream on a branch. This issue is a main-compatible, deliberately-deletable variant, in the same spirit as [WIP] feat: native subagent & workflow observability pingdotgg/t3code#5219.session.status— synthetic turns deadlock it" applies directly to child-completion detection here.File:line evidence (all verified in this checkout at
main)apps/server/src/provider/Services/ProviderAdapter.ts:26-32; identical values atClaudeAdapter.ts:3934,CodexAdapter.ts:1705,CursorAdapter.ts:1167,GrokAdapter.ts:1449,OpenCodeAdapter.ts:1703apps/server/src/provider/builtInDrivers.ts:47ClaudeAdapter.ts:3523,CursorAdapter.ts:534,CodexAdapter.ts:1397,GrokAdapter.ts:572,OpenCodeAdapter.ts:1217apps/server/src/mcp/McpInvocationContext.ts:10-19; issuanceapps/server/src/mcp/McpSessionRegistry.ts:131apps/server/src/mcp/McpHttpServer.ts:206-225; only toolkit today:apps/server/src/mcp/toolkits/preview/(tools.ts,handlers.ts,tools.test.ts,handlers.test.ts)apps/server/src/t3x/index.ts:71(T3xLayerLive),:100(T3xRoutesLive); existing seamsautoResume/,webPush/apps/server/src/t3x/autoResume/Reactor.ts:75(thread.activity.append),:111(commandId)apps/server/src/ws.ts:750,:891,:908,:940,:961apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:684,:1117-1119,:1323apps/server/src/provider/Layers/ProviderSessionReaper.ts:17packages/shared/src/agentAwareness.ts:53,:77packages/contracts/src/orchestration.ts:335,:341;apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts:168packages/contracts/src/orchestration.ts:677,:701,:720thread.activity.appendcommand:packages/contracts/src/orchestration.ts:865ClaudeProvider.ts:75,:406,:425;ClaudeAdapter.ts:3510,:3521apps/server/src/textGeneration/TextGeneration.ts:77-82apps/web/src/outbox/composerSteering.logic.ts:13,:43,:51docs/t3x/SEAMS.md:5,:21,:22-28,:41Duplicate search performed before filing
Searched all 1,615 upstream
pingdotgg/t3codeissues (open + closed — count confirmed againstgh api search/issuestotal_count) by dumping the full title corpus locally and grepping ~80 term variants, plus body-levelgh search issuesper concept (orchestration, delegation, subagent, parallel agents, swarm, judge, voting, best of n, consensus, agent team, teammate, ultracode, fan-out, pipeline, workflow), plus agh search prssweep. Also searched all 21radroid/t3codefork issues. Upstream Discussions are disabled (hasDiscussionsEnabled: false), so issues and PRs are the complete surface.Result: this is not a duplicate, but it is a strong overlap and the body says so explicitly.
Found and cross-referenced above: pingdotgg#3138 (Orchestration/Delegation — closest, owns the goal statement), pingdotgg#4649 (configurable pipelines — owns the staged-workflow half), pingdotgg#5218 / pingdotgg#4456 / pingdotgg#1740 / pingdotgg#538 / pingdotgg#2921 (subagent UI, nested threads, CLI orchestration — all the presentation half this issue defers), and in-flight PRs pingdotgg#4663, pingdotgg#5219.
Most decisive finding: PR pingdotgg#3638 is MERGED — but into the stack branch
t3code/codex-turn-mapping, gated behind still-open pingdotgg#2829. Verifiedgit merge-base --is-ancestor dac514e6b upstream/main→ NOT an ancestor ofupstream/main,origin/main, or fork HEAD, and noscheduled_tasks/orchestratortoolkit files are tracked in this checkout (ls apps/server/src/mcp/toolkits→previewonly). So the capability exists upstream on a branch but is not reachable frommaintoday, which is the justification for filing this on the fork rather than upstream.Zero hits in either repo for judge panels, best-of-N, adjudication, voting, consensus, or swarm — that slice is genuinely unclaimed, and is called out as a deferred follow-up rather than folded into v1.
No fork issue duplicates this. The only adjacent fork issue is #38 (Loop Watch), which is about supervising a single long-running thread and explicitly rules cron out of scope; it is referenced for its
session.statushazard note, not as an overlap.Contribution