From ec336f235f5df4081bbdc533d374695b0b94d687 Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Thu, 22 Oct 2026 01:31:57 +0000
Subject: [PATCH 1/6] fix(orchestration): launch parallel delegated children
async before waiting
Batched delegate_task mode='wait' calls can be serialized by the provider's
MCP client before T3 receives them, so "parallel" waited children ran one
after another. Guidance now says to launch every child with mode='async'
first, keep each taskId, and not report a child as running until its launch
response confirms it.
Fixes #23191
Co-Authored-By: Claude Opus 5.5
---
apps/server/src/mcp/toolkits/orchestrator/tools.ts | 2 +-
packages/contracts/src/orchestratorMcp.ts | 2 +-
packages/provider-core/src/server/orchestrationInstructions.ts | 3 ++-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/apps/server/src/mcp/toolkits/orchestrator/tools.ts b/apps/server/src/mcp/toolkits/orchestrator/tools.ts
index 111870be9bbda..9a399d0d59c78 100644
--- a/apps/server/src/mcp/toolkits/orchestrator/tools.ts
+++ b/apps/server/src/mcp/toolkits/orchestrator/tools.ts
@@ -67,7 +67,7 @@ const OrchestratorCapabilitiesTool = Tool.make("orchestrator_capabilities", {
const DelegateTaskTool = Tool.make("delegate_task", {
description:
- "Needs an agent running inside a T3 thread. Delegate one task to a T3-owned child agent/subagent of THIS thread and run it with only the supplied task prompt, without copying parent conversation history. Choose providers and models from orchestrator_capabilities, which uses the same live catalog as the composer. Prefer native subagent tools for same-provider work only when they support the chosen model and no model options are requested or inherited. Use this for any model missing from the native tool, including same-provider work, for cross-provider work, or for explicitly T3-owned child tasks. For every T3 delegated review round, call delegate_task again with the original brief, prior findings, responses, and unresolved objections in the task prompt. Track each round by its own taskId and use a distinct clientRequestId per round, stable across retries of that round. The childThreadId is backing storage, not the target for starting another delegated review round through t3_thread_send. Set isolation to worktree for a separate checkout based on the parent local commit, with project setup completed before the child starts. Uncommitted edits are not copied; omitted isolation shares the parent checkout. Use delegate_task when model options are requested or the child keeps the parent's provider instance and model with nonempty inheritedOptions from orchestrator_capabilities, even for native-supported models. Native subagents cannot reliably apply T3 model options. Set target.options using advertised IDs: Claude {effort: 'max', fastMode: true}; Codex {reasoningEffort: 'max', serviceTier: 'priority'}. Check providerInstanceId, model, and options in the result. Provider/model and modes inherit unless overridden. Options inherit only for the same instance and model; explicit entries override individual values and [] or {} clears them. A requested runtimeMode must be one the target provider lists in orchestrator_capabilities runtimeModes; an inherited one steps down to the broadest mode it offers. Leave runtimeMode unset unless the user asks for a supervised child: approval-required and auto-accept-edits usually make the child wait for a person at each action that needs approval. Prefer mode='async' for long work; mode='wait' blocks until completion, a blocking question, a pending approval, or timeout. If hasPendingQuestion, pass threadId=childThreadId to t3_pending_request_list, t3_pending_request_read (with requestId) and t3_pending_request_respond, or ask the user; the task's completion still wakes this thread. If pendingApproval is set, read the fresh full request in childThreadId before responding: the record can outlive its provider callback. It grants no approval authority; decisions remain with the child's existing approval holder, and pending request tools answer questions only. The child keeps running and its completion still wakes this thread. timeoutMs on mode=wait is only the parent's wait budget and does not cancel the child. waitTimedOut on that wait call means the timeout fired; keep that taskId and read status on later task_status. An async child's completion wakes this thread through a notification, steered into active turns where supported or queued otherwise, so end the turn instead of polling or spawning watchers; use task_status only when the result is needed mid-turn.",
+ "Needs an agent running inside a T3 thread. Delegate one task to a T3-owned child agent/subagent of THIS thread and run it with only the supplied task prompt, without copying parent conversation history. Choose providers and models from orchestrator_capabilities, which uses the same live catalog as the composer. Prefer native subagent tools for same-provider work only when they support the chosen model and no model options are requested or inherited. Use this for any model missing from the native tool, including same-provider work, for cross-provider work, or for explicitly T3-owned child tasks. For every T3 delegated review round, call delegate_task again with the original brief, prior findings, responses, and unresolved objections in the task prompt. Track each round by its own taskId and use a distinct clientRequestId per round, stable across retries of that round. The childThreadId is backing storage, not the target for starting another delegated review round through t3_thread_send. Set isolation to worktree for a separate checkout based on the parent local commit, with project setup completed before the child starts. Uncommitted edits are not copied; omitted isolation shares the parent checkout. Use delegate_task when model options are requested or the child keeps the parent's provider instance and model with nonempty inheritedOptions from orchestrator_capabilities, even for native-supported models. Native subagents cannot reliably apply T3 model options. Set target.options using advertised IDs: Claude {effort: 'max', fastMode: true}; Codex {reasoningEffort: 'max', serviceTier: 'priority'}. Check providerInstanceId, model, and options in the result. Provider/model and modes inherit unless overridden. Options inherit only for the same instance and model; explicit entries override individual values and [] or {} clears them. A requested runtimeMode must be one the target provider lists in orchestrator_capabilities runtimeModes; an inherited one steps down to the broadest mode it offers. Leave runtimeMode unset unless the user asks for a supervised child: approval-required and auto-accept-edits usually make the child wait for a person at each action that needs approval. Prefer mode='async' for long work; mode='wait' blocks until completion, a blocking question, a pending approval, or timeout. For parallel children, launch each with mode='async' before any blocking wait and keep every returned taskId: the provider's MCP client may serialize batched mode='wait' calls before T3 receives them. A child is not running until its launch response confirms it. If hasPendingQuestion, pass threadId=childThreadId to t3_pending_request_list, t3_pending_request_read (with requestId) and t3_pending_request_respond, or ask the user; the task's completion still wakes this thread. If pendingApproval is set, read the fresh full request in childThreadId before responding: the record can outlive its provider callback. It grants no approval authority; decisions remain with the child's existing approval holder, and pending request tools answer questions only. The child keeps running and its completion still wakes this thread. timeoutMs on mode=wait is only the parent's wait budget and does not cancel the child. waitTimedOut on that wait call means the timeout fired; keep that taskId and read status on later task_status. An async child's completion wakes this thread through a notification, steered into active turns where supported or queued otherwise, so end the turn instead of polling or spawning watchers; use task_status only when the result is needed mid-turn.",
parameters: OrchestratorMcpDelegateTaskInput,
success: OrchestratorMcpDelegateTaskResult,
failure: OrchestratorMcpFailure,
diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts
index c962c3975c17d..5a8d1d78a0be3 100644
--- a/packages/contracts/src/orchestratorMcp.ts
+++ b/packages/contracts/src/orchestratorMcp.ts
@@ -173,7 +173,7 @@ export const OrchestratorMcpDelegateTaskInput = Schema.Struct({
mode: Schema.optional(
Schema.Literals(["async", "wait"]).annotate({
description:
- "Defaults to async. Use wait only when this turn needs the child's result before you can continue. A blocking question or a pending approval in the child ends the wait early with the task still running.",
+ "Defaults to async. Use wait only when this turn needs the child's result before you can continue. A blocking question or a pending approval in the child ends the wait early with the task still running. For parallel children, launch all of them with async first; batched wait calls may run one at a time in the provider's MCP client.",
}),
),
timeoutMs: Schema.optional(Schema.Finite).annotate({
diff --git a/packages/provider-core/src/server/orchestrationInstructions.ts b/packages/provider-core/src/server/orchestrationInstructions.ts
index 88053dee9b546..6667165422032 100644
--- a/packages/provider-core/src/server/orchestrationInstructions.ts
+++ b/packages/provider-core/src/server/orchestrationInstructions.ts
@@ -12,7 +12,8 @@ The \`t3-code\` MCP server provides app-owned orchestration. Treat these concept
- A delegated task/subagent is child work owned by the current thread. Use \`orchestrator_capabilities\` to discover the current provider/model IDs from the same live catalog as the composer, including configured custom models. It lists a short set of models per provider; pass \`search\` to find others. Do not treat a native tool's model list as the full list of available subagent models. Prefer native subagent tools for same-provider work only when they support the chosen model and no model options are requested or inherited. Use \`delegate_task\` with that provider instance and model when native tools cannot, including for same-provider work. Also use \`delegate_task\` for cross-provider or explicitly T3-owned child tasks. For a child task that needs a separate checkout, use \`delegate_task\` with \`isolation:"worktree"\` across any provider or model. It starts from the parent local commit and runs project setup; uncommitted edits are not copied. Retain each returned \`taskId\`, and use \`task_status\` or \`task_cancel\` to manage it. The returned \`childThreadId\` is backing storage for the subagent, not the target for starting another delegated review round.
- \`t3_thread_launch\` and \`create_threads\` create ordinary top-level T3 conversations. Use them only when the user explicitly asks for separate/new/top-level threads or conversations. Never use them merely because the user said "subagent" or requested parallel delegated work.
-- For every T3 delegated review round, call \`delegate_task\` again. Include the original brief, prior findings, responses, and unresolved objections in each new task prompt. Track each round by its own \`taskId\`. Use a distinct \`clientRequestId\` per round, stable across retries of that round. Do not use \`t3_thread_send\` on \`childThreadId\` to continue a delegated review.`,
+- For every T3 delegated review round, call \`delegate_task\` again. Include the original brief, prior findings, responses, and unresolved objections in each new task prompt. Track each round by its own \`taskId\`. Use a distinct \`clientRequestId\` per round, stable across retries of that round. Do not use \`t3_thread_send\` on \`childThreadId\` to continue a delegated review.
+- For parallel children, launch every child with \`delegate_task\` \`mode:"async"\` before any blocking wait, and retain each returned \`taskId\`. Your MCP client may run tool calls one at a time before T3 receives them, so batching \`mode:"wait"\` calls can run the children one after another. Do not describe a child as running until its launch response confirms it.`,
`- \`schedule_task\` creates persistent recurring work in the app scheduler. Pass \`schedule\` as a structured object, never as JSON text: \`{"type":"interval","everyMs":3600000}\` for an interval, or \`{"type":"fixed_time","timeOfDay":"09:00","weekdays":[1,2,3,4,5]}\` for a wall-clock schedule, or \`{"type":"webhook"}\` to run on each request to the returned \`webhookUrl\` (the run sees the request only through \`{{body.path}}\`-style placeholders in the prompt). By default runs return to the current thread, which suits orchestrating: each trigger arrives here and you delegate or dedupe; set \`bindToCurrentThread=false\` only when the user wants a fresh thread for every run. After scheduling a timer, report the returned cadence and next run time; for a webhook, report its \`webhookUrl\`, or say T3 Connect remote access is needed if it is missing.
- When you need a secret from the user (a token, API key, or webhook signing secret), call \`request_secret\` so they enter it privately, then pass the returned \`secretRef\` to the tool that needs it, e.g. \`signature.secretRef\` on a webhook task for a sender that signs requests such as GitHub. A \`secretRef\` works once. Never ask for a secret in chat, never invent one, and never repeat one.
- To mention another thread to the user, link it as \`[title](t3-thread://v1/)\` with its exact \`threadId\`, not URL-encoded. T3 Code opens the thread in the app and shows its current title.`,
From ed62906edde3e5a38e925527965cab6b0a2f6f43 Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Wed, 11 Nov 2026 01:46:25 +0000
Subject: [PATCH 2/6] fix(mcp): fit parallel launch guidance to the shorter
instructions
Main rewrote the orchestration instructions and delegate_task schema
tersely. Restate the async-first parallel launch guidance in that style,
measure the context budget, and add an eval case for batched waits.
Fixes pingdotgg/t3code#23191
AI-assisted; verified with the tests and tools listed above.
---
apps/server/src/provider/agentContextBudget.json | 6 +++---
evals/mcp-tool-use/cases/orchestration.ndjson | 1 +
packages/contracts/src/orchestratorMcp.ts | 2 +-
.../provider-core/src/server/orchestrationInstructions.ts | 2 +-
4 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json
index 4439c50940722..c748c2e82f551 100644
--- a/apps/server/src/provider/agentContextBudget.json
+++ b/apps/server/src/provider/agentContextBudget.json
@@ -1,5 +1,5 @@
{
- "claudeSystemAppendBytes": 5597,
- "codexAdditionalContextBytes": 7327,
- "mcpToolListJsonBytes": 48217
+ "claudeSystemAppendBytes": 5747,
+ "codexAdditionalContextBytes": 7477,
+ "mcpToolListJsonBytes": 48260
}
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index e69f14f7cc547..a860ac56cee30 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -35,3 +35,4 @@
{"id": "worker-counts-per-model", "area": "orchestration", "kind": "bad", "pattern": "named-model rule read as a cap", "experience": "Asked for 20 Sol workers and 20 Luna workers, the agent started one worker per model (#25359).", "rule": "Before delegate_task ... One child per named model. (intended: every named model gets a child; read as a cap of one child per model)", "privacy": "commit-safe", "setup": {"role": "top", "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claudeAgent", "inheritedModel": "claude-sonnet-5-5", "inheritedOptions": [{"id": "effort", "value": "high"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Claude", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "fastMode", "type": "boolean", "default": false}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-haiku-5-5", "label": "Claude Haiku 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "codex", "driverKind": "codex", "displayName": "Codex", "models": [{"id": "gpt-6.1-sol", "label": "GPT-6.1 Sol", "options": [{"id": "reasoningEffort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "serviceTier", "values": ["default", "priority"], "default": "default"}]}]}, {"providerInstanceId": "muse", "driverKind": "muse", "displayName": "Muse Code", "runtimeModes": ["approval-required", "full-access"], "models": [{"id": "muse-wonder-full", "label": "Muse Wonder Full", "options": [{"id": "reasoningEffort", "values": ["minimal", "low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "grok", "driverKind": "grok", "displayName": "Grok", "models": [{"id": "grok-4.7", "label": "Grok 4.7", "options": []}]}]}, "delegate_task": "default"}, "world": {"files": {"tests/flaky/retry.test.ts": "it('retry is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/upload.test.ts": "it('upload is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/session.test.ts": "it('session is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/search.test.ts": "it('search is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/billing.test.ts": "it('billing is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/webhook.test.ts": "it('webhook is flaky', async () => { /* fails 1 in 5 runs */ });\n"}}}, "prompt": "Six tests in tests/flaky/ fail now and then. Start 3 Muse Wonder Full workers and 3 Grok workers, one test file each, to find and fix the flake.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "muse-wonder-full"}}, {"tool": "delegate_task", "args": {"target.model": "grok-4.7"}}], "minCalls": {"delegate_task": 6}}, "notes": "Scaled down from the real 20 Sol + 20 Luna ask. Muse and Grok need delegate_task from every parent, so a native Sol helper cannot pass or fail it."}
{"id": "linked-thread-chip-reads-there", "area": "orchestration", "kind": "guard", "pattern": "thread chip from a linked machine read on this one", "experience": "A thread chip from a linked machine names its environmentId; an agent that reads it without passing that id reads this machine, where the thread does not exist.", "rule": "The thread chip names its environmentId; t3_thread_read takes it.", "privacy": "commit-safe", "setup": {"role": "top", "linkedEnvironments": true, "mocks": {"t3_environment_links": {"environmentId": "environment-laptop", "links": [{"environmentId": "environment-vps", "label": "Hetzner VPS", "status": "reachable", "access": "auto-accept-edits", "expiresAt": "2026-12-01T00:00:00.000Z", "lastError": null}], "notListed": "Not listed: not linked yet. Call t3_environment_link with a hint from the user's words."}, "t3_thread_read": {"thread": {"threadId": "thread-deploy-api", "title": "Deploy the API", "status": "idle"}, "items": [{"position": 1, "createdBy": "user", "text": "Deploy the API to the VPS and check the health endpoint."}, {"position": 2, "createdBy": "assistant", "text": "Deployed. /health returns 200, but /health/db times out after 5s: the API can't reach Postgres on 10.0.0.5:5432."}], "nextPosition": 3, "hasMore": false}}}, "prompt": "Pick up where [Deploy the API](t3-context://v1/thread/thread_deploy-api) left off. What is still failing?", "promptContext": {"records": [{"version": 1, "kind": "thread", "contextId": "thread_deploy-api", "label": "Deploy the API", "environmentId": "environment-vps", "threadId": "thread-deploy-api", "title": "Deploy the API"}]}, "expect": {"calls": [{"tool": "t3_thread_read", "args": {"threadId": "thread-deploy-api", "environmentId": "environment-vps"}}], "notCalls": ["t3_environment_link"]}, "source": {"request": "akj on #17130: attach a linked machine's thread without dragging"}}
{"id": "thread-send-fresh-context", "area": "orchestration", "kind": "guard", "pattern": "t3_thread_send that should start a fresh provider session", "experience": "The user wanted a confused long-running thread to start over in the same thread without its old context; a plain send would keep feeding it the history.", "rule": "`freshContext: true` on t3_thread_send starts a new provider session in the same thread, like /clear.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "The thread working on the CSV importer keeps reverting to the old column mapping from early in its history."}, {"role": "assistant", "text": "That thread has a long history with three different mappings in it, so the model keeps picking up the stale one."}], "mocks": {"t3_thread_send": {"threadId": "11111111-1111-4111-8111-111111111111", "link": "", "messageId": "msg-1", "runId": "run-1", "status": "starting", "delivery": "started"}}}, "prompt": "Send it the final mapping below and have it start over with a clean context in that same thread, so the old history stops leaking in: thread 11111111-1111-4111-8111-111111111111.\n\nFinal mapping: id -> customer_id, amt -> amount_cents, ts -> created_at.", "expect": {"calls": [{"tool": "t3_thread_send", "args": {"threadId": "11111111-1111-4111-8111-111111111111", "freshContext": true}}], "notCalls": ["delegate_task", "t3_thread_launch", "create_threads"]}, "source": {"request": "juliusmarminge (maintainer) request, 2026-11-03: fresh provider sessions in one T3 thread"}, "notes": "Pairs with thread-send-to-named-top-level-thread, where an ordinary send must not set freshContext."}
+{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "Parallel delegates: launch all with mode:\"async\" first; batched waits may run one at a time. A delegate runs only once its launch result says so.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23191"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts
index 39e9b35181695..490feded0e1e1 100644
--- a/packages/contracts/src/orchestratorMcp.ts
+++ b/packages/contracts/src/orchestratorMcp.ts
@@ -180,7 +180,7 @@ export const OrchestratorMcpDelegateTaskInput = Schema.Struct({
mode: Schema.optional(
Schema.Literals(["async", "wait"]).annotate({
description:
- "Default 'async'. 'wait' only if this turn needs the result. A question or approval ends the wait. For parallel children, launch all async first: batched waits may run one at a time.",
+ "Default 'async'. 'wait' only if this turn needs the result. A question or approval ends the wait. Parallel children: launch all async first.",
}),
),
timeoutMs: Schema.optional(Schema.Finite).annotate({
diff --git a/packages/provider-core/src/server/orchestrationInstructions.ts b/packages/provider-core/src/server/orchestrationInstructions.ts
index 20842772fa9e5..dd6fa4d4ccbbe 100644
--- a/packages/provider-core/src/server/orchestrationInstructions.ts
+++ b/packages/provider-core/src/server/orchestrationInstructions.ts
@@ -9,7 +9,7 @@ Ordinary coding tasks need no T3 tools.
- Subagents: use your native tool when the child uses your provider, a model it offers, and no settings beyond yours (Claude \`Agent\`, \`run_in_background:false\`; Codex \`spawn_agent\`, then wait). Use \`delegate_task\` for another provider's model, user-asked settings, a separate checkout (\`isolation:"worktree"\`), or \`delegate_task\` by name. A native type on another provider's model counts too. Background agent: \`delegate_task\`, never a background \`Agent\`. Workflow only when asked.
- Call \`orchestrator_capabilities\` once per thread, before the first \`delegate_task\`. Again only after an error or a user-named provider change. Settings go in \`target.options\`. Provider or model unavailable: say so. Never switch unasked, or fall back to native tools, Workflow, or CLIs. Never make a child's checkout in the shell or in \`task\`.
- Every new delegate review or re-check is a new \`delegate_task\` (brief, findings, objections), never a \`t3_thread_send\`. Fix small review findings yourself; start no delegate for them. When told to proceed without a delegate, or to run fewer, \`task_cancel\` the extras. Start no replacement.
-- Parallel delegates: launch each with \`mode:"async"\` before any wait, since batched waits may run one at a time. Call a delegate running only once its launch result says so.
+- Parallel delegates: launch all with \`mode:"async"\` first; batched waits may run one at a time. A delegate runs only once its launch result says so.
- Follow a running delegate's \`next\`. Do not poll it (\`task_status\`, \`sleep\`, watchers, Monitor, ScheduleWakeup). Stop a delegate run you replace or no longer need with \`task_cancel\`, never \`t3_thread_interrupt\` on a \`childThreadId\`.
- If you are a subagent or child, do the work yourself. Start no subagents unless your task says to.`,
`- \`t3_thread_launch\` and \`create_threads\` make new top-level threads, only when the user asks for a new thread. "Subagent", "delegate", and "parallel" never mean one. Worktree thread: set \`workspaceStrategy\`. Honor a requested base. Otherwise, independent work uses the default branch as \`remote/branch\` with \`startFromOrigin:true\`; with no remote, use the local default. Required unpublished commits and dependent work use their local base. Shell worktrees do not bind it, and no checkout setup in its prompt. No \`workspaceStrategy\`: project root, not your worktree.
From e1b865d9437cd656148b03ef87dfed4e5a7c8e2c Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Wed, 11 Nov 2026 09:42:45 +0000
Subject: [PATCH 3/6] fix(mcp): keep parallel async guidance within the agent
context budget
Move the parallel-launch rule into the delegate_task mode description and
drop the duplicate 'separate checkout or PR per child' clause from the
isolation description, so the system prompts stay at main's size and the
tool list shrinks.
AI-assisted; verified with the tests and tools listed above.
---
apps/server/src/provider/agentContextBudget.json | 6 +++---
evals/mcp-tool-use/cases/orchestration.ndjson | 2 +-
packages/contracts/src/orchestratorMcp.ts | 4 ++--
.../provider-core/src/server/orchestrationInstructions.ts | 1 -
4 files changed, 6 insertions(+), 7 deletions(-)
diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json
index c748c2e82f551..6b65e30a692d7 100644
--- a/apps/server/src/provider/agentContextBudget.json
+++ b/apps/server/src/provider/agentContextBudget.json
@@ -1,5 +1,5 @@
{
- "claudeSystemAppendBytes": 5747,
- "codexAdditionalContextBytes": 7477,
- "mcpToolListJsonBytes": 48260
+ "claudeSystemAppendBytes": 5597,
+ "codexAdditionalContextBytes": 7327,
+ "mcpToolListJsonBytes": 48208
}
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index a860ac56cee30..c48483fa3764c 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -35,4 +35,4 @@
{"id": "worker-counts-per-model", "area": "orchestration", "kind": "bad", "pattern": "named-model rule read as a cap", "experience": "Asked for 20 Sol workers and 20 Luna workers, the agent started one worker per model (#25359).", "rule": "Before delegate_task ... One child per named model. (intended: every named model gets a child; read as a cap of one child per model)", "privacy": "commit-safe", "setup": {"role": "top", "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claudeAgent", "inheritedModel": "claude-sonnet-5-5", "inheritedOptions": [{"id": "effort", "value": "high"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Claude", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "fastMode", "type": "boolean", "default": false}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-haiku-5-5", "label": "Claude Haiku 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "codex", "driverKind": "codex", "displayName": "Codex", "models": [{"id": "gpt-6.1-sol", "label": "GPT-6.1 Sol", "options": [{"id": "reasoningEffort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "serviceTier", "values": ["default", "priority"], "default": "default"}]}]}, {"providerInstanceId": "muse", "driverKind": "muse", "displayName": "Muse Code", "runtimeModes": ["approval-required", "full-access"], "models": [{"id": "muse-wonder-full", "label": "Muse Wonder Full", "options": [{"id": "reasoningEffort", "values": ["minimal", "low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "grok", "driverKind": "grok", "displayName": "Grok", "models": [{"id": "grok-4.7", "label": "Grok 4.7", "options": []}]}]}, "delegate_task": "default"}, "world": {"files": {"tests/flaky/retry.test.ts": "it('retry is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/upload.test.ts": "it('upload is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/session.test.ts": "it('session is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/search.test.ts": "it('search is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/billing.test.ts": "it('billing is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/webhook.test.ts": "it('webhook is flaky', async () => { /* fails 1 in 5 runs */ });\n"}}}, "prompt": "Six tests in tests/flaky/ fail now and then. Start 3 Muse Wonder Full workers and 3 Grok workers, one test file each, to find and fix the flake.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "muse-wonder-full"}}, {"tool": "delegate_task", "args": {"target.model": "grok-4.7"}}], "minCalls": {"delegate_task": 6}}, "notes": "Scaled down from the real 20 Sol + 20 Luna ask. Muse and Grok need delegate_task from every parent, so a native Sol helper cannot pass or fail it."}
{"id": "linked-thread-chip-reads-there", "area": "orchestration", "kind": "guard", "pattern": "thread chip from a linked machine read on this one", "experience": "A thread chip from a linked machine names its environmentId; an agent that reads it without passing that id reads this machine, where the thread does not exist.", "rule": "The thread chip names its environmentId; t3_thread_read takes it.", "privacy": "commit-safe", "setup": {"role": "top", "linkedEnvironments": true, "mocks": {"t3_environment_links": {"environmentId": "environment-laptop", "links": [{"environmentId": "environment-vps", "label": "Hetzner VPS", "status": "reachable", "access": "auto-accept-edits", "expiresAt": "2026-12-01T00:00:00.000Z", "lastError": null}], "notListed": "Not listed: not linked yet. Call t3_environment_link with a hint from the user's words."}, "t3_thread_read": {"thread": {"threadId": "thread-deploy-api", "title": "Deploy the API", "status": "idle"}, "items": [{"position": 1, "createdBy": "user", "text": "Deploy the API to the VPS and check the health endpoint."}, {"position": 2, "createdBy": "assistant", "text": "Deployed. /health returns 200, but /health/db times out after 5s: the API can't reach Postgres on 10.0.0.5:5432."}], "nextPosition": 3, "hasMore": false}}}, "prompt": "Pick up where [Deploy the API](t3-context://v1/thread/thread_deploy-api) left off. What is still failing?", "promptContext": {"records": [{"version": 1, "kind": "thread", "contextId": "thread_deploy-api", "label": "Deploy the API", "environmentId": "environment-vps", "threadId": "thread-deploy-api", "title": "Deploy the API"}]}, "expect": {"calls": [{"tool": "t3_thread_read", "args": {"threadId": "thread-deploy-api", "environmentId": "environment-vps"}}], "notCalls": ["t3_environment_link"]}, "source": {"request": "akj on #17130: attach a linked machine's thread without dragging"}}
{"id": "thread-send-fresh-context", "area": "orchestration", "kind": "guard", "pattern": "t3_thread_send that should start a fresh provider session", "experience": "The user wanted a confused long-running thread to start over in the same thread without its old context; a plain send would keep feeding it the history.", "rule": "`freshContext: true` on t3_thread_send starts a new provider session in the same thread, like /clear.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "The thread working on the CSV importer keeps reverting to the old column mapping from early in its history."}, {"role": "assistant", "text": "That thread has a long history with three different mappings in it, so the model keeps picking up the stale one."}], "mocks": {"t3_thread_send": {"threadId": "11111111-1111-4111-8111-111111111111", "link": "", "messageId": "msg-1", "runId": "run-1", "status": "starting", "delivery": "started"}}}, "prompt": "Send it the final mapping below and have it start over with a clean context in that same thread, so the old history stops leaking in: thread 11111111-1111-4111-8111-111111111111.\n\nFinal mapping: id -> customer_id, amt -> amount_cents, ts -> created_at.", "expect": {"calls": [{"tool": "t3_thread_send", "args": {"threadId": "11111111-1111-4111-8111-111111111111", "freshContext": true}}], "notCalls": ["delegate_task", "t3_thread_launch", "create_threads"]}, "source": {"request": "juliusmarminge (maintainer) request, 2026-11-03: fresh provider sessions in one T3 thread"}, "notes": "Pairs with thread-send-to-named-top-level-thread, where an ordinary send must not set freshContext."}
-{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "Parallel delegates: launch all with mode:\"async\" first; batched waits may run one at a time. A delegate runs only once its launch result says so.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23191"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
+{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "delegate_task mode: Default 'async'. Parallel children: all 'async'. 'wait' only if this turn needs the result.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23191"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts
index 490feded0e1e1..27cf1587f357e 100644
--- a/packages/contracts/src/orchestratorMcp.ts
+++ b/packages/contracts/src/orchestratorMcp.ts
@@ -174,13 +174,13 @@ export const OrchestratorMcpDelegateTaskInput = Schema.Struct({
isolation: Schema.optional(
Schema.Literals(["shared", "worktree"]).annotate({
description:
- "'shared' (default): parent checkout. 'worktree': new prepared checkout from the parent commit, for a separate checkout or PR per child. Uncommitted edits stay out.",
+ "'shared' (default): parent checkout. 'worktree': new prepared checkout from the parent commit. Uncommitted edits stay out.",
}),
),
mode: Schema.optional(
Schema.Literals(["async", "wait"]).annotate({
description:
- "Default 'async'. 'wait' only if this turn needs the result. A question or approval ends the wait. Parallel children: launch all async first.",
+ "Default 'async'. Parallel children: all 'async'. 'wait' only if this turn needs the result. A question or approval ends the wait.",
}),
),
timeoutMs: Schema.optional(Schema.Finite).annotate({
diff --git a/packages/provider-core/src/server/orchestrationInstructions.ts b/packages/provider-core/src/server/orchestrationInstructions.ts
index dd6fa4d4ccbbe..9c9720edf18b7 100644
--- a/packages/provider-core/src/server/orchestrationInstructions.ts
+++ b/packages/provider-core/src/server/orchestrationInstructions.ts
@@ -9,7 +9,6 @@ Ordinary coding tasks need no T3 tools.
- Subagents: use your native tool when the child uses your provider, a model it offers, and no settings beyond yours (Claude \`Agent\`, \`run_in_background:false\`; Codex \`spawn_agent\`, then wait). Use \`delegate_task\` for another provider's model, user-asked settings, a separate checkout (\`isolation:"worktree"\`), or \`delegate_task\` by name. A native type on another provider's model counts too. Background agent: \`delegate_task\`, never a background \`Agent\`. Workflow only when asked.
- Call \`orchestrator_capabilities\` once per thread, before the first \`delegate_task\`. Again only after an error or a user-named provider change. Settings go in \`target.options\`. Provider or model unavailable: say so. Never switch unasked, or fall back to native tools, Workflow, or CLIs. Never make a child's checkout in the shell or in \`task\`.
- Every new delegate review or re-check is a new \`delegate_task\` (brief, findings, objections), never a \`t3_thread_send\`. Fix small review findings yourself; start no delegate for them. When told to proceed without a delegate, or to run fewer, \`task_cancel\` the extras. Start no replacement.
-- Parallel delegates: launch all with \`mode:"async"\` first; batched waits may run one at a time. A delegate runs only once its launch result says so.
- Follow a running delegate's \`next\`. Do not poll it (\`task_status\`, \`sleep\`, watchers, Monitor, ScheduleWakeup). Stop a delegate run you replace or no longer need with \`task_cancel\`, never \`t3_thread_interrupt\` on a \`childThreadId\`.
- If you are a subagent or child, do the work yourself. Start no subagents unless your task says to.`,
`- \`t3_thread_launch\` and \`create_threads\` make new top-level threads, only when the user asks for a new thread. "Subagent", "delegate", and "parallel" never mean one. Worktree thread: set \`workspaceStrategy\`. Honor a requested base. Otherwise, independent work uses the default branch as \`remote/branch\` with \`startFromOrigin:true\`; with no remote, use the local default. Required unpublished commits and dependent work use their local base. Shell worktrees do not bind it, and no checkout setup in its prompt. No \`workspaceStrategy\`: project root, not your worktree.
From 4fd132cd9dd3cfa4e873999d814fe960456a2e95 Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Fri, 13 Nov 2026 02:56:32 +0000
Subject: [PATCH 4/6] test(evals): cite the issue behind the parallel delegate
case
The case named #23191, the GitCafe number. On GitHub that is an
unrelated PR; the report is #23175.
Co-Authored-By: Claude Opus 5.5
---
evals/mcp-tool-use/cases/orchestration.ndjson | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index c2bb2a5b4a2dd..6d0838293baa2 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -42,4 +42,4 @@
{"id": "settle-after-delayed-merge-waits", "area": "orchestration", "kind": "guard", "pattern": "settle before delayed work finishes", "experience": "Asked to merge a PR once its checks pass and then settle this thread, an agent following the #26749 settle bullet can settle in the first turn; a settled thread refuses watch_pull_request and drops PR-watch wakes, so the merge never happens.", "rule": "Asked to settle ... a thread, including this one: call t3_thread_organize. (the request is to settle after the merge, not now)", "privacy": "commit-safe", "notes": "From the CodeRabbit finding on #26765. Settling this thread applies at turn end, and Orchestrator.ts rejects watch wakes on a settled thread. Good: watch #123 and end the turn without settling. gh pr merge --auto plus a settle would still lose the wake that confirms the merge.", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default", "t3_thread_organize": {"settlesWhenTurnEnds": true}}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return ;\n+ return ;\n }\ndiff --git a/apps/web/src/HtmlPreview.test.tsx b/apps/web/src/HtmlPreview.test.tsx\nnew file mode 100644\n--- /dev/null\n+++ b/apps/web/src/HtmlPreview.test.tsx\n+import { expect, it } from 'vitest';\n+import { render, screen } from '@testing-library/react';\n+import { HtmlPreview } from './HtmlPreview';\n+\n+it('gives the preview frame an accessible name', () => {\n+ render(hi
\" />);\n+ expect(screen.getByTitle('HTML preview')).toBeTruthy();\n+});\n", "prs": [{"number": 123, "title": "fix(web): HTML preview frame gets an accessible title", "headRefName": "fix/html-preview-title", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Check", "state": "pending"}, {"name": "Test Web", "state": "pending"}]}], "files": {"apps/web/src/HtmlPreview.tsx": "export function HtmlPreview({ html }: { html: string }) {\n return ;\n}\n", "apps/web/src/HtmlPreview.test.tsx": "import { expect, it } from 'vitest';\nimport { render, screen } from '@testing-library/react';\nimport { HtmlPreview } from './HtmlPreview';\n\nit('gives the preview frame an accessible name', () => {\n render(hi\" />);\n expect(screen.getByTitle('HTML preview')).toBeTruthy();\n});\n"}}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): HTML preview frame gets an accessible title)"}]}, "prompt": "Merge it once its checks pass, then settle this thread.", "expect": {"calls": [{"tool": "watch_pull_request", "args": {"url": "~/pull/123"}}], "notCalls": ["t3_thread_organize", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "pr-wait", "gh pr merge(?![^\\n]*--auto)"]}}
{"id": "settle-after-merge-on-wake", "area": "orchestration", "kind": "bad", "pattern": "settle read as plain English for finish", "experience": "Asked to merge a PR once its checks pass and then settle this thread, the agent merged on the checks-passed wake and ended the turn without calling t3_thread_organize (#26749).", "rule": "Asked to settle ... a thread, including this one: call t3_thread_organize.", "privacy": "commit-safe", "notes": "From #26749's delayed pattern. Wake text is copied from pullRequestWatch.ts. Good: merge #123, then settle this thread on the same turn.", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}, {"role": "user", "text": "Merge it once its checks pass, then settle this thread."}, {"role": "assistant", "text": "T3 Code is watching [#123](https://github.com/acme/app/pull/123). I'll merge it once Check and Test Web pass, then settle this thread.", "toolCalls": [{"tool": "watch_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "watching": true, "wasWatching": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default", "t3_thread_organize": {"settlesWhenTurnEnds": true}, "unwatch_pull_request": "default"}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return ;\n+ return ;\n }\ndiff --git a/apps/web/src/HtmlPreview.test.tsx b/apps/web/src/HtmlPreview.test.tsx\nnew file mode 100644\n--- /dev/null\n+++ b/apps/web/src/HtmlPreview.test.tsx\n+import { expect, it } from 'vitest';\n+import { render, screen } from '@testing-library/react';\n+import { HtmlPreview } from './HtmlPreview';\n+\n+it('gives the preview frame an accessible name', () => {\n+ render(hi\" />);\n+ expect(screen.getByTitle('HTML preview')).toBeTruthy();\n+});\n", "prs": [{"number": 123, "title": "fix(web): HTML preview frame gets an accessible title", "headRefName": "fix/html-preview-title", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Check", "state": "pass"}, {"name": "Test Web", "state": "pass"}], "mergeStateStatus": "CLEAN"}], "files": {"apps/web/src/HtmlPreview.tsx": "export function HtmlPreview({ html }: { html: string }) {\n return ;\n}\n", "apps/web/src/HtmlPreview.test.tsx": "import { expect, it } from 'vitest';\nimport { render, screen } from '@testing-library/react';\nimport { HtmlPreview } from './HtmlPreview';\n\nit('gives the preview frame an accessible name', () => {\n render(hi\" />);\n expect(screen.getByTitle('HTML preview')).toBeTruthy();\n});\n"}}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): HTML preview frame gets an accessible title)"}]}, "prompt": "Update on pull request #123 (https://github.com/acme/app/pull/123), which T3 Code is watching for you:\n- All 4 checks passed on abc1234.\n\nLook into each item and act on it as your task requires. This PR stays linked and watched, so do not link, watch, or list it again. End your turn when you are done, and T3 Code wakes you on the next change. When you hand the work back to the user, call unwatch_pull_request first so the thread returns to their inbox.", "expect": {"calls": [{"tool": "Bash", "args": {"command": "~gh pr merge"}}, {"tool": "t3_thread_organize", "args": {"action": "settle"}}], "notCalls": ["watch_pull_request", "t3_thread_update", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "pr-wait"]}}
{"id": "thread-send-fresh-context", "area": "orchestration", "kind": "guard", "pattern": "t3_thread_send that should start a fresh provider session", "experience": "The user wanted a confused long-running thread to start over in the same thread without its old context; a plain send would keep feeding it the history.", "rule": "`freshContext: true` on t3_thread_send starts a new provider session in the same thread, like /clear.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "The thread working on the CSV importer keeps reverting to the old column mapping from early in its history."}, {"role": "assistant", "text": "That thread has a long history with three different mappings in it, so the model keeps picking up the stale one."}], "mocks": {"t3_thread_send": {"threadId": "11111111-1111-4111-8111-111111111111", "link": "", "messageId": "msg-1", "runId": "run-1", "status": "starting", "delivery": "started"}}}, "prompt": "Send it the final mapping below and have it start over with a clean context in that same thread, so the old history stops leaking in: thread 11111111-1111-4111-8111-111111111111.\n\nFinal mapping: id -> customer_id, amt -> amount_cents, ts -> created_at.", "expect": {"calls": [{"tool": "t3_thread_send", "args": {"threadId": "11111111-1111-4111-8111-111111111111", "freshContext": true}}], "notCalls": ["delegate_task", "t3_thread_launch", "create_threads"]}, "source": {"request": "juliusmarminge (maintainer) request, 2026-11-03: fresh provider sessions in one T3 thread"}, "notes": "Pairs with thread-send-to-named-top-level-thread, where an ordinary send must not set freshContext."}
-{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "delegate_task mode: Default 'async'. Parallel children: all 'async'. 'wait' only if this turn needs the result.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23191"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
+{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "delegate_task mode: Default 'async'. Parallel children: all 'async'. 'wait' only if this turn needs the result.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23175"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
From 16b95f2a22e8fbd7b0cb26302027a0eb1c0c3f68 Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Fri, 13 Nov 2026 03:08:10 +0000
Subject: [PATCH 5/6] fix(mcp): launch every parallel child before following
next
An async delegate_task result tells the parent to end its turn. A
parent starting two children could obey that after the first launch,
leaving the second never started while it reported both running. The
orchestration instructions now say to launch every parallel child
before following next, and to call a child running only once its
taskId returns.
Co-Authored-By: Claude Opus 5.5
---
apps/server/src/provider/agentContextBudget.json | 4 ++--
evals/mcp-tool-use/cases/orchestration.ndjson | 2 +-
.../provider-core/src/server/orchestrationInstructions.ts | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json
index 101d6b8fbca97..03b65e9ee4b6c 100644
--- a/apps/server/src/provider/agentContextBudget.json
+++ b/apps/server/src/provider/agentContextBudget.json
@@ -1,5 +1,5 @@
{
- "claudeSystemAppendBytes": 5597,
- "codexAdditionalContextBytes": 7327,
+ "claudeSystemAppendBytes": 5689,
+ "codexAdditionalContextBytes": 7419,
"mcpToolListJsonBytes": 48090
}
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index 6d0838293baa2..0dc8d671f0602 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -42,4 +42,4 @@
{"id": "settle-after-delayed-merge-waits", "area": "orchestration", "kind": "guard", "pattern": "settle before delayed work finishes", "experience": "Asked to merge a PR once its checks pass and then settle this thread, an agent following the #26749 settle bullet can settle in the first turn; a settled thread refuses watch_pull_request and drops PR-watch wakes, so the merge never happens.", "rule": "Asked to settle ... a thread, including this one: call t3_thread_organize. (the request is to settle after the merge, not now)", "privacy": "commit-safe", "notes": "From the CodeRabbit finding on #26765. Settling this thread applies at turn end, and Orchestrator.ts rejects watch wakes on a settled thread. Good: watch #123 and end the turn without settling. gh pr merge --auto plus a settle would still lose the wake that confirms the merge.", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default", "t3_thread_organize": {"settlesWhenTurnEnds": true}}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return ;\n+ return ;\n }\ndiff --git a/apps/web/src/HtmlPreview.test.tsx b/apps/web/src/HtmlPreview.test.tsx\nnew file mode 100644\n--- /dev/null\n+++ b/apps/web/src/HtmlPreview.test.tsx\n+import { expect, it } from 'vitest';\n+import { render, screen } from '@testing-library/react';\n+import { HtmlPreview } from './HtmlPreview';\n+\n+it('gives the preview frame an accessible name', () => {\n+ render(hi\" />);\n+ expect(screen.getByTitle('HTML preview')).toBeTruthy();\n+});\n", "prs": [{"number": 123, "title": "fix(web): HTML preview frame gets an accessible title", "headRefName": "fix/html-preview-title", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Check", "state": "pending"}, {"name": "Test Web", "state": "pending"}]}], "files": {"apps/web/src/HtmlPreview.tsx": "export function HtmlPreview({ html }: { html: string }) {\n return ;\n}\n", "apps/web/src/HtmlPreview.test.tsx": "import { expect, it } from 'vitest';\nimport { render, screen } from '@testing-library/react';\nimport { HtmlPreview } from './HtmlPreview';\n\nit('gives the preview frame an accessible name', () => {\n render(hi\" />);\n expect(screen.getByTitle('HTML preview')).toBeTruthy();\n});\n"}}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): HTML preview frame gets an accessible title)"}]}, "prompt": "Merge it once its checks pass, then settle this thread.", "expect": {"calls": [{"tool": "watch_pull_request", "args": {"url": "~/pull/123"}}], "notCalls": ["t3_thread_organize", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "pr-wait", "gh pr merge(?![^\\n]*--auto)"]}}
{"id": "settle-after-merge-on-wake", "area": "orchestration", "kind": "bad", "pattern": "settle read as plain English for finish", "experience": "Asked to merge a PR once its checks pass and then settle this thread, the agent merged on the checks-passed wake and ended the turn without calling t3_thread_organize (#26749).", "rule": "Asked to settle ... a thread, including this one: call t3_thread_organize.", "privacy": "commit-safe", "notes": "From #26749's delayed pattern. Wake text is copied from pullRequestWatch.ts. Good: merge #123, then settle this thread on the same turn.", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}, {"role": "user", "text": "Merge it once its checks pass, then settle this thread."}, {"role": "assistant", "text": "T3 Code is watching [#123](https://github.com/acme/app/pull/123). I'll merge it once Check and Test Web pass, then settle this thread.", "toolCalls": [{"tool": "watch_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "watching": true, "wasWatching": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default", "t3_thread_organize": {"settlesWhenTurnEnds": true}, "unwatch_pull_request": "default"}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return ;\n+ return ;\n }\ndiff --git a/apps/web/src/HtmlPreview.test.tsx b/apps/web/src/HtmlPreview.test.tsx\nnew file mode 100644\n--- /dev/null\n+++ b/apps/web/src/HtmlPreview.test.tsx\n+import { expect, it } from 'vitest';\n+import { render, screen } from '@testing-library/react';\n+import { HtmlPreview } from './HtmlPreview';\n+\n+it('gives the preview frame an accessible name', () => {\n+ render(hi\" />);\n+ expect(screen.getByTitle('HTML preview')).toBeTruthy();\n+});\n", "prs": [{"number": 123, "title": "fix(web): HTML preview frame gets an accessible title", "headRefName": "fix/html-preview-title", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Check", "state": "pass"}, {"name": "Test Web", "state": "pass"}], "mergeStateStatus": "CLEAN"}], "files": {"apps/web/src/HtmlPreview.tsx": "export function HtmlPreview({ html }: { html: string }) {\n return ;\n}\n", "apps/web/src/HtmlPreview.test.tsx": "import { expect, it } from 'vitest';\nimport { render, screen } from '@testing-library/react';\nimport { HtmlPreview } from './HtmlPreview';\n\nit('gives the preview frame an accessible name', () => {\n render(hi\" />);\n expect(screen.getByTitle('HTML preview')).toBeTruthy();\n});\n"}}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): HTML preview frame gets an accessible title)"}]}, "prompt": "Update on pull request #123 (https://github.com/acme/app/pull/123), which T3 Code is watching for you:\n- All 4 checks passed on abc1234.\n\nLook into each item and act on it as your task requires. This PR stays linked and watched, so do not link, watch, or list it again. End your turn when you are done, and T3 Code wakes you on the next change. When you hand the work back to the user, call unwatch_pull_request first so the thread returns to their inbox.", "expect": {"calls": [{"tool": "Bash", "args": {"command": "~gh pr merge"}}, {"tool": "t3_thread_organize", "args": {"action": "settle"}}], "notCalls": ["watch_pull_request", "t3_thread_update", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "pr-wait"]}}
{"id": "thread-send-fresh-context", "area": "orchestration", "kind": "guard", "pattern": "t3_thread_send that should start a fresh provider session", "experience": "The user wanted a confused long-running thread to start over in the same thread without its old context; a plain send would keep feeding it the history.", "rule": "`freshContext: true` on t3_thread_send starts a new provider session in the same thread, like /clear.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "The thread working on the CSV importer keeps reverting to the old column mapping from early in its history."}, {"role": "assistant", "text": "That thread has a long history with three different mappings in it, so the model keeps picking up the stale one."}], "mocks": {"t3_thread_send": {"threadId": "11111111-1111-4111-8111-111111111111", "link": "", "messageId": "msg-1", "runId": "run-1", "status": "starting", "delivery": "started"}}}, "prompt": "Send it the final mapping below and have it start over with a clean context in that same thread, so the old history stops leaking in: thread 11111111-1111-4111-8111-111111111111.\n\nFinal mapping: id -> customer_id, amt -> amount_cents, ts -> created_at.", "expect": {"calls": [{"tool": "t3_thread_send", "args": {"threadId": "11111111-1111-4111-8111-111111111111", "freshContext": true}}], "notCalls": ["delegate_task", "t3_thread_launch", "create_threads"]}, "source": {"request": "juliusmarminge (maintainer) request, 2026-11-03: fresh provider sessions in one T3 thread"}, "notes": "Pairs with thread-send-to-named-top-level-thread, where an ordinary send must not set freshContext."}
-{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "delegate_task mode: Default 'async'. Parallel children: all 'async'. 'wait' only if this turn needs the result.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23175"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
+{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "delegate_task mode: Default 'async'. Parallel children: all 'async'. / Launch every parallel child before you follow a running delegate's `next`. Call a child running only once its `taskId` returns.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23175"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
diff --git a/packages/provider-core/src/server/orchestrationInstructions.ts b/packages/provider-core/src/server/orchestrationInstructions.ts
index bb4d33d2c56e2..e7e4431da6c01 100644
--- a/packages/provider-core/src/server/orchestrationInstructions.ts
+++ b/packages/provider-core/src/server/orchestrationInstructions.ts
@@ -9,7 +9,7 @@ Ordinary coding tasks need no T3 tools.
- Subagents: use your native tool when the child uses your provider, a model it offers and no settings beyond yours (Claude \`Agent\`, \`run_in_background:false\`; Codex \`spawn_agent\`, then wait). Use \`delegate_task\` for another provider's model, user-asked settings, a separate checkout (\`isolation:"worktree"\`), or \`delegate_task\` by name. A native type on another provider's model counts too. Background agent: \`delegate_task\`, never a background \`Agent\`. Workflow only when asked.
- Call \`orchestrator_capabilities\` once per thread, before the first \`delegate_task\`. Again only after an error or a user-named provider change. Settings go in \`target.options\`. Provider or model unavailable: say so. Never switch unasked or fall back to native tools, Workflow or CLIs. Never make a child's checkout in the shell or in \`task\`.
- Every new delegate review or re-check is a new \`delegate_task\` (brief, findings, objections), never a \`t3_thread_send\`. Fix small review findings yourself; start no delegate for them. When told to proceed without a delegate, or to run fewer, \`task_cancel\` the extras. Start no replacement.
-- Follow a running delegate's \`next\`. Don't poll it (\`task_status\`, \`sleep\`, watchers, Monitor, ScheduleWakeup). Stop a delegate run you replace or no longer need with \`task_cancel\`, never \`t3_thread_interrupt\` on a \`childThreadId\`.
+- Launch every parallel child before you follow a running delegate's \`next\`. Call a child running only once its \`taskId\` returns. Don't poll it (\`task_status\`, \`sleep\`, watchers, Monitor, ScheduleWakeup). Stop a delegate run you replace or no longer need with \`task_cancel\`, never \`t3_thread_interrupt\` on a \`childThreadId\`.
- As a subagent or child, do the work yourself. Start no subagents unless your task says to.`,
`- \`t3_thread_launch\` and \`create_threads\` make new top-level threads, only when the user asks for a new thread. "Subagent", "delegate", and "parallel" never mean one. Worktree thread: set \`workspaceStrategy\`. Honor a requested base. Otherwise independent work uses the default branch as \`remote/branch\` with \`startFromOrigin:true\`; with no remote, use the local default. Required unpublished commits and dependent work use their local base. Shell worktrees don't bind it, and no checkout setup in its prompt. No \`workspaceStrategy\`: project root, not your worktree.
- Given a thread id, \`t3_thread_read\` it before you answer about it. Asked to settle a thread, this one too: \`t3_thread_organize\`.
From b26a110006cb3662ed4af17c1c2fa36d588cb2fb Mon Sep 17 00:00:00 2001
From: Julius Marminge
Date: Fri, 13 Nov 2026 03:29:54 +0000
Subject: [PATCH 6/6] fix(mcp): fit parallel launch ordering into the mode
description
The ordering rule in the orchestration instructions grew both
instruction budgets by 92 bytes. Move it into delegate_task's mode
description, where the parallel rule already lives: start every
parallel child, then follow next, and wait only for one child. The
tool list stays at main's size and the instructions match main.
Co-Authored-By: Claude Opus 5.5
---
apps/server/src/provider/agentContextBudget.json | 6 +++---
evals/mcp-tool-use/cases/orchestration.ndjson | 2 +-
packages/contracts/src/orchestratorMcp.ts | 2 +-
.../provider-core/src/server/orchestrationInstructions.ts | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json
index 03b65e9ee4b6c..2ce96a5c4180c 100644
--- a/apps/server/src/provider/agentContextBudget.json
+++ b/apps/server/src/provider/agentContextBudget.json
@@ -1,5 +1,5 @@
{
- "claudeSystemAppendBytes": 5689,
- "codexAdditionalContextBytes": 7419,
- "mcpToolListJsonBytes": 48090
+ "claudeSystemAppendBytes": 5597,
+ "codexAdditionalContextBytes": 7327,
+ "mcpToolListJsonBytes": 48099
}
diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson
index 0dc8d671f0602..82696a9e3981d 100644
--- a/evals/mcp-tool-use/cases/orchestration.ndjson
+++ b/evals/mcp-tool-use/cases/orchestration.ndjson
@@ -42,4 +42,4 @@
{"id": "settle-after-delayed-merge-waits", "area": "orchestration", "kind": "guard", "pattern": "settle before delayed work finishes", "experience": "Asked to merge a PR once its checks pass and then settle this thread, an agent following the #26749 settle bullet can settle in the first turn; a settled thread refuses watch_pull_request and drops PR-watch wakes, so the merge never happens.", "rule": "Asked to settle ... a thread, including this one: call t3_thread_organize. (the request is to settle after the merge, not now)", "privacy": "commit-safe", "notes": "From the CodeRabbit finding on #26765. Settling this thread applies at turn end, and Orchestrator.ts rejects watch wakes on a settled thread. Good: watch #123 and end the turn without settling. gh pr merge --auto plus a settle would still lose the wake that confirms the merge.", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default", "t3_thread_organize": {"settlesWhenTurnEnds": true}}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return ;\n+ return ;\n }\ndiff --git a/apps/web/src/HtmlPreview.test.tsx b/apps/web/src/HtmlPreview.test.tsx\nnew file mode 100644\n--- /dev/null\n+++ b/apps/web/src/HtmlPreview.test.tsx\n+import { expect, it } from 'vitest';\n+import { render, screen } from '@testing-library/react';\n+import { HtmlPreview } from './HtmlPreview';\n+\n+it('gives the preview frame an accessible name', () => {\n+ render(hi\" />);\n+ expect(screen.getByTitle('HTML preview')).toBeTruthy();\n+});\n", "prs": [{"number": 123, "title": "fix(web): HTML preview frame gets an accessible title", "headRefName": "fix/html-preview-title", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Check", "state": "pending"}, {"name": "Test Web", "state": "pending"}]}], "files": {"apps/web/src/HtmlPreview.tsx": "export function HtmlPreview({ html }: { html: string }) {\n return ;\n}\n", "apps/web/src/HtmlPreview.test.tsx": "import { expect, it } from 'vitest';\nimport { render, screen } from '@testing-library/react';\nimport { HtmlPreview } from './HtmlPreview';\n\nit('gives the preview frame an accessible name', () => {\n render(hi\" />);\n expect(screen.getByTitle('HTML preview')).toBeTruthy();\n});\n"}}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): HTML preview frame gets an accessible title)"}]}, "prompt": "Merge it once its checks pass, then settle this thread.", "expect": {"calls": [{"tool": "watch_pull_request", "args": {"url": "~/pull/123"}}], "notCalls": ["t3_thread_organize", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "pr-wait", "gh pr merge(?![^\\n]*--auto)"]}}
{"id": "settle-after-merge-on-wake", "area": "orchestration", "kind": "bad", "pattern": "settle read as plain English for finish", "experience": "Asked to merge a PR once its checks pass and then settle this thread, the agent merged on the checks-passed wake and ended the turn without calling t3_thread_organize (#26749).", "rule": "Asked to settle ... a thread, including this one: call t3_thread_organize.", "privacy": "commit-safe", "notes": "From #26749's delayed pattern. Wake text is copied from pullRequestWatch.ts. Good: merge #123, then settle this thread on the same turn.", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Okay this looks good to me. Any risk merging it?"}, {"role": "assistant", "text": "Low risk. [#123](https://github.com/acme/app/pull/123) only adds an accessible title to the HTML preview frame, and the new test covers it. Two checks are still running: Check and Test Web.", "toolCalls": [{"tool": "Bash", "input": {"command": "gh pr view 123 --json title,files --jq '.title, .files[].path'"}, "result": "fix(web): HTML preview frame gets an accessible title\napps/web/src/HtmlPreview.tsx\napps/web/src/HtmlPreview.test.tsx"}, {"tool": "Bash", "input": {"command": "gh pr checks 123"}, "result": "Lint\tpass\t41s\nTypecheck\tpass\t1m2s\nCheck\tpending\t0\nTest Web\tpending\t0"}, {"tool": "link_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "alreadyLinked": false}}]}, {"role": "user", "text": "Merge it once its checks pass, then settle this thread."}, {"role": "assistant", "text": "T3 Code is watching [#123](https://github.com/acme/app/pull/123). I'll merge it once Check and Test Web pass, then settle this thread.", "toolCalls": [{"tool": "watch_pull_request", "input": {"url": "https://github.com/acme/app/pull/123"}, "result": {"host": "github.com", "repository": "acme/app", "number": 123, "url": "https://github.com/acme/app/pull/123", "watching": true, "wasWatching": false}}]}], "mocks": {"watch_pull_request": "default", "link_pull_request": "default", "t3_thread_organize": {"settlesWhenTurnEnds": true}, "unwatch_pull_request": "default"}, "world": {"branch": "fix/html-preview-title", "commits": [{"sha": "abc1234", "subject": "fix(web): HTML preview frame gets an accessible title"}], "diff": "diff --git a/apps/web/src/HtmlPreview.tsx b/apps/web/src/HtmlPreview.tsx\n--- a/apps/web/src/HtmlPreview.tsx\n+++ b/apps/web/src/HtmlPreview.tsx\n@@ -1,3 +1,3 @@\n export function HtmlPreview({ html }: { html: string }) {\n- return ;\n+ return ;\n }\ndiff --git a/apps/web/src/HtmlPreview.test.tsx b/apps/web/src/HtmlPreview.test.tsx\nnew file mode 100644\n--- /dev/null\n+++ b/apps/web/src/HtmlPreview.test.tsx\n+import { expect, it } from 'vitest';\n+import { render, screen } from '@testing-library/react';\n+import { HtmlPreview } from './HtmlPreview';\n+\n+it('gives the preview frame an accessible name', () => {\n+ render(hi\" />);\n+ expect(screen.getByTitle('HTML preview')).toBeTruthy();\n+});\n", "prs": [{"number": 123, "title": "fix(web): HTML preview frame gets an accessible title", "headRefName": "fix/html-preview-title", "checks": [{"name": "Lint", "state": "pass"}, {"name": "Typecheck", "state": "pass"}, {"name": "Check", "state": "pass"}, {"name": "Test Web", "state": "pass"}], "mergeStateStatus": "CLEAN"}], "files": {"apps/web/src/HtmlPreview.tsx": "export function HtmlPreview({ html }: { html: string }) {\n return ;\n}\n", "apps/web/src/HtmlPreview.test.tsx": "import { expect, it } from 'vitest';\nimport { render, screen } from '@testing-library/react';\nimport { HtmlPreview } from './HtmlPreview';\n\nit('gives the preview frame an accessible name', () => {\n render(hi\" />);\n expect(screen.getByTitle('HTML preview')).toBeTruthy();\n});\n"}}, "shellMocks": [{"match": "gh pr merge", "output": "✓ Squashed and merged pull request acme/app#123 (fix(web): HTML preview frame gets an accessible title)"}]}, "prompt": "Update on pull request #123 (https://github.com/acme/app/pull/123), which T3 Code is watching for you:\n- All 4 checks passed on abc1234.\n\nLook into each item and act on it as your task requires. This PR stays linked and watched, so do not link, watch, or list it again. End your turn when you are done, and T3 Code wakes you on the next change. When you hand the work back to the user, call unwatch_pull_request first so the thread returns to their inbox.", "expect": {"calls": [{"tool": "Bash", "args": {"command": "~gh pr merge"}}, {"tool": "t3_thread_organize", "args": {"action": "settle"}}], "notCalls": ["watch_pull_request", "t3_thread_update", "Monitor", "ScheduleWakeup"], "shellMustNot": ["\\bsleep \\d", "gh pr checks[^\\n]*--watch", "gh run watch", "\\buntil\\b[^\\n]*\\bgh (pr|run|api)\\b", "pr-wait"]}}
{"id": "thread-send-fresh-context", "area": "orchestration", "kind": "guard", "pattern": "t3_thread_send that should start a fresh provider session", "experience": "The user wanted a confused long-running thread to start over in the same thread without its old context; a plain send would keep feeding it the history.", "rule": "`freshContext: true` on t3_thread_send starts a new provider session in the same thread, like /clear.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "The thread working on the CSV importer keeps reverting to the old column mapping from early in its history."}, {"role": "assistant", "text": "That thread has a long history with three different mappings in it, so the model keeps picking up the stale one."}], "mocks": {"t3_thread_send": {"threadId": "11111111-1111-4111-8111-111111111111", "link": "", "messageId": "msg-1", "runId": "run-1", "status": "starting", "delivery": "started"}}}, "prompt": "Send it the final mapping below and have it start over with a clean context in that same thread, so the old history stops leaking in: thread 11111111-1111-4111-8111-111111111111.\n\nFinal mapping: id -> customer_id, amt -> amount_cents, ts -> created_at.", "expect": {"calls": [{"tool": "t3_thread_send", "args": {"threadId": "11111111-1111-4111-8111-111111111111", "freshContext": true}}], "notCalls": ["delegate_task", "t3_thread_launch", "create_threads"]}, "source": {"request": "juliusmarminge (maintainer) request, 2026-11-03: fresh provider sessions in one T3 thread"}, "notes": "Pairs with thread-send-to-named-top-level-thread, where an ordinary send must not set freshContext."}
-{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "delegate_task mode: Default 'async'. Parallel children: all 'async'. / Launch every parallel child before you follow a running delegate's `next`. Call a child running only once its `taskId` returns.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23175"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
+{"id": "parallel-delegates-launch-async", "area": "orchestration", "kind": "bad", "pattern": "parallel delegates launched with batched waits", "experience": "Asked for two delegates in parallel, the agent batched two delegate_task mode:'wait' calls. The provider's MCP client ran them one at a time, so the second child started only after the first wait returned, while the agent told the user both were running.", "rule": "delegate_task mode: Default 'async'. Parallel: start all, then follow `next`. 'wait' only for one child this turn needs.", "privacy": "commit-safe", "setup": {"role": "top", "context": "Repository: /home/user/app (acme/app). This is the local machine.", "world": {"repo": "acme/app", "branch": "main", "status": "", "files": {"src/thread-pool.ts": "export const workerCount = 4;\n", "src/task-queue.ts": "export const queueLimit = 10;\n"}}}, "prompt": "Have two GPT-6.1 Sol delegates in Codex review in parallel: one reviews src/thread-pool.ts, the other src/task-queue.ts. I need both reviews before you answer.", "expect": {"calls": [{"tool": "delegate_task", "args": {"task": "~thread-pool"}}, {"tool": "delegate_task", "args": {"task": "~task-queue"}}], "notCalls": ["t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "delegate_task", "args": {"mode": "wait"}}]}, "source": {"issue": "pingdotgg/t3code#23175"}, "notes": "Synthetic Acme scenario; no field transcript copied. The simulated MCP client does not serialize calls, so the case checks the launch mode, not dispatch timing."}
diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts
index ca604da48083f..60cedbf224ed9 100644
--- a/packages/contracts/src/orchestratorMcp.ts
+++ b/packages/contracts/src/orchestratorMcp.ts
@@ -180,7 +180,7 @@ export const OrchestratorMcpDelegateTaskInput = Schema.Struct({
mode: Schema.optional(
Schema.Literals(["async", "wait"]).annotate({
description:
- "Default 'async'. Parallel children: all 'async'. 'wait' only if this turn needs the result. A question or approval ends the wait.",
+ "Default 'async'. Parallel: start all, then follow `next`. 'wait' only for one child this turn needs. A question or approval ends the wait.",
}),
),
timeoutMs: Schema.optional(Schema.Finite).annotate({
diff --git a/packages/provider-core/src/server/orchestrationInstructions.ts b/packages/provider-core/src/server/orchestrationInstructions.ts
index e7e4431da6c01..bb4d33d2c56e2 100644
--- a/packages/provider-core/src/server/orchestrationInstructions.ts
+++ b/packages/provider-core/src/server/orchestrationInstructions.ts
@@ -9,7 +9,7 @@ Ordinary coding tasks need no T3 tools.
- Subagents: use your native tool when the child uses your provider, a model it offers and no settings beyond yours (Claude \`Agent\`, \`run_in_background:false\`; Codex \`spawn_agent\`, then wait). Use \`delegate_task\` for another provider's model, user-asked settings, a separate checkout (\`isolation:"worktree"\`), or \`delegate_task\` by name. A native type on another provider's model counts too. Background agent: \`delegate_task\`, never a background \`Agent\`. Workflow only when asked.
- Call \`orchestrator_capabilities\` once per thread, before the first \`delegate_task\`. Again only after an error or a user-named provider change. Settings go in \`target.options\`. Provider or model unavailable: say so. Never switch unasked or fall back to native tools, Workflow or CLIs. Never make a child's checkout in the shell or in \`task\`.
- Every new delegate review or re-check is a new \`delegate_task\` (brief, findings, objections), never a \`t3_thread_send\`. Fix small review findings yourself; start no delegate for them. When told to proceed without a delegate, or to run fewer, \`task_cancel\` the extras. Start no replacement.
-- Launch every parallel child before you follow a running delegate's \`next\`. Call a child running only once its \`taskId\` returns. Don't poll it (\`task_status\`, \`sleep\`, watchers, Monitor, ScheduleWakeup). Stop a delegate run you replace or no longer need with \`task_cancel\`, never \`t3_thread_interrupt\` on a \`childThreadId\`.
+- Follow a running delegate's \`next\`. Don't poll it (\`task_status\`, \`sleep\`, watchers, Monitor, ScheduleWakeup). Stop a delegate run you replace or no longer need with \`task_cancel\`, never \`t3_thread_interrupt\` on a \`childThreadId\`.
- As a subagent or child, do the work yourself. Start no subagents unless your task says to.`,
`- \`t3_thread_launch\` and \`create_threads\` make new top-level threads, only when the user asks for a new thread. "Subagent", "delegate", and "parallel" never mean one. Worktree thread: set \`workspaceStrategy\`. Honor a requested base. Otherwise independent work uses the default branch as \`remote/branch\` with \`startFromOrigin:true\`; with no remote, use the local default. Required unpublished commits and dependent work use their local base. Shell worktrees don't bind it, and no checkout setup in its prompt. No \`workspaceStrategy\`: project root, not your worktree.
- Given a thread id, \`t3_thread_read\` it before you answer about it. Asked to settle a thread, this one too: \`t3_thread_organize\`.