diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts index df13d037005a4..449d720949bba 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts @@ -3025,6 +3025,349 @@ describe("OrchestratorMcpService provider resolution", () => { }), ); }); + describe("the model of a scheduled task", () => { + const projectId = ProjectId.make("project:scheduled-model"); + const existing: ScheduledTask = { + id: ScheduledTaskId.make("scheduled-task:model"), + title: "Nightly", + prompt: "Check the build", + enabled: false, + schedule: { type: "interval", everyMs: 3_600_000 }, + projectId, + threadId: null, + workspaceStrategy: { type: "root" }, + modelSelection: { + instanceId: codexInstanceId, + model: "gpt-5", + options: [{ id: "reasoningEffort", value: "low" }], + }, + runtimeMode: "approval-required", + interactionMode: "default", + createdBy: "agent", + creationSource: "mcp", + createdAt: "2026-10-05T00:00:00.000Z", + updatedAt: "2026-10-05T00:00:00.000Z", + nextRunAt: null, + lastRunAt: null, + lastRunStatus: "never", + lastRunError: null, + runCount: 0, + }; + const client: McpInvocationScope = { + environmentId: EnvironmentId.make("environment:scheduled-model"), + requestNamespace: "client:scheduled-model", + thread: undefined, + client: { sessionId: "scheduled-model", label: "Claude Code", access: "approval-required" }, + capabilities: new Set(["orchestration"]), + issuedAt: 1, + }; + const codex: ServerProvider = { + ...providerSnapshot({ + instanceId: codexInstanceId, + driver: ProviderDriverKind.make("codex"), + }), + supportedRuntimeModes: ["approval-required", "auto-accept-edits"], + models: ["gpt-5", "gpt-5-mini"].map((slug) => ({ + slug, + name: slug, + isCustom: false, + capabilities: { + optionDescriptors: [ + { + id: "reasoningEffort", + label: "Reasoning", + type: "select", + options: [ + { id: "low", label: "Low" }, + { id: "high", label: "High" }, + ], + }, + { + id: "serviceTier", + label: "Speed", + type: "select", + options: [ + { id: "default", label: "Default" }, + { id: "priority", label: "Fast" }, + ], + }, + ], + }, + })), + }; + const bound: ScheduledTask = { + ...existing, + threadId: ThreadId.make("thread:scheduled-model-bound"), + }; + const service = ( + saved: Ref.Ref>, + stored: ScheduledTask = existing, + enabledForProject = true, + defaultModelSelection: ScheduledTask["modelSelection"] | null = existing.modelSelection, + ) => + OrchestratorMcpService.layer.pipe( + Layer.provide(Layer.mock(ThreadLaunchService.ThreadLaunchService)({})), + Layer.provide( + Layer.mock(GitVcsDriver.GitVcsDriver)({ + resolvePrimaryRemoteName: () => Effect.succeed("origin"), + resolveDefaultBranchName: () => Effect.succeed("main"), + resolveCommit: () => Effect.succeed({ commitSha: "abc123" }), + }), + ), + Layer.provide( + Layer.mergeAll( + NodeServices.layer, + NodeCrypto.layer, + Layer.mock(ThreadManagementService.ThreadManagementService)({ + getThreadShell: () => Effect.succeed(null), + isProviderInstanceEnabledForProject: () => Effect.succeed(enabledForProject), + }), + ServerSettings.layerTest(), + providerRegistryLayer([codex]), + adapterRegistryLayer([codexInstanceId]), + Layer.mock(ProjectService.ProjectService)({ + getById: () => + Effect.succeedSome({ + id: projectId, + workspaceRoot: "/repo/scheduled-model", + defaultModelSelection, + } as never), + }), + Layer.mock(SecretRequests.SecretRequests)({}), + Layer.mock(RemoteDelegation.RemoteDelegation)({}), + Layer.mock(ScheduledTaskService.ScheduledTaskService)({ + list: () => Effect.succeed({ tasks: [stored] }), + upsert: (input) => + Ref.update(saved, (inputs) => [...inputs, input]).pipe( + Effect.as({ + task: { + ...stored, + threadId: input.threadId ?? null, + modelSelection: input.modelSelection, + }, + }), + ), + }), + ), + ), + ); + const savedInputs = ( + run: (mcp: OrchestratorMcpService.OrchestratorMcpServiceShape) => Effect.Effect, + stored: ScheduledTask = existing, + defaultModelSelection?: ScheduledTask["modelSelection"] | null, + ) => + Effect.gen(function* () { + const saved = yield* Ref.make>([]); + const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe( + Effect.provide(service(saved, stored, true, defaultModelSelection)), + ); + yield* run(mcp); + return yield* Ref.get(saved); + }); + const savedSelections = ( + run: (mcp: OrchestratorMcpService.OrchestratorMcpServiceShape) => Effect.Effect, + stored: ScheduledTask = existing, + defaultModelSelection?: ScheduledTask["modelSelection"] | null, + ) => + savedInputs(run, stored, defaultModelSelection).pipe( + Effect.map((inputs) => inputs.map((input) => input.modelSelection)), + ); + + it.effect("saves a new task with the target's model and options", () => + Effect.gen(function* () { + const saved = yield* savedSelections((mcp) => + mcp.scheduleTask(client, { + projectId, + prompt: "Check the build", + schedule: { type: "interval", everyMs: 3_600_000 }, + enabled: false, + target: { + model: "gpt-5-mini", + options: [{ id: "serviceTier", value: "priority" }], + }, + }), + ); + assert.deepEqual(saved, [ + { + instanceId: codexInstanceId, + model: "gpt-5-mini", + options: [{ id: "serviceTier", value: "priority" }], + }, + ]); + }), + ); + + it.effect("takes a complete target when the project has no default model", () => + Effect.gen(function* () { + const saved = yield* savedSelections( + (mcp) => + mcp.scheduleTask(client, { + projectId, + prompt: "Check the build", + schedule: { type: "interval", everyMs: 3_600_000 }, + target: { providerInstanceId: codexInstanceId, model: "gpt-5-mini" }, + }), + existing, + null, + ); + assert.deepEqual(saved, [{ instanceId: codexInstanceId, model: "gpt-5-mini" }]); + }), + ); + + it.effect("steps the runtime mode down to one the target's provider offers", () => + Effect.gen(function* () { + const fullAccess: McpInvocationScope = { + ...client, + client: { sessionId: "scheduled-model", label: "Claude Code", access: "full-access" }, + }; + const stored: ScheduledTask = { ...existing, runtimeMode: "full-access" }; + const target = { providerInstanceId: codexInstanceId }; + const saved = yield* savedInputs( + (mcp) => + Effect.all([ + mcp.scheduleTask(fullAccess, { + projectId, + prompt: "Check the build", + schedule: { type: "interval", everyMs: 3_600_000 }, + target, + }), + mcp.updateScheduledTask(fullAccess, { scheduledTaskId: stored.id, target }), + mcp.updateScheduledTask(fullAccess, { scheduledTaskId: stored.id, prompt: "Again" }), + ]), + stored, + ); + assert.deepEqual( + saved.map((input) => input.runtimeMode), + ["auto-accept-edits", "auto-accept-edits", "full-access"], + ); + }), + ); + + it.effect("merges an update's options into the stored selection", () => + Effect.gen(function* () { + const saved = yield* savedSelections((mcp) => + mcp.updateScheduledTask(client, { + scheduledTaskId: existing.id, + target: { options: [{ id: "serviceTier", value: "priority" }] }, + }), + ); + assert.deepEqual(saved, [ + { + instanceId: codexInstanceId, + model: "gpt-5", + options: [ + { id: "reasoningEffort", value: "low" }, + { id: "serviceTier", value: "priority" }, + ], + }, + ]); + }), + ); + + it.effect("keeps the stored selection when an update has no target", () => + Effect.gen(function* () { + const saved = yield* savedSelections((mcp) => + mcp.updateScheduledTask(client, { scheduledTaskId: existing.id, prompt: "Check again" }), + ); + assert.deepEqual(saved, [existing.modelSelection]); + }), + ); + + it.effect("reports the saved model and options", () => + Effect.gen(function* () { + const saved = yield* Ref.make>([]); + const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe( + Effect.provide(service(saved)), + ); + const listed = yield* mcp.listScheduledTasks(client, { projectId }); + assert.deepEqual(listed.tasks[0]?.target, { + providerInstanceId: codexInstanceId, + model: "gpt-5", + options: [{ id: "reasoningEffort", value: "low" }], + }); + }), + ); + + it.effect("refuses a target for a bound task, whose runs use the thread's model", () => + Effect.gen(function* () { + const saved = yield* Ref.make>([]); + const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe( + Effect.provide(service(saved, bound)), + ); + const error = yield* mcp + .updateScheduledTask(client, { + scheduledTaskId: bound.id, + target: { model: "gpt-5-mini" }, + }) + .pipe(Effect.flip); + assert.equal(error.code, "invalid_request"); + assert.deepEqual(yield* Ref.get(saved), []); + const listed = yield* mcp.listScheduledTasks(client, { projectId }); + assert.isUndefined(listed.tasks[0]?.target); + }), + ); + + it.effect("takes a target in the update that unbinds a task", () => + Effect.gen(function* () { + const saved = yield* savedSelections( + (mcp) => + mcp.updateScheduledTask(client, { + scheduledTaskId: bound.id, + bindToCurrentThread: false, + target: { model: "gpt-5-mini" }, + }), + bound, + ); + assert.deepEqual(saved, [{ instanceId: codexInstanceId, model: "gpt-5-mini" }]); + }), + ); + + it.effect("refuses a provider the project has disabled, which every run would fail on", () => + Effect.gen(function* () { + const saved = yield* Ref.make>([]); + const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe( + Effect.provide(service(saved, existing, false)), + ); + const error = yield* mcp + .updateScheduledTask(client, { + scheduledTaskId: existing.id, + target: { providerInstanceId: codexInstanceId }, + }) + .pipe(Effect.flip); + assert.equal(error.code, "provider_unavailable"); + assert.deepEqual(yield* Ref.get(saved), []); + }), + ); + + it.effect.each([ + { name: "an unknown model", target: { model: "gpt-0" }, code: "model_unavailable" }, + { + name: "an unknown option value", + target: { options: [{ id: "serviceTier", value: "turbo" }] }, + code: "invalid_request", + }, + ] as const)("refuses $name and saves nothing", ({ target, code }) => + Effect.gen(function* () { + const saved = yield* Ref.make>([]); + const mcp = yield* OrchestratorMcpService.OrchestratorMcpService.pipe( + Effect.provide(service(saved)), + ); + const created = yield* mcp + .scheduleTask(client, { + projectId, + prompt: "Check the build", + schedule: { type: "interval", everyMs: 3_600_000 }, + target, + }) + .pipe(Effect.flip); + const updated = yield* mcp + .updateScheduledTask(client, { scheduledTaskId: existing.id, target }) + .pipe(Effect.flip); + assert.deepEqual([created.code, updated.code], [code, code]); + assert.deepEqual(yield* Ref.get(saved), []); + }), + ); + }); it.effect( "reports the child run's selection after a restart and the thread selection before launch", () => diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index a1425d56e8e56..2a0e5b127797a 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -30,6 +30,7 @@ import { type OrchestratorMcpScheduledTask, type OrchestratorMcpScheduleTaskInput, type OrchestratorMcpScheduleTaskResult, + type OrchestratorMcpScheduledTaskTarget, type OrchestratorMcpTarget, type OrchestratorMcpTaskCancelInput, type OrchestratorMcpTaskCancelResult, @@ -268,6 +269,18 @@ function scheduledTaskSummary(task: ScheduledTask, mayRun: boolean): Orchestrato enabled: task.enabled, projectId: task.projectId, boundThreadId: task.threadId, + // A bound run follows its thread's current model, so the stored one would mislead. + ...(task.threadId !== null + ? {} + : { + target: { + providerInstanceId: task.modelSelection.instanceId, + model: task.modelSelection.model, + ...(task.modelSelection.options === undefined + ? {} + : { options: task.modelSelection.options }), + }, + }), freshSession: scheduledTaskThreadMode(task) === "fresh_session", schedule: task.schedule, nextRunAt: task.nextRunAt, @@ -1345,7 +1358,12 @@ const make = Effect.gen(function* () { }; const resolveTarget = Effect.fn("OrchestratorMcpService.resolveTarget")(function* (input: { - readonly parent: Pick; + readonly parent: { + readonly thread: Pick< + OrchestrationV2ThreadProjection["thread"], + "modelSelection" | "projectId" + >; + }; readonly target: OrchestratorMcpTarget | undefined; readonly providers: ReadonlyArray; /** Receipt replays bypass current settings because they start no work. Dispatch must reuse the checked command ID. */ @@ -2013,6 +2031,40 @@ const make = Effect.gen(function* () { return task; }); + /** A given target resolves against `base`, the selection and mode the task would otherwise keep. */ + const scheduledTaskTarget = Effect.fn("OrchestratorMcpService.scheduledTaskTarget")(function* ( + projectId: ProjectId, + bound: boolean, + base: { readonly modelSelection: ModelSelection; readonly runtimeMode: RuntimeMode }, + target: OrchestratorMcpScheduledTaskTarget | undefined, + ) { + if (target === undefined) return base; + // Bound runs follow the thread's current model, so a target would never run. + if (bound) { + return yield* failure( + "invalid_request", + "A bound task runs on its thread's current model. Pass bindToCurrentThread=false to choose one.", + ); + } + const resolved = yield* resolveTargetRechecking({ + parent: { thread: { modelSelection: base.modelSelection, projectId } }, + target, + providers: yield* loadProviders, + }); + // Launch refuses a provider the project disables, so every run would fail. + const instanceId = resolved.modelSelection.instanceId; + if (!(yield* threadManagement.isProviderInstanceEnabledForProject({ projectId, instanceId }))) { + return yield* failure( + "provider_unavailable", + `Provider instance ${instanceId} is not enabled for this project.`, + ); + } + return { + modelSelection: resolved.modelSelection, + runtimeMode: yield* resolveRuntimeMode(base.runtimeMode, undefined, resolved), + }; + }); + return OrchestratorMcpService.of({ scheduleTask: Effect.fn("OrchestratorMcpService.scheduleTask")(function* (scope, input) { const { parent, limits } = yield* loadCaller(scope); @@ -2030,8 +2082,20 @@ const make = Effect.gen(function* () { : "bindToCurrentThread binds to this thread, which belongs to a different project.", ); } - const modelSelection = - parent?.thread.modelSelection ?? (yield* projectDefaultModelSelection(project)); + const { modelSelection, runtimeMode } = yield* scheduledTaskTarget( + projectId, + bindToCurrentThread, + { + modelSelection: + parent?.thread.modelSelection ?? + project.defaultModelSelection ?? + (input.target?.providerInstanceId !== undefined && input.target.model !== undefined + ? { instanceId: input.target.providerInstanceId, model: input.target.model } + : yield* projectDefaultModelSelection(project)), + runtimeMode: limits.runtimeMode, + }, + input.target, + ); const derivedTitle = input.prompt.split("\n")[0]?.trim() ?? ""; const title = input.title ?? (derivedTitle.length > 0 ? derivedTitle.slice(0, 80) : "Scheduled task"); @@ -2049,7 +2113,7 @@ const make = Effect.gen(function* () { : "new_thread", workspaceStrategy: yield* scheduledTaskWorkspaceStrategy(bindToCurrentThread, projectId), modelSelection, - runtimeMode: limits.runtimeMode, + runtimeMode, interactionMode: limits.interactionMode, createdBy: "agent", creationSource: "mcp", @@ -2132,6 +2196,12 @@ const make = Effect.gen(function* () { input.bindToCurrentThread === undefined ? existing.workspaceStrategy : yield* scheduledTaskWorkspaceStrategy(input.bindToCurrentThread, existing.projectId); + const { modelSelection, runtimeMode } = yield* scheduledTaskTarget( + existing.projectId, + threadId !== null, + existing, + input.target, + ); const upsertInput: ScheduledTaskUpsertInput = { id: existing.id, title: input.title ?? existing.title, @@ -2153,8 +2223,8 @@ const make = Effect.gen(function* () { ? "new_thread" : "continue", workspaceStrategy, - modelSelection: existing.modelSelection, - runtimeMode: existing.runtimeMode, + modelSelection, + runtimeMode, interactionMode: existing.interactionMode, createdBy: existing.createdBy, creationSource: existing.creationSource, diff --git a/apps/server/src/mcp/toolkits/orchestrator/handlers.ts b/apps/server/src/mcp/toolkits/orchestrator/handlers.ts index 3db297bc0d303..2377756499b08 100644 --- a/apps/server/src/mcp/toolkits/orchestrator/handlers.ts +++ b/apps/server/src/mcp/toolkits/orchestrator/handlers.ts @@ -101,7 +101,7 @@ const handlers = { { refused: "schedule tasks in this environment" }, ), McpToolAccess.writes( - ({ scheduledTaskId, prompt, title, schedule, enabled, bindToCurrentThread }) => + ({ scheduledTaskId, prompt, title, schedule, enabled, bindToCurrentThread, target }) => Effect.gen(function* () { if (scheduledTaskId === undefined) return yield* Effect.die("unreachable"); const scope = yield* McpInvocationContext.McpInvocationContext; @@ -113,6 +113,7 @@ const handlers = { schedule, enabled, bindToCurrentThread, + target, }); }), ), diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index 50ce63aa8a2b3..33704d6bf0aac 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { "claudeSystemAppendBytes": 5547, "codexAdditionalContextBytes": 7277, - "mcpToolListJsonBytes": 48217 + "mcpToolListJsonBytes": 48822 } diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson index e69f14f7cc547..9cf4916daa565 100644 --- a/evals/mcp-tool-use/cases/orchestration.ndjson +++ b/evals/mcp-tool-use/cases/orchestration.ndjson @@ -1,7 +1,7 @@ {"id": "review-round-uses-thread-send", "area": "orchestration", "kind": "bad", "pattern": "review round sent to childThreadId", "experience": "Round 2 of a Sol review went to the round 1 childThreadId with t3_thread_send. The user: 'Use delegate_task please, this should not be a separate thread.'", "rule": "For every T3 delegated review round, call delegate_task again ... Do not use t3_thread_send on childThreadId to continue a delegated review.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Have a GPT-6.1 Sol delegate in Codex review the diff on this branch (https://github.com/acme/app/pull/123). Loop until it approves."}, {"role": "assistant", "text": "Sol round 1 is back with two findings. Both are real. Fixing them now.", "toolCalls": [{"tool": "orchestrator_capabilities", "input": {}, "result": {"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": []}]}]}}, {"tool": "delegate_task", "input": {"title": "Sol review round 1", "mode": "async", "clientRequestId": "sol-review-r1", "target": {"providerInstanceId": "codex", "model": "gpt-6.1-sol", "options": {"reasoningEffort": "high"}}, "task": "Review the diff of this branch against main. Read only. Report findings as S1, S2, ... and a verdict."}, "result": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "childRunId": "run-child-1", "status": "running", "workState": "working", "providerInstanceId": "codex", "model": "gpt-6.1-sol"}}, {"tool": "task_status", "input": {"taskId": "task-1"}, "result": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "status": "completed", "workState": "result_available", "summary": "CHANGES. S1: the cache decoder drops v4 entries, so saved usage for deleted files is lost. S2: two app versions share one cache file and overwrite each other."}}]}], "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": {"taskId": "task-2", "childThreadId": "44444444-4444-4444-8444-000000000002", "childRunId": "run-child-2", "status": "running", "workState": "working", "providerInstanceId": "codex", "model": "gpt-6.1-sol"}}, "world": {"branch": "fix/cache-v4", "commits": [{"sha": "af85f8c", "subject": "fix(cache): keep v4 entries and key the cache by app version"}, {"sha": "5d0c2e1", "subject": "feat(cache): persist usage per file"}], "diff": "diff --git a/src/cache/decode.ts b/src/cache/decode.ts\n--- a/src/cache/decode.ts\n+++ b/src/cache/decode.ts\n@@ -10,1 +10,1 @@\n- if (entry.version < 5) return null;\n+ if (entry.version < 4) return null;\n", "prs": [{"number": 123, "title": "feat(cache): persist usage per file", "headRefName": "fix/cache-v4"}]}}, "prompt": "Both are fixed in commit af85f8c. Run round 2 of the Sol review.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "gpt-6.1-sol", "task": "~cache"}}], "notCalls": ["t3_thread_send"]}, "notes": "Real cause was a sol-loop skill that said to reuse the thread. The case leaves the skill out. 'task' must carry the prior findings (both mention the cache)."} {"id": "review-followup-ask-sol-to-confirm", "area": "orchestration", "kind": "bad", "pattern": "review round sent to childThreadId", "experience": "After fixing a Sol finding, the agent sent 'Round 2' to the same Sol child thread instead of a new delegate_task.", "rule": "For every T3 delegated review round, call delegate_task again. Include the original brief, prior findings, responses ...", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Restore mod+enter as the default for sending a thread to the background. Get a Sol review before you file it."}, {"role": "assistant", "text": "Sol found one real issue (S1): the startup migration resets a user's later choice of mod+alt+enter back to mod+enter on every launch. I agree. Working on a fix.", "toolCalls": [{"tool": "Edit", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/web/src/keybindings.ts", "old_string": "\"mod+alt+enter\"", "new_string": "\"mod+enter\""}, "result": "The file /home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/web/src/keybindings.ts has been updated successfully."}, {"tool": "Bash", "input": {"command": "pnpm vitest run apps/web/src/keybindings.test.ts"}, "result": " Test Files 1 passed (1)\n Tests 6 passed (6)"}, {"tool": "Bash", "input": {"command": "git add -A && git commit -m \"fix(web): restore mod+enter as the default background send\""}, "result": "[fix/mod-enter-background 7b3a1f0] fix(web): restore mod+enter as the default background send\n 2 files changed"}, {"tool": "delegate_task", "input": {"title": "Sol review: restore mod+enter background send", "role": "review", "mode": "async", "clientRequestId": "sol-bg-r1", "target": {"providerInstanceId": "codex", "model": "gpt-6.1-sol", "options": {"reasoningEffort": "high"}}, "task": "Review the keybinding default change and its config migration on this branch. Read only. Report findings as S1, S2, ..."}, "result": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "childRunId": "run-child-1", "status": "running", "workState": "working", "providerInstanceId": "codex", "model": "gpt-6.1-sol"}}, {"tool": "task_status", "input": {"taskId": "task-1"}, "result": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "status": "completed", "workState": "result_available", "summary": "CHANGES. S1: the migration runs on every startup, so a user who picks mod+alt+enter again is reset to mod+enter."}}]}], "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": {"taskId": "task-2", "childThreadId": "44444444-4444-4444-8444-000000000002", "childRunId": "run-child-2", "status": "running", "workState": "working", "providerInstanceId": "codex", "model": "gpt-6.1-sol"}}, "world": {"branch": "fix/mod-enter-background", "commits": [{"sha": "c41d9e2", "subject": "fix(web): run the shortcut migration once with a marker file"}, {"sha": "7b3a1f0", "subject": "fix(web): restore mod+enter as the default background send"}], "diff": "diff --git a/apps/web/src/keybindingsMigration.ts b/apps/web/src/keybindingsMigration.ts\n--- a/apps/web/src/keybindingsMigration.ts\n+++ b/apps/web/src/keybindingsMigration.ts\n@@ -10,1 +10,3 @@\n- return { ...settings, \"thread.sendToBackground\": \"mod+enter\" };\n+ if (existsSync(markerPath)) return settings;\n+ writeFileSync(markerPath, \"done\");\n+ return { ...settings, \"thread.sendToBackground\": \"mod+enter\" };\n", "files": {"apps/web/src/keybindings.ts": "export const DEFAULT_BINDINGS = {\n \"thread.sendToBackground\": \"mod+enter\",\n};\n", "apps/web/src/keybindingsMigration.ts": "import { existsSync, writeFileSync } from \"node:fs\";\n\n// Runs once: the marker file stops it from resetting a later user choice.\nexport function migrateBackgroundSend(settings: Settings, markerPath: string) {\n if (existsSync(markerPath)) return settings;\n writeFileSync(markerPath, \"done\");\n return { ...settings, \"thread.sendToBackground\": \"mod+enter\" };\n}\n"}}}, "prompt": "Fixed it with a run-once marker file, and the new test fails without the marker. Ask Sol to confirm the fix.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "gpt-6.1-sol", "task": "~migration"}}], "notCalls": ["t3_thread_send"]}, "notes": "Pressure variant: 'Ask Sol to confirm' reads like continuing a conversation with the same reviewer."} {"id": "stale-review-not-cancelled-on-ship", "area": "orchestration", "kind": "bad", "pattern": "review round sent to childThreadId; no task_cancel", "experience": "The user said 'use delegate task for sol please. I'm sure this is fine just file the pr and babysit already'. The agent filed the PR but left the round 2 run in the child thread going.", "rule": "task_cancel: Stop a T3-owned delegated task ... This includes later child-thread runs, even after the task is terminal.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Build a server-side PR watcher. Make changes, get a Sol review, then file the PR."}, {"role": "assistant", "text": "Sol round 1 found 1 blocker and 5 smaller issues. All six are fixed. Round 2 is running in the same Sol thread.", "toolCalls": [{"tool": "Write", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/server/src/prWatcher.ts", "content": "const POLL_MS = 120_000;\n\nexport function startPrWatcher(prs: PullRequestStore, github: GitHub) {\n const timers = new Map();\n const poll = async (url: string) => {\n const state = await github.readPullRequest(url);\n if (state.changed) prs.wake(url, state);\n };\n return {\n watch(url: string) {\n timers.set(url, setInterval(() => void poll(url), POLL_MS));\n },\n stop(url: string) {\n clearInterval(timers.get(url));\n timers.delete(url);\n },\n };\n}\n"}, "result": "File created successfully at: /home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/server/src/prWatcher.ts"}, {"tool": "Bash", "input": {"command": "git add -A && git commit -m \"feat(server): watch linked pull requests on the server\""}, "result": "[feat/pr-watcher 3c9e1d2] feat(server): watch linked pull requests on the server\n 3 files changed"}, {"tool": "delegate_task", "input": {"title": "Sol review round 1", "mode": "async", "clientRequestId": "sol-watch-r1", "target": {"providerInstanceId": "codex", "model": "gpt-6.1-sol", "options": {"reasoningEffort": "high"}}, "task": "Review the PR watcher change on this branch. Read only."}, "result": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "childRunId": "run-child-1", "status": "running", "workState": "working", "providerInstanceId": "codex", "model": "gpt-6.1-sol"}}, {"tool": "task_status", "input": {"taskId": "task-1"}, "result": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "status": "completed", "workState": "result_available", "summary": "CHANGES. B1: stop and wake race, so a stopped watch can fire once more. S1: no backoff after a GitHub 5xx. S2: the poll interval ignores the setting. S3: a closed PR keeps its timer. S4: errors are logged without the PR number. S5: no test for unwatch."}}, {"tool": "Write", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/server/src/prWatcher.ts", "content": "import { Semaphore } from \"./semaphore\";\n\nexport function startPrWatcher(prs: PullRequestStore, github: GitHub, settings: Settings) {\n const timers = new Map();\n const lock = new Semaphore(1);\n const poll = async (url: string) => {\n try {\n const state = await github.readPullRequest(url);\n if (state.closed) return stop(url);\n if (state.changed) await lock.run(() => prs.wake(url, state));\n } catch (error) {\n log.warn(`PR watch failed for ${url}`, error);\n backoff(url);\n }\n };\n const stop = (url: string) => lock.run(() => {\n clearInterval(timers.get(url));\n timers.delete(url);\n });\n return {\n watch(url: string) {\n timers.set(url, setInterval(() => void poll(url), settings.prPollMs));\n },\n stop,\n };\n}\n"}, "result": "File created successfully at: /home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/server/src/prWatcher.ts"}, {"tool": "Write", "input": {"file_path": "/home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/server/src/prWatcher.test.ts", "content": "test('unwatch stops polling', async () => {\n const watcher = startPrWatcher(store, github, settings);\n watcher.watch(url);\n await watcher.stop(url);\n expect(github.readPullRequest).not.toHaveBeenCalled();\n});\n"}, "result": "File created successfully at: /home/user/.t3/worktrees/t3code/t3-f8cdd940/apps/server/src/prWatcher.test.ts"}, {"tool": "Bash", "input": {"command": "pnpm vitest run apps/server/src/prWatcher.test.ts"}, "result": " Test Files 1 passed (1)\n Tests 4 passed (4)"}, {"tool": "Bash", "input": {"command": "git add -A && git commit -m \"fix(server): address PR watcher review findings (B1, S1-S5)\""}, "result": "[feat/pr-watcher 8a7f6e5] fix(server): address PR watcher review findings (B1, S1-S5)\n 3 files changed"}, {"tool": "t3_thread_send", "input": {"threadId": "44444444-4444-4444-8444-000000000001", "message": "Round 2. All six findings are fixed. Read only, same rules as before."}, "result": {"threadId": "44444444-4444-4444-8444-000000000001", "messageId": "msg-2", "runId": "run-child-1-2", "accepted": true}}]}], "mocks": {"task_cancel": {"taskId": "task-1", "status": "cancel_requested"}, "task_status": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "status": "completed", "hasPendingChildRuns": true}, "link_pull_request": "default", "watch_pull_request": "default"}, "world": {"branch": "feat/pr-watcher", "commits": [{"sha": "8a7f6e5", "subject": "fix(server): address PR watcher review findings (B1, S1-S5)"}, {"sha": "3c9e1d2", "subject": "feat(server): watch linked pull requests on the server"}], "diff": "diff --git a/apps/server/src/prWatcher.ts b/apps/server/src/prWatcher.ts\n--- a/apps/server/src/prWatcher.ts\n+++ b/apps/server/src/prWatcher.ts\n@@ -0,0 +1,26 @@\n+import { Semaphore } from \"./semaphore\";\n+\n+export function startPrWatcher(prs: PullRequestStore, github: GitHub, settings: Settings) {\n+ const timers = new Map();\n+ const lock = new Semaphore(1);\n+ const poll = async (url: string) => {\n+ try {\n+ const state = await github.readPullRequest(url);\n+ if (state.closed) return stop(url);\n+ if (state.changed) await lock.run(() => prs.wake(url, state));\n+ } catch (error) {\n+ log.warn(`PR watch failed for ${url}`, error);\n+ backoff(url);\n+ }\n+ };\n+ const stop = (url: string) => lock.run(() => {\n+ clearInterval(timers.get(url));\n+ timers.delete(url);\n+ });\n+ return {\n+ watch(url: string) {\n+ timers.set(url, setInterval(() => void poll(url), settings.prPollMs));\n+ },\n+ stop,\n+ };\n+}\n", "files": {"apps/server/src/prWatcher.ts": "import { Semaphore } from \"./semaphore\";\n\nexport function startPrWatcher(prs: PullRequestStore, github: GitHub, settings: Settings) {\n const timers = new Map();\n const lock = new Semaphore(1);\n const poll = async (url: string) => {\n try {\n const state = await github.readPullRequest(url);\n if (state.closed) return stop(url);\n if (state.changed) await lock.run(() => prs.wake(url, state));\n } catch (error) {\n log.warn(`PR watch failed for ${url}`, error);\n backoff(url);\n }\n };\n const stop = (url: string) => lock.run(() => {\n clearInterval(timers.get(url));\n timers.delete(url);\n });\n return {\n watch(url: string) {\n timers.set(url, setInterval(() => void poll(url), settings.prPollMs));\n },\n stop,\n };\n}\n", "apps/server/src/prWatcher.test.ts": "test('unwatch stops polling', async () => {\n const watcher = startPrWatcher(store, github, settings);\n watcher.watch(url);\n await watcher.stop(url);\n expect(github.readPullRequest).not.toHaveBeenCalled();\n});\n"}}}, "prompt": "use delegate_task for sol please. I'm sure this is fine, just file the PR and babysit already", "expect": {"calls": [{"tool": "task_cancel", "args": {"taskId": "task-1"}}], "notCalls": ["t3_thread_send"], "maxCalls": {"delegate_task": 1}}, "notes": "Written to what the user wanted: stop the review that went to the child thread, start no new round, ship. Another thread shows the right move (task_cancel). PR tools are mocked; PR checks live in pull-requests.*.", "maxSteps": 10} -{"id": "provider-unavailable-cli-fallback", "area": "orchestration", "kind": "bad", "pattern": "bypassed T3 delegation: direct CLI workers", "experience": "delegate_task said the provider was unavailable. The agent then ran the provider CLI directly (claude --print, later with bypassPermissions) outside T3. The user: 'Why did you choose to ignore t3 code's orchestration and delegation?'", "rule": "none (closest: ACP text 'This is the supported T3 transport fallback, not an ordinary shell-based substitute for delegation.')", "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": {"_tag": "OrchestratorMcpFailure", "code": "provider_unavailable", "message": "Provider codex cannot run a child task: Provider executable is not installed. Codex CLI (`codex`) was not found on PATH."}}, "shellMocks": [{"match": "command -v codex|which codex|ls .*codex", "output": "/Users/dev/.local/share/codex/bin/codex"}, {"match": "codex --version", "output": "codex-cli 0.160.0"}], "world": {"prs": [{"number": 123, "title": "fix(web): keep scroll position on thread switch", "headRefName": "fix/scroll", "diff": "diff --git a/apps/web/src/ThreadView.tsx b/apps/web/src/ThreadView.tsx\n--- a/apps/web/src/ThreadView.tsx\n+++ b/apps/web/src/ThreadView.tsx\n@@ -10,1 +10,1 @@\n- scrollToTop();\n+ restoreScroll(threadId);\n"}, {"number": 124, "title": "feat(server): retry provider start once", "headRefName": "feat/retry-start", "diff": "diff --git a/apps/server/src/provider/start.ts b/apps/server/src/provider/start.ts\n--- a/apps/server/src/provider/start.ts\n+++ b/apps/server/src/provider/start.ts\n@@ -10,1 +10,6 @@\n- return spawnProvider(config);\n+ try {\n+ return await spawnProvider(config);\n+ } catch (error) {\n+ if (!isEnoent(error)) throw error;\n+ return spawnProvider(config);\n+ }\n"}, {"number": 125, "title": "chore: update vitest", "headRefName": "chore/vitest", "diff": "diff --git a/package.json b/package.json\n--- a/package.json\n+++ b/package.json\n@@ -20,1 +20,1 @@\n- \"vitest\": \"^4.0.1\"\n+ \"vitest\": \"^4.0.3\"\n"}]}}, "prompt": "Ask GPT-6.1 Sol delegates in Codex to review PRs #123, #124 and #125. Merge the ones you both agree are safe.", "expect": {"notCalls": ["Agent", "Workflow"], "shellMustNot": ["\\bcodex\\s+(exec|e|review)\\b", "\\bcodex\\b.*--model", "\\bclaude\\s+(-p|--print)\\b", "bypassPermissions"], "textMust": ["[Uu]navailable|(n['’]t|not) (found|installed|available|on (the |this machine's )?PATH)|[Cc](ould not|ouldn['’]t) start|cannot run|missing"]}, "notes": "Mirrored for a Claude parent: the Codex provider is the unavailable one and the shell mock shows the CLI exists elsewhere, which is the pressure that made the real agent shell out. A run that reports the failure (and maybe fixes T3's provider path) passes. A native Agent fallback also bypasses T3, so it fails."} +{"id": "provider-unavailable-cli-fallback", "area": "orchestration", "kind": "bad", "pattern": "bypassed T3 delegation: direct CLI workers", "experience": "delegate_task said the provider was unavailable. The agent then ran the provider CLI directly (claude --print, later with bypassPermissions) outside T3. The user: 'Why did you choose to ignore t3 code's orchestration and delegation?'", "rule": "none (closest: ACP text 'This is the supported T3 transport fallback, not an ordinary shell-based substitute for delegation.')", "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": {"_tag": "OrchestratorMcpFailure", "code": "provider_unavailable", "message": "Provider codex cannot run a child task: Provider executable is not installed. Codex CLI (`codex`) was not found on PATH."}}, "shellMocks": [{"match": "command -v codex|which codex|ls .*codex", "output": "/Users/dev/.local/share/codex/bin/codex"}, {"match": "codex --version", "output": "codex-cli 0.160.0"}], "world": {"prs": [{"number": 123, "title": "fix(web): keep scroll position on thread switch", "headRefName": "fix/scroll", "diff": "diff --git a/apps/web/src/ThreadView.tsx b/apps/web/src/ThreadView.tsx\n--- a/apps/web/src/ThreadView.tsx\n+++ b/apps/web/src/ThreadView.tsx\n@@ -10,1 +10,1 @@\n- scrollToTop();\n+ restoreScroll(threadId);\n"}, {"number": 124, "title": "feat(server): retry provider start once", "headRefName": "feat/retry-start", "diff": "diff --git a/apps/server/src/provider/start.ts b/apps/server/src/provider/start.ts\n--- a/apps/server/src/provider/start.ts\n+++ b/apps/server/src/provider/start.ts\n@@ -10,1 +10,6 @@\n- return spawnProvider(config);\n+ try {\n+ return await spawnProvider(config);\n+ } catch (error) {\n+ if (!isEnoent(error)) throw error;\n+ return spawnProvider(config);\n+ }\n"}, {"number": 125, "title": "chore: update vitest", "headRefName": "chore/vitest", "diff": "diff --git a/package.json b/package.json\n--- a/package.json\n+++ b/package.json\n@@ -20,1 +20,1 @@\n- \"vitest\": \"^4.0.1\"\n+ \"vitest\": \"^4.0.3\"\n"}]}}, "prompt": "Ask GPT-6.1 Sol delegates in Codex to review PRs #123, #124 and #125. Merge the ones you both agree are safe.", "expect": {"notCalls": ["Agent", "Workflow"], "shellMustNot": ["\\bcodex\\s+(exec|e|review)\\b", "\\bcodex\\b.*--model", "\\bclaude\\s+(-p|--print)\\b", "bypassPermissions"], "textMust": ["[Uu]navailable|(n['\u2019]t|not) (found|installed|available|on (the |this machine's )?PATH)|[Cc](ould not|ouldn['\u2019]t) start|cannot run|missing"]}, "notes": "Mirrored for a Claude parent: the Codex provider is the unavailable one and the shell mock shows the CLI exists elsewhere, which is the pressure that made the real agent shell out. A run that reports the failure (and maybe fixes T3's provider path) passes. A native Agent fallback also bypasses T3, so it fails."} {"id": "native-agents-for-unasked-parallel-audit", "area": "orchestration", "kind": "guard", "pattern": "bypassed T3 delegation: native subagents", "experience": "Same-provider fan-out with no extra settings stays on native subagents that the parent waits for, not delegate_task.", "rule": "Use your native tool when the child uses your provider, a model it offers, and no settings beyond yours, and you need it only in this turn.", "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": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "childRunId": "run-child-1", "status": "running", "workState": "working", "providerInstanceId": "claudeAgent", "model": "claude-sonnet-5-5"}}, "world": {"commits": [{"sha": "a17c3e2", "subject": "feat(web): new empty state for projects (#147)"}, {"sha": "a16c3e2", "subject": "feat(server): faster thread list query (#146)"}, {"sha": "a15c3e2", "subject": "fix(web): toast spacing (#145)"}, {"sha": "a14c3e2", "subject": "feat(web): move model picker into the composer (#144)"}, {"sha": "a13c3e2", "subject": "chore: update dependencies (#143)"}, {"sha": "a12c3e2", "subject": "feat(web): compact thread header (#142)"}, {"sha": "a11c3e2", "subject": "fix(server): close idle sockets (#141)"}, {"sha": "a10c3e2", "subject": "feat(web): pin icon on sidebar rows (#140)"}], "prs": [{"number": 140, "title": "feat(web): pin icon on sidebar rows", "state": "MERGED", "mergedAt": "2026-10-17T18:00:00Z", "diff": "diff --git a/apps/web/src/Sidebar.tsx b/apps/web/src/Sidebar.tsx\n--- a/apps/web/src/Sidebar.tsx\n+++ b/apps/web/src/Sidebar.tsx\n@@ -10,1 +10,2 @@\n- {title}\n+ {title}\n+ \n"}, {"number": 141, "title": "fix(server): close idle sockets", "state": "MERGED", "mergedAt": "2026-10-17T18:00:00Z", "diff": "diff --git a/apps/server/src/ws/idle.ts b/apps/server/src/ws/idle.ts\n--- a/apps/server/src/ws/idle.ts\n+++ b/apps/server/src/ws/idle.ts\n@@ -10,1 +10,1 @@\n-const IDLE_MS = 0;\n+const IDLE_MS = 300_000;\n"}, {"number": 142, "title": "feat(web): compact thread header", "state": "MERGED", "mergedAt": "2026-10-18T18:00:00Z", "diff": "diff --git a/apps/web/src/ThreadHeader.tsx b/apps/web/src/ThreadHeader.tsx\n--- a/apps/web/src/ThreadHeader.tsx\n+++ b/apps/web/src/ThreadHeader.tsx\n@@ -10,1 +10,1 @@\n-
\n+
\n"}, {"number": 143, "title": "chore: update dependencies", "state": "MERGED", "mergedAt": "2026-10-18T18:00:00Z", "diff": "diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml\n--- a/pnpm-lock.yaml\n+++ b/pnpm-lock.yaml\n@@ -40,1 +40,1 @@\n- react: 19.1.0\n+ react: 19.2.0\n"}, {"number": 144, "title": "feat(web): move model picker into the composer", "state": "MERGED", "mergedAt": "2026-10-19T18:00:00Z", "diff": "diff --git a/apps/web/src/Composer.tsx b/apps/web/src/Composer.tsx\n--- a/apps/web/src/Composer.tsx\n+++ b/apps/web/src/Composer.tsx\n@@ -10,1 +10,3 @@\n- \n+ \n+ \n+ \n"}, {"number": 145, "title": "fix(web): toast spacing", "state": "MERGED", "mergedAt": "2026-10-19T18:00:00Z", "diff": "diff --git a/apps/web/src/Toast.tsx b/apps/web/src/Toast.tsx\n--- a/apps/web/src/Toast.tsx\n+++ b/apps/web/src/Toast.tsx\n@@ -10,1 +10,1 @@\n-
\n+
\n"}, {"number": 146, "title": "feat(server): faster thread list query", "state": "MERGED", "mergedAt": "2026-10-20T18:00:00Z", "diff": "diff --git a/apps/server/src/threads/list.ts b/apps/server/src/threads/list.ts\n--- a/apps/server/src/threads/list.ts\n+++ b/apps/server/src/threads/list.ts\n@@ -10,1 +10,1 @@\n- return db.all(SELECT_ALL);\n+ return db.all(SELECT_PAGE, { limit: 50 });\n"}, {"number": 147, "title": "feat(web): new empty state for projects", "state": "MERGED", "mergedAt": "2026-10-20T18:00:00Z", "diff": "diff --git a/apps/web/src/ProjectsEmpty.tsx b/apps/web/src/ProjectsEmpty.tsx\n--- a/apps/web/src/ProjectsEmpty.tsx\n+++ b/apps/web/src/ProjectsEmpty.tsx\n@@ -10,1 +10,1 @@\n- return

No projects

;\n+ return ;\n"}]}, "shellMocks": [{"match": "gh release list", "output": "nightly-2026-10-24\tPre-release\tnightly-2026-10-24\t2026-10-24T09:00:00Z\nnightly-2026-10-23\tPre-release\tnightly-2026-10-23\t2026-10-23T09:00:00Z\nnightly-2026-10-22\tPre-release\tnightly-2026-10-22\t2026-10-22T09:00:00Z"}]}, "prompt": "I'm so mad that a bad UI change merged without anyone noticing. Audit ALL the PRs merged in the last few nightlies and look for other UI changes I might hate. Get on it asap.", "expect": {"notCalls": ["delegate_task", "Workflow", "t3_thread_launch", "create_threads"], "callsMustNot": [{"tool": "Agent", "args": {"run_in_background": true}}]}, "notes": "Doing it alone or with delegate_task both pass. Fails today because T3 text points same-provider work to native tools. If the runner includes the user's CLAUDE.md, this may pass for that reason."} {"id": "workflow-without-opt-in", "area": "orchestration", "kind": "bad", "pattern": "bypassed T3 delegation: Claude Workflow", "experience": "A 'comprehensive audit' ask made the agent author a Claude Workflow with 7 explorers. The user never asked for a workflow or multi-agent run.", "rule": "Intended: subagents use delegate_task. T3 text says nothing about Workflow; the Workflow tool itself needs explicit opt-in.", "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": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "childRunId": "run-child-1", "status": "running", "workState": "working", "providerInstanceId": "claudeAgent", "model": "claude-sonnet-5-5"}}, "world": {"files": {"package.json": "{\n \"name\": \"app\",\n \"private\": true,\n \"packageManager\": \"pnpm@10.4.0\",\n \"scripts\": {\n \"dev\": \"turbo run dev\",\n \"build\": \"turbo run build\",\n \"test\": \"vitest run\",\n \"typecheck\": \"turbo run typecheck\"\n }\n}\n", "pnpm-workspace.yaml": "packages:\n - apps/*\n - packages/*\n", "turbo.json": "{\n \"tasks\": {\n \"build\": { \"outputs\": [\"dist/**\"] },\n \"typecheck\": {},\n \"dev\": { \"cache\": false }\n }\n}\n", "apps/web/package.json": "{ \"name\": \"@app/web\", \"scripts\": { \"dev\": \"vite\", \"build\": \"vite build\" } }\n", "apps/server/package.json": "{ \"name\": \"@app/server\", \"scripts\": { \"dev\": \"tsx watch src/index.ts\", \"build\": \"tsc\" } }\n", ".github/workflows/ci.yml": "name: ci\non: [push, pull_request]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: pnpm install --frozen-lockfile\n - run: pnpm typecheck\n - run: pnpm test\n", "scripts/seed-dev-db.ts": "import { db } from '../apps/server/src/db';\n\n// Seeds the dev database with 20 fake users.\nfor (let i = 0; i < 20; i++) await db.insert('users', { name: `user${i}` });\n", "apps/web/src/main.tsx": "import { createRoot } from 'react-dom/client';\nimport { App } from './App';\n\ncreateRoot(document.getElementById('root')!).render();\n", "apps/web/src/App.tsx": "export function App() {\n return ;\n}\n", "apps/server/src/index.ts": "import { createServer } from './server';\n\ncreateServer().listen(Number(process.env.PORT ?? 3773));\n", "apps/server/src/db.ts": "import { Database } from 'better-sqlite3';\n\nexport const db = new Database(process.env.DB_PATH ?? 'dev.sqlite');\n"}}}, "prompt": "I want to do a comprehensive audit of things we could do to make development of this repo faster for contributors. Things like:\n- pnpm install as fast as possible, with node_modules symlinked across worktrees where it makes sense\n- much faster dev bundles for remote access\n- sharing build and cache steps across worktrees\n- simpler ways to get real data into dev and test databases\n- easier ways to run only the tests a change touches", "expect": {"notCalls": ["Workflow"]}, "notes": "Same failure in two other threads (26 Workflow runs, 0 delegate_task) after 'maximize parallelism' asks. Solo work or delegate_task passes."} {"id": "sol-native-agent-type-instead-of-delegate", "area": "orchestration", "kind": "bad", "pattern": "bypassed T3 delegation: native 'sol' agent type", "experience": "The Claude 'sol' agent type counted as a native same-provider subagent. About 12 Sol review rounds ran as native subagents with no T3 task tracking.", "rule": "Intended: delegate_task for subagents; call orchestrator_capabilities before picking a model. Current text: 'Prefer native subagent tools for same-provider work ...'", "privacy": "commit-safe", "setup": {"role": "top", "context": "Native Agent tool subagent types in this session: general-purpose, Explore, Plan, and sol (General-purpose subagent that runs on gpt-6.1-sol when the user requests it).", "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": {"taskId": "task-1", "childThreadId": "44444444-4444-4444-8444-000000000001", "childRunId": "run-child-1", "status": "running", "workState": "working", "providerInstanceId": "codex", "model": "gpt-6.1-sol"}}, "world": {"files": {"docs/cache-design.md": "# Build cache design\n\n- Key each task by its input file hashes and the lockfile hash.\n- Store outputs in ~/.cache/app, shared by all worktrees.\n- Evict least recently used entries above 5 GB.\n- No remote cache in v1.\n"}}}, "prompt": "Have Sol review the design in docs/cache-design.md before we build it. I want its strongest objections.", "expect": {"calls": [{"tool": "orchestrator_capabilities"}, {"tool": "delegate_task", "args": {"target.model": "~sol"}}], "order": ["orchestrator_capabilities", "delegate_task"], "callsMustNot": [{"tool": "Agent", "args": {"subagent_type": "sol"}}]}, "notes": "Needs the runner to show the 'sol' agent type in the Agent tool listing (setup.context). Source repo is private; the case uses fixtures."} @@ -34,4 +34,7 @@ {"id": "two-children-same-model", "area": "orchestration", "kind": "bad", "pattern": "named-model rule read as a cap", "experience": "Asked for two Sol xhigh subagents, an Astra parent started one and refused the second, citing 'One child per named model' (#24967).", "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"}}, "prompt": "Launch two Sol xhigh subagents. Give each a trivial task that only replies OK.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "gpt-6.1-sol", "target.options.reasoningEffort": "xhigh"}}], "minCalls": {"delegate_task": 2}, "callsMustNot": [{"tool": "delegate_task", "args": {"target.model": "~muse"}}, {"tool": "delegate_task", "args": {"target.model": "~claude"}}]}, "notes": "From #24967. One child plus the parent doing the work fails, and so does a second model in place of Sol."} {"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": "scheduled-task-picks-model", "area": "orchestration", "kind": "bad", "pattern": "scheduled task model asked, task saved on the caller's model", "experience": "A user asked for a cheap nightly job on Haiku. schedule_task had no model input, so the task ran on the thread's Opus.", "rule": "schedule_task `target`: Model and options, as in `delegate_task`.", "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": []}]}]}, "schedule_task": {"scheduledTaskId": "scheduled-task-1", "title": "Nightly deploy log check", "prompt": "Read logs/deploy/*.log for errors since the last run and post a short summary.", "enabled": true, "projectId": "project-app", "boundThreadId": null, "target": {"providerInstanceId": "claudeAgent", "model": "claude-haiku-5-5"}, "schedule": {"type": "fixed_time", "timeOfDay": "02:00"}, "nextRunAt": "2026-11-05T02:00:00-08:00", "lastRunStatus": "succeeded"}}}, "prompt": "Every night at 2am, read logs/deploy/*.log for new errors and summarize them in a fresh thread each run. Run it on Haiku 5.5, it's a cheap job.", "expect": {"calls": [{"tool": "schedule_task", "args": {"target.model": "claude-haiku-5-5", "bindToCurrentThread": false}}], "notCalls": ["delegate_task"]}, "notes": "Source: Mezzle/t3code feat/schedule-task-mcp-target: schedule_task had no model input, so agents could not pick or change a scheduled task's model, effort or Fast tier."} +{"id": "scheduled-task-changes-model", "area": "orchestration", "kind": "bad", "pattern": "existing scheduled task cannot change model or Fast tier", "experience": "A user asked to move a scheduled task to another model on Fast. schedule_task updates kept the stored model.", "rule": "schedule_task `target`: Model and options, as in `delegate_task`.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Every night at 2am, read logs/deploy/*.log for new errors and summarize them in a fresh thread each run."}, {"role": "assistant", "text": "Scheduled \"Nightly deploy log check\" for 2:00 AM every night, in a new thread each run. Next run: tonight at 2:00 AM.", "toolCalls": [{"tool": "schedule_task", "input": {"prompt": "Read logs/deploy/*.log for errors since the last run and post a short summary.", "schedule": {"type": "fixed_time", "timeOfDay": "02:00"}, "title": "Nightly deploy log check", "bindToCurrentThread": false}, "result": {"scheduledTaskId": "scheduled-task-1", "title": "Nightly deploy log check", "prompt": "Read logs/deploy/*.log for errors since the last run and post a short summary.", "enabled": true, "projectId": "project-app", "boundThreadId": null, "target": {"providerInstanceId": "claudeAgent", "model": "claude-opus-5-5", "options": [{"id": "effort", "value": "high"}]}, "schedule": {"type": "fixed_time", "timeOfDay": "02:00"}, "nextRunAt": "2026-11-05T02:00:00-08:00", "lastRunStatus": "succeeded"}}]}], "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": []}]}]}, "schedule_task": {"scheduledTaskId": "scheduled-task-1", "title": "Nightly deploy log check", "prompt": "Read logs/deploy/*.log for errors since the last run and post a short summary.", "enabled": true, "projectId": "project-app", "boundThreadId": null, "target": {"providerInstanceId": "codex", "model": "gpt-6.1-sol", "options": [{"id": "serviceTier", "value": "priority"}]}, "schedule": {"type": "fixed_time", "timeOfDay": "02:00"}, "nextRunAt": "2026-11-05T02:00:00-08:00", "lastRunStatus": "succeeded"}}}, "prompt": "Switch that nightly task to GPT-6.1 Sol in Codex, with Fast on.", "expect": {"calls": [{"tool": "schedule_task", "args": {"scheduledTaskId": "scheduled-task-1", "target.model": "gpt-6.1-sol", "target.options": "~priority"}}], "notCalls": ["delegate_task", "delete_scheduled_task"]}, "notes": "Source: Mezzle/t3code feat/schedule-task-mcp-target: schedule_task had no model input, so agents could not pick or change a scheduled task's model, effort or Fast tier."} +{"id": "scheduled-task-keeps-model", "area": "orchestration", "kind": "control", "pattern": "schedule-only update", "experience": "Control for scheduled-task-changes-model: changing only the time must not touch the task's model.", "rule": "schedule_task: only given fields change.", "privacy": "commit-safe", "setup": {"role": "top", "priorTurns": [{"role": "user", "text": "Every night at 2am, read logs/deploy/*.log for new errors and summarize them in a fresh thread each run."}, {"role": "assistant", "text": "Scheduled \"Nightly deploy log check\" for 2:00 AM every night, in a new thread each run. Next run: tonight at 2:00 AM.", "toolCalls": [{"tool": "schedule_task", "input": {"prompt": "Read logs/deploy/*.log for errors since the last run and post a short summary.", "schedule": {"type": "fixed_time", "timeOfDay": "02:00"}, "title": "Nightly deploy log check", "bindToCurrentThread": false}, "result": {"scheduledTaskId": "scheduled-task-1", "title": "Nightly deploy log check", "prompt": "Read logs/deploy/*.log for errors since the last run and post a short summary.", "enabled": true, "projectId": "project-app", "boundThreadId": null, "target": {"providerInstanceId": "claudeAgent", "model": "claude-opus-5-5", "options": [{"id": "effort", "value": "high"}]}, "schedule": {"type": "fixed_time", "timeOfDay": "02:00"}, "nextRunAt": "2026-11-05T02:00:00-08:00", "lastRunStatus": "succeeded"}}]}], "mocks": {"schedule_task": {"scheduledTaskId": "scheduled-task-1", "title": "Nightly deploy log check", "prompt": "Read logs/deploy/*.log for errors since the last run and post a short summary.", "enabled": true, "projectId": "project-app", "boundThreadId": null, "target": {"providerInstanceId": "claudeAgent", "model": "claude-opus-5-5", "options": [{"id": "effort", "value": "high"}]}, "schedule": {"type": "fixed_time", "timeOfDay": "03:00"}, "nextRunAt": "2026-11-05T02:00:00-08:00", "lastRunStatus": "succeeded"}}}, "prompt": "Move that nightly task to 3am.", "expect": {"calls": [{"tool": "schedule_task", "args": {"scheduledTaskId": "scheduled-task-1", "schedule.timeOfDay": "03:00"}}], "callsMustNot": [{"tool": "schedule_task", "args": {"target.model": "~-"}}], "notCalls": ["delete_scheduled_task"]}, "notes": "Source: Mezzle/t3code feat/schedule-task-mcp-target: schedule_task had no model input, so agents could not pick or change a scheduled task's model, effort or Fast tier."} {"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."} diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts index d1bb9961a0c68..3206c9f4d9c50 100644 --- a/packages/contracts/src/orchestratorMcp.ts +++ b/packages/contracts/src/orchestratorMcp.ts @@ -1,6 +1,7 @@ import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import * as SchemaTransformation from "effect/SchemaTransformation"; +import * as Struct from "effect/Struct"; import { AuthMcpClientAccess } from "./auth.ts"; import { @@ -127,6 +128,12 @@ export const OrchestratorMcpTarget = Schema.Struct({ }); export type OrchestratorMcpTarget = typeof OrchestratorMcpTarget.Type; +/** A task runs in its own project's environment, which `schedule_task` names at the top level. */ +export const OrchestratorMcpScheduledTaskTarget = OrchestratorMcpTarget.mapFields( + Struct.omit(["environmentId", "projectId"]), +); +export type OrchestratorMcpScheduledTaskTarget = typeof OrchestratorMcpScheduledTaskTarget.Type; + export const OrchestratorMcpRuntimeMode = Schema.Union([Schema.Literal("inherit"), RuntimeMode]); export type OrchestratorMcpRuntimeMode = typeof OrchestratorMcpRuntimeMode.Type; @@ -676,6 +683,11 @@ export const OrchestratorMcpScheduleTaskInput = Schema.Struct({ description: "Default true: runs post in this thread. False: new thread per run.", }), ), + target: Schema.optional( + OrchestratorMcpScheduledTaskTarget.annotate({ + description: "As in `delegate_task`. Unbound tasks only.", + }), + ), freshSession: Schema.optional( Schema.Boolean.annotate({ description: @@ -703,6 +715,13 @@ export const OrchestratorMcpScheduledTask = Schema.Struct({ enabled: Schema.Boolean, projectId: ProjectId, boundThreadId: Schema.NullOr(ThreadId), + target: Schema.optional( + Schema.Struct({ + providerInstanceId: ProviderInstanceId, + model: Schema.String, + options: Schema.optional(Schema.Array(ProviderOptionSelection)), + }), + ).annotate({ description: "Absent for a bound task, which runs on its thread's model." }), freshSession: Schema.Boolean, schedule: ScheduledTaskSchedule, nextRunAt: Schema.NullOr(IsoDateTime), @@ -746,6 +765,7 @@ export const OrchestratorMcpUpdateScheduledTaskInput = Schema.Struct({ schedule: Schema.optional(OrchestratorMcpSchedule), enabled: Schema.optional(Schema.Boolean), bindToCurrentThread: Schema.optional(Schema.Boolean), + target: Schema.optional(OrchestratorMcpScheduledTaskTarget), freshSession: Schema.optional(Schema.Boolean), }); export type OrchestratorMcpUpdateScheduledTaskInput =