Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import * as ServerSettings from "../serverSettings.ts";
import * as SecretRequests from "../secrets/SecretRequests.ts";
import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";
import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
import { OrchestratorDispatchError } from "../orchestration-v2/Orchestrator.ts";
import * as ThreadManagementService from "../orchestration-v2/ThreadManagementService.ts";
import type * as McpInvocationContext from "./McpInvocationContext.ts";
import * as OrchestratorMcpService from "./OrchestratorMcpService.ts";
Expand Down Expand Up @@ -603,3 +604,85 @@ it("readThread and sendToThread reach threads in other projects", async () => {
expect(stopped.code).toBe("parent_not_active");
}).pipe(Effect.provide(layer), Effect.runPromise);
});

it("sendToThread reports why the orchestrator refused the message", async () => {
const targetThreadId = ThreadId.make("thread-mcp-orchestrator-dead-session");
const targetProjection = {
thread: baseThread({
threadId: targetThreadId,
title: "Stalled",
instanceId: parentInstanceId,
model: "gpt-5.4",
}),
runs: [],
visibleTurnItems: [],
runtimeRequests: [],
messages: [],
contextTransfers: [],
subagents: [],
updatedAt: now,
} as unknown as OrchestrationV2ThreadProjection;

const layer = layerOrchestratorService.pipe(
Layer.provide(
Layer.mergeAll(
Layer.mock(ThreadManagementService.ThreadManagementService)({
getThreadShell: (threadId) =>
Effect.succeed(
threadId === targetThreadId
? (targetProjection.thread as unknown as OrchestrationV2ThreadShell)
: liveThreadShell(parentThreadId),
),
getThreadRecords: (threadId) =>
Effect.succeed(
threadId === targetThreadId
? targetProjection
: ({
...targetProjection,
thread: baseThread({
threadId: parentThreadId,
title: "Parent",
instanceId: parentInstanceId,
model: "gpt-5.4",
}),
runs: [
makeRun({ id: RunId.make("run-parent-live"), ordinal: 1, status: "running" }),
],
} as unknown as OrchestrationV2ThreadProjection),
),
getProjectThreadRecords: () => Effect.succeed(targetProjection),
sendToThread: (input) =>
Effect.fail(
new OrchestratorDispatchError({
commandId: input.commandId,
commandType: "message.dispatch",
cause: "Provider session provider-session-dead is not active.",
}),
),
} satisfies Partial<ThreadManagementService.ThreadManagementService["Service"]>),
ServerSettings.layerTest(),
Layer.mock(ProviderRegistry.ProviderRegistry)({
getProviders: Effect.succeed([]),
} satisfies Partial<ProviderRegistry.ProviderRegistry["Service"]>),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
list: () => Effect.succeed([]),
} satisfies Partial<ProviderAdapterRegistry.ProviderAdapterRegistryV2["Service"]>),
NodeCrypto.layer,
),
),
);

await Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const failure = yield* service
.sendToThread(makeScope(), { threadId: targetThreadId, message: "restart", mode: "restart" })
.pipe(Effect.flip);
expect(failure.code).toBe("orchestration_error");
expect(failure.message).toBe(
`Unable to send to thread ${targetThreadId}: Provider session provider-session-dead is not active.`,
);
}).pipe(Effect.provide(layer), Effect.runPromise);
});
3 changes: 2 additions & 1 deletion apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import * as ThreadManagementService from "../orchestration-v2/ThreadManagementSe
import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts";
import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
import { isSnoozed } from "../orchestration-v2/ThreadSettlementService.ts";
import { userFacingDispatchErrorMessage } from "../orchestration-v2/UserFacingErrors.ts";
import * as ProjectService from "../project/ProjectService.ts";
import * as ProviderRegistry from "../provider/ProviderRegistry.ts";
import * as ScheduledTaskService from "../scheduledTasks/ScheduledTaskService.ts";
Expand Down Expand Up @@ -2574,7 +2575,7 @@ const make = Effect.gen(function* () {
? threadManagementFailure(error)
: failure(
"orchestration_error",
`Unable to send to thread ${input.threadId}: ${errorMessage(error)}`,
`Unable to send to thread ${input.threadId}: ${userFacingDispatchErrorMessage(error) ?? errorMessage(error)}`,
),
),
);
Expand Down
Loading