From 75ab9f09d734e7b81542fd9e702ad00618a709dd Mon Sep 17 00:00:00 2001 From: Henry Chu Date: Wed, 28 Oct 2026 17:35:19 -0400 Subject: [PATCH 1/2] fix(server): launched threads step down to a runtime mode their provider offers Co-Authored-By: Claude Opus 5.5 --- .../src/mcp/toolkits/project/handlers.test.ts | 52 +++++++++++++++++++ .../src/mcp/toolkits/project/handlers.ts | 21 +++++++- apps/server/src/mcp/toolkits/project/tools.ts | 2 + apps/server/src/peer/PeerForwarding.test.ts | 4 +- apps/server/src/peer/RemoteDelegation.test.ts | 4 +- .../orchestrator-mcp-server.md | 4 +- 6 files changed, 83 insertions(+), 4 deletions(-) diff --git a/apps/server/src/mcp/toolkits/project/handlers.test.ts b/apps/server/src/mcp/toolkits/project/handlers.test.ts index 4031654a8c826..4133ab6b45a8c 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.test.ts @@ -7,6 +7,7 @@ import { type ModelSelection, type RuntimeMode, type OrchestrationV2ThreadShell, + type ServerProvider, } from "@t3tools/contracts"; import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; @@ -26,6 +27,7 @@ import * as ServerConfig from "../../../config.ts"; import * as ServerSettings from "../../../serverSettings.ts"; import * as Project from "../../../project/ProjectService.ts"; import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; +import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts"; import * as PeerForwarding from "../../../peer/PeerForwarding.ts"; import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; import * as VcsProcess from "../../../vcs/VcsProcess.ts"; @@ -42,6 +44,7 @@ const clientLaunchHarness = (input: { readonly launched: Array; readonly workspaceRoot?: string; readonly checkWorktreeBase?: ThreadLaunch.ThreadLaunchService["Service"]["checkWorktreeBase"]; + readonly providers?: ReadonlyArray; }) => { const projectId = ProjectId.make("project:client-target"); const modelSelection = { @@ -128,6 +131,9 @@ const clientLaunchHarness = (input: { }), Layer.mock(PeerForwarding.PeerForwarding)({}), Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ namedProjectsRoot: "/projects" }), + Layer.mock(ProviderRegistry.ProviderRegistry)({ + getProviders: Effect.succeed(input.providers ?? []), + }), DocumentText.layer.pipe(Layer.provideMerge(NodeServices.layer)), ).pipe( Layer.provideMerge(GitVcsDriver.layer), @@ -224,6 +230,52 @@ it.effect( }), ); +it.effect("a launch steps an inherited mode down to the provider's, and refuses one it lacks", () => + Effect.gen(function* () { + // Pi offers no Auto; a mode a provider lacks would run as Supervised. + const pi = ProviderInstanceId.make("pi"); + const launchPi = Effect.fn("test.launchPi")(function* (runtimeMode?: RuntimeMode) { + const launched: Array = []; + const { projectId, dependencies } = clientLaunchHarness({ + runtimeModeCeiling: "auto", + launched, + providers: [ + { + instanceId: pi, + supportedRuntimeModes: ["approval-required", "auto-accept-edits", "full-access"], + } as unknown as ServerProvider, + ], + }); + const toolkit = yield* ProjectToolkit.pipe( + Effect.provide( + McpToolAccess.HandlersLayer.layer(ProjectHandlers.layer).pipe( + Layer.provide(dependencies), + ), + ), + ); + const result = yield* toolkit + .handle("t3_thread_launch", { + projectId, + title: "Fix", + modelSelection: { instanceId: pi, model: "pi-model" }, + ...(runtimeMode === undefined ? {} : { runtimeMode }), + }) + .pipe(Stream.unwrap, Stream.runCollect, Effect.provide(dependencies)); + return { result: result.at(-1)?.result, launched }; + }); + + const inherited = yield* launchPi(); + expect(inherited.launched[0]?.runtimeMode).toBe("auto-accept-edits"); + + const requested = yield* launchPi("auto"); + expect(requested.result).toMatchObject({ code: "invalid_request" }); + expect(requested.launched).toHaveLength(0); + + const offered = yield* launchPi("approval-required"); + expect(offered.launched[0]?.runtimeMode).toBe("approval-required"); + }), +); + it.effect("a launch retried with the same clientRequestId replays the first one", () => Effect.gen(function* () { const launched: Array = []; diff --git a/apps/server/src/mcp/toolkits/project/handlers.ts b/apps/server/src/mcp/toolkits/project/handlers.ts index 950cb38c6c4fd..095d470d0f1de 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.ts @@ -15,6 +15,7 @@ import * as ThreadMessageIntake from "../../../orchestration-v2/ThreadMessageInt import * as Claims from "../../../orchestration-v2/AttachmentClaims.ts"; import * as Project from "../../../project/ProjectService.ts"; import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; +import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts"; import * as Repositories from "../../../sourceControl/SourceControlRepositoryService.ts"; import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; import * as ServerSettings from "../../../serverSettings.ts"; @@ -189,12 +190,30 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, { message: "Pass modelSelection: the project has no default model. orchestrator_capabilities lists providers and models.", }); - const startedRuntimeMode = + const defaultedRuntimeMode = projectDefaults !== undefined && input.runtimeMode === undefined ? yield* resolveRuntimeMode(runtimeMode, projectDefaults.defaultRuntimeMode).pipe( Effect.orElseSucceed(() => runtimeMode), ) : runtimeMode; + // As for delegate_task: the provider runs a mode it does not offer as + // Supervised, so a requested one is refused and an inherited one steps down. + const startedRuntimeMode = replaying + ? defaultedRuntimeMode + : yield* ProviderRegistry.ProviderRegistry.pipe( + Effect.flatMap((registry) => registry.getProviders), + Effect.map( + (providers) => + providers.find((provider) => provider.instanceId === modelSelection.instanceId) + ?.supportedRuntimeModes, + ), + Effect.flatMap((supportedRuntimeModes) => + resolveRuntimeMode(defaultedRuntimeMode, input.runtimeMode, { + modelSelection, + supportedRuntimeModes, + }), + ), + ); const result = yield* ThreadMessageIntake.launchThread({ commandId, threadId, diff --git a/apps/server/src/mcp/toolkits/project/tools.ts b/apps/server/src/mcp/toolkits/project/tools.ts index 38931596868b4..2021e2c83d983 100644 --- a/apps/server/src/mcp/toolkits/project/tools.ts +++ b/apps/server/src/mcp/toolkits/project/tools.ts @@ -30,6 +30,7 @@ import * as Schema from "effect/Schema"; import { Tool, Toolkit } from "effect/ai"; import * as ProjectService from "../../../project/ProjectService.ts"; import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; +import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts"; import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts"; import * as SourceControlRepositoryService from "../../../sourceControl/SourceControlRepositoryService.ts"; import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; @@ -167,6 +168,7 @@ const ThreadLaunchTool = Tool.make("t3_thread_launch", { ServerConfig.ServerConfig, ServerSettings.ServerSettingsService, DocumentText.DocumentText, + ProviderRegistry.ProviderRegistry, ], }) .annotate(Tool.Destructive, true) diff --git a/apps/server/src/peer/PeerForwarding.test.ts b/apps/server/src/peer/PeerForwarding.test.ts index 50be40682066b..b0812d3e42c3b 100644 --- a/apps/server/src/peer/PeerForwarding.test.ts +++ b/apps/server/src/peer/PeerForwarding.test.ts @@ -224,7 +224,9 @@ const serveBox = (seen: Ref.Ref, linkSession: Ref.Ref) => ), ), Layer.provide(NodeServices.layer), - Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})), + Layer.provide( + Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }), + ), Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})), Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})), Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})), diff --git a/apps/server/src/peer/RemoteDelegation.test.ts b/apps/server/src/peer/RemoteDelegation.test.ts index a344f63c523ab..cce7093992021 100644 --- a/apps/server/src/peer/RemoteDelegation.test.ts +++ b/apps/server/src/peer/RemoteDelegation.test.ts @@ -481,7 +481,9 @@ const boxToolkitLayer = (thread: BoxThread) => { Layer.provide(NodeServices.layer), ), ), - Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})), + Layer.provide( + Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }), + ), Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})), Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})), Layer.provide( diff --git a/docs/orchestration-v2/orchestrator-mcp-server.md b/docs/orchestration-v2/orchestrator-mcp-server.md index befaca4c077c8..115518d33adb3 100644 --- a/docs/orchestration-v2/orchestrator-mcp-server.md +++ b/docs/orchestration-v2/orchestrator-mcp-server.md @@ -364,7 +364,9 @@ Pass the task in `message`. Project, model, and modes inherit when omitted; workspace does not. `scratch: true` launches without a project, in a folder of its own under the environment's Scratch project. For stacked PRs, use the parent branch as `baseRef` with `startFromOrigin: false`. The new thread may not run with broader runtime or -interaction modes than the caller. Launch has no retry key, so inspect existing threads after a failed or lost response before +interaction modes than the caller. As for `delegate_task`, a runtime mode the +provider does not offer is refused, and an inherited or default one steps down +to the broadest mode it offers. Launch has no retry key, so inspect existing threads after a failed or lost response before launching again. `create_threads` remains the batch option for a shared checkout. ### `t3_thread_list` From 36d3f4ff20e00637018e3f24a6128fe654e2ffc2 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Fri, 30 Oct 2026 02:22:35 +0000 Subject: [PATCH 2/2] fix(server): simplify provider mode checks after merging main --- .../src/mcp/toolkits/project/handlers.test.ts | 18 ++++++----- .../src/mcp/toolkits/project/handlers.ts | 30 +++++++------------ apps/server/src/mcp/toolkits/project/tools.ts | 2 -- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/apps/server/src/mcp/toolkits/project/handlers.test.ts b/apps/server/src/mcp/toolkits/project/handlers.test.ts index 718f671b6853c..7617de634347a 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.test.ts @@ -10,7 +10,6 @@ import { type ModelSelection, type RuntimeMode, type OrchestrationV2ThreadShell, - type ServerProvider, } from "@t3tools/contracts"; import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodeServices from "@effect/platform-node/NodeServices"; @@ -30,7 +29,6 @@ import * as ServerConfig from "../../../config.ts"; import * as ServerSettings from "../../../serverSettings.ts"; import * as Project from "../../../project/ProjectService.ts"; import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; -import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts"; import * as PeerForwarding from "../../../peer/PeerForwarding.ts"; import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; import * as VcsProcess from "../../../vcs/VcsProcess.ts"; @@ -49,7 +47,6 @@ const clientLaunchHarness = (input: { readonly launched: Array; readonly workspaceRoot?: string; readonly checkWorktreeBase?: ThreadLaunch.ThreadLaunchService["Service"]["checkWorktreeBase"]; - readonly providers?: ReadonlyArray; readonly webOrigin?: string; }) => { const projectId = ProjectId.make("project:client-target"); @@ -142,9 +139,6 @@ const clientLaunchHarness = (input: { }), Layer.mock(PeerForwarding.PeerForwarding)({}), Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ namedProjectsRoot: "/projects" }), - Layer.mock(ProviderRegistry.ProviderRegistry)({ - getProviders: Effect.succeed(input.providers ?? []), - }), DocumentText.layer.pipe(Layer.provideMerge(NodeServices.layer)), ).pipe( Layer.provideMerge(GitVcsDriver.layer), @@ -261,8 +255,18 @@ it.effect("a launch steps an inherited mode down to the provider's, and refuses providers: [ { instanceId: pi, + driver: ProviderDriverKind.make("pi"), + enabled: true, + installed: true, + status: "ready", + version: null, + auth: { status: "authenticated" }, + checkedAt: "2026-01-01T00:00:00Z", + models: [], + slashCommands: [], + skills: [], supportedRuntimeModes: ["approval-required", "auto-accept-edits", "full-access"], - } as unknown as ServerProvider, + }, ], }); const toolkit = yield* ProjectToolkit.pipe( diff --git a/apps/server/src/mcp/toolkits/project/handlers.ts b/apps/server/src/mcp/toolkits/project/handlers.ts index d3d408671d046..e5826433501d2 100644 --- a/apps/server/src/mcp/toolkits/project/handlers.ts +++ b/apps/server/src/mcp/toolkits/project/handlers.ts @@ -15,7 +15,6 @@ import * as ThreadMessageIntake from "../../../orchestration-v2/ThreadMessageInt import * as Claims from "../../../orchestration-v2/AttachmentClaims.ts"; import * as Project from "../../../project/ProjectService.ts"; import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; -import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts"; import * as Repositories from "../../../sourceControl/SourceControlRepositoryService.ts"; import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; import * as ServerSettings from "../../../serverSettings.ts"; @@ -203,30 +202,21 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, { message: "Pass modelSelection: the project has no default model. orchestrator_capabilities lists providers and models.", }); - const defaultedRuntimeMode = + let startedRuntimeMode = projectDefaults !== undefined && input.runtimeMode === undefined ? yield* resolveRuntimeMode(runtimeMode, projectDefaults.defaultRuntimeMode).pipe( Effect.orElseSucceed(() => runtimeMode), ) : runtimeMode; - // As for delegate_task: the provider runs a mode it does not offer as - // Supervised, so a requested one is refused and an inherited one steps down. - const startedRuntimeMode = replaying - ? defaultedRuntimeMode - : yield* ProviderRegistry.ProviderRegistry.pipe( - Effect.flatMap((registry) => registry.getProviders), - Effect.map( - (providers) => - providers.find((provider) => provider.instanceId === modelSelection.instanceId) - ?.supportedRuntimeModes, - ), - Effect.flatMap((supportedRuntimeModes) => - resolveRuntimeMode(defaultedRuntimeMode, input.runtimeMode, { - modelSelection, - supportedRuntimeModes, - }), - ), - ); + if (!replaying) { + const providers = yield* (yield* ProviderRegistry.ProviderRegistry).getProviders; + startedRuntimeMode = yield* resolveRuntimeMode(startedRuntimeMode, input.runtimeMode, { + modelSelection, + supportedRuntimeModes: providers.find( + (provider) => provider.instanceId === modelSelection.instanceId, + )?.supportedRuntimeModes, + }); + } const result = yield* ThreadMessageIntake.launchThread({ commandId, threadId, diff --git a/apps/server/src/mcp/toolkits/project/tools.ts b/apps/server/src/mcp/toolkits/project/tools.ts index 62cacd602bf81..3ea5a8ea9e48f 100644 --- a/apps/server/src/mcp/toolkits/project/tools.ts +++ b/apps/server/src/mcp/toolkits/project/tools.ts @@ -31,7 +31,6 @@ import * as Schema from "effect/Schema"; import { Tool, Toolkit } from "effect/ai"; import * as ProjectService from "../../../project/ProjectService.ts"; import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; -import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts"; import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts"; import * as SourceControlRepositoryService from "../../../sourceControl/SourceControlRepositoryService.ts"; import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; @@ -170,7 +169,6 @@ const ThreadLaunchTool = Tool.make("t3_thread_launch", { ServerSettings.ServerSettingsService, ProviderRegistry.ProviderRegistry, DocumentText.DocumentText, - ProviderRegistry.ProviderRegistry, ], }) .annotate(Tool.Destructive, true)