From cacc469a42201f6d288991bd1a3edf4065dfd864 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 18:42:03 +1000 Subject: [PATCH] fix(server): select an available provider for delegated tasks A driver-only delegation target could fail on an unavailable inherited provider instance even when another instance of that driver was healthy. Prefer the inherited instance only when it passes the availability checks, then fall back to a healthy peer. Explicit instance selection remains strict. Adapted from the proposal in pingdotgg/t3code#11154 onto the adapter-registry capability lookup. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/mcp/OrchestratorMcpService.test.ts | 189 +++++++++++++++++- apps/server/src/mcp/OrchestratorMcpService.ts | 7 +- .../orchestrator-mcp-server.md | 5 +- 3 files changed, 196 insertions(+), 5 deletions(-) diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts index 8fb3517135a3..aa5257078a37 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts @@ -396,7 +396,14 @@ describe("OrchestratorMcpService provider resolution", () => { }), ); - const parentProjection = (subagents: ReadonlyArray): OrchestrationV2ThreadProjection => + const parentProjection = ( + subagents: ReadonlyArray, + modelSelection: { + readonly instanceId: ProviderInstanceId; + readonly model: string; + readonly options?: ReadonlyArray<{ readonly id: string; readonly value: unknown }>; + } = { instanceId: codexInstanceId, model: "gpt-5.4" }, + ): OrchestrationV2ThreadProjection => ({ thread: { id: parentThreadId, @@ -404,7 +411,7 @@ describe("OrchestratorMcpService provider resolution", () => { title: "MCP parent", createdBy: "user", creationSource: "web", - modelSelection: { instanceId: codexInstanceId, model: "gpt-5.4" }, + modelSelection, runtimeMode: "full-access", interactionMode: "default", }, @@ -415,7 +422,7 @@ describe("OrchestratorMcpService provider resolution", () => { status: "running", rootNodeId: parentNodeId, providerInstanceId: codexInstanceId, - modelSelection: { instanceId: codexInstanceId, model: "gpt-5.4" }, + modelSelection, }, ], contextTransfers: [], @@ -784,4 +791,180 @@ describe("OrchestratorMcpService provider resolution", () => { }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies)))); }), ); + + it.effect( + "inherits an available parent instance for driver-only targets and otherwise selects a healthy peer", + () => + Effect.gen(function* () { + const codexAltInstanceId = ProviderInstanceId.make("codex-alt"); + const driver = ProviderDriverKind.make("codex"); + const parentModelSelection = { + instanceId: codexInstanceId, + model: "gpt-5.4", + options: [{ id: "reasoningEffort", value: "high" }], + } as const; + const task = { + id: taskId, + threadId: parentThreadId, + runId: parentRunId, + parentNodeId, + origin: "app_owned", + createdBy: "agent", + driver, + providerInstanceId: codexInstanceId, + providerThreadId: null, + childThreadId, + nativeTaskRef: null, + prompt: "Summarize the diff.", + title: null, + model: "gpt-5.4", + status: "running", + result: null, + startedAt: null, + completedAt: null, + }; + const cases = [ + { + name: "healthy-inherited", + inheritedEnabled: true, + peerEnabled: true, + explicit: false, + selectedInstanceId: codexInstanceId, + }, + { + name: "unavailable-inherited-falls-back-to-healthy-peer", + inheritedEnabled: false, + peerEnabled: true, + explicit: false, + selectedInstanceId: codexAltInstanceId, + }, + { + name: "no-available-peer", + inheritedEnabled: false, + peerEnabled: false, + explicit: false, + selectedInstanceId: null, + }, + { + name: "explicit-unavailable", + inheritedEnabled: false, + peerEnabled: true, + explicit: true, + selectedInstanceId: null, + }, + { + name: "explicit-healthy", + inheritedEnabled: true, + peerEnabled: true, + explicit: true, + selectedInstanceId: codexAltInstanceId, + }, + ] as const; + + for (const testCase of cases) { + const dispatched = yield* Ref.make>([]); + let delegated = false; + const dependencies = Layer.mergeAll( + NodeServices.layer, + Layer.mock(ThreadManagementService)({ + getThreadProjection: (threadId) => + Effect.succeed( + threadId === parentThreadId + ? parentProjection(delegated ? [task] : [], parentModelSelection) + : childProjection, + ), + dispatch: (command) => + Ref.update(dispatched, (commands) => [...commands, command]).pipe( + Effect.andThen( + Effect.sync(() => { + delegated = true; + }), + ), + Effect.as({ + sequence: 1, + storedEvents: [ + { + sequence: 1, + commandId: null, + event: { type: "subagent.updated", payload: task }, + }, + ], + } as never), + ), + }), + Layer.mock(ProviderRegistry)({ + getProviders: Effect.succeed([ + providerSnapshot({ + instanceId: codexInstanceId, + driver, + model: "gpt-5.4", + enabled: testCase.inheritedEnabled, + }), + providerSnapshot({ + instanceId: codexAltInstanceId, + driver, + model: "codex-alt-model", + enabled: testCase.peerEnabled, + }), + ]), + }), + adapterRegistryLayer([codexInstanceId, codexAltInstanceId]), + Layer.mock(ScheduledTaskService)({}), + ); + + yield* Effect.gen(function* () { + const service = yield* OrchestratorMcpService.OrchestratorMcpService; + const target = testCase.explicit + ? ({ + providerInstanceId: + testCase.selectedInstanceId === null + ? codexInstanceId + : testCase.selectedInstanceId, + } as const) + : ({ driverKind: driver } as const); + if (testCase.selectedInstanceId === null) { + const error = yield* service + .delegateTask(scope, { + task: "Summarize the diff.", + target, + mode: "async", + clientRequestId: `delegate-select-${testCase.name}`, + }) + .pipe(Effect.flip); + assert.equal(error.code, "provider_unavailable", testCase.name); + assert.deepEqual(yield* Ref.get(dispatched), [], testCase.name); + return; + } + const result = yield* service.delegateTask(scope, { + task: "Summarize the diff.", + target, + mode: "async", + clientRequestId: `delegate-select-${testCase.name}`, + }); + assert.equal(result.status, "running", testCase.name); + const commands = yield* Ref.get(dispatched); + assert.equal(commands.length, 1, testCase.name); + const request = commands[0] as { + type: string; + modelSelection: { + instanceId: string; + model: string; + options?: ReadonlyArray<{ id: string; value: unknown }>; + }; + }; + assert.equal(request.type, "delegated_task.request", testCase.name); + assert.equal( + request.modelSelection.instanceId, + testCase.selectedInstanceId, + testCase.name, + ); + if (testCase.name === "healthy-inherited") { + assert.deepEqual(request.modelSelection, parentModelSelection, testCase.name); + } else { + assert.equal(request.modelSelection.model, "codex-alt-model", testCase.name); + } + }).pipe(Effect.provide(OrchestratorMcpService.layer.pipe(Layer.provide(dependencies)))); + } + }), + ); }); diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index 5140640f615f..aacfb9609648 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -815,8 +815,13 @@ const make = Effect.gen(function* () { `No V2 provider adapter is registered for driver ${requestedDriver}.`, ); } + // Inherit the parent's instance only when it can actually serve the + // child; an unavailable parent yields to a healthy instance of the + // requested driver rather than failing the delegation. const inheritedCandidate = candidates.find( - (candidate) => candidate.instanceId === input.parent.thread.modelSelection.instanceId, + (candidate) => + candidate.instanceId === input.parent.thread.modelSelection.instanceId && + providerConstraints(candidate, true).length === 0, ); const availableCandidate = candidates.find( (candidate) => providerConstraints(candidate, true).length === 0, diff --git a/docs/orchestration-v2/orchestrator-mcp-server.md b/docs/orchestration-v2/orchestrator-mcp-server.md index 7ac66217ae7a..88459a94465b 100644 --- a/docs/orchestration-v2/orchestrator-mcp-server.md +++ b/docs/orchestration-v2/orchestrator-mcp-server.md @@ -222,7 +222,10 @@ type DelegateTaskInput = { ``` Provider, model, runtime mode, and interaction mode inherit from the parent -when omitted. Selecting a different provider without a model uses that +when omitted. A driver-only target inherits the parent's provider instance +when it can run child tasks, and otherwise selects an available instance of +that driver; an explicit `providerInstanceId` is honored exactly and fails +when unavailable. Selecting a different provider without a model uses that provider's first advertised model. Delegation requires an active parent run owned by the MCP credential's