| @@ -19,6 +19,7 @@ import { · 18 unmodified lines |
| 19 | OrchestratorMcpThreadReadResult, | 19 | OrchestratorMcpThreadReadResult, |
| 20 | OrchestratorMcpThreadSendResult, | 20 | OrchestratorMcpThreadSendResult, |
| 21 | OrchestratorMcpThreadWaitResult, | 21 | OrchestratorMcpThreadWaitResult, |
| | 22 | type Project, |
| 22 | ProjectId, | 23 | ProjectId, |
| 23 | ProviderDriverKind, | 24 | ProviderDriverKind, |
| 24 | ProviderInstanceId, | 25 | ProviderInstanceId, |
| @@ -37,6 +38,7 @@ import * as DateTime from "effect/DateTime"; · 12 unmodified lines |
| 37 | import * as Deferred from "effect/Deferred"; | 38 | import * as Deferred from "effect/Deferred"; |
| 38 | import * as Effect from "effect/Effect"; | 39 | import * as Effect from "effect/Effect"; |
| 39 | import * as FileSystem from "effect/FileSystem"; | 40 | import * as FileSystem from "effect/FileSystem"; |
| | 41 | import * as Fiber from "effect/Fiber"; |
| 40 | import * as Layer from "effect/Layer"; | 42 | import * as Layer from "effect/Layer"; |
| 41 | import * as Option from "effect/Option"; | 43 | import * as Option from "effect/Option"; |
| 42 | import * as PubSub from "effect/PubSub"; | 44 | import * as PubSub from "effect/PubSub"; |
| @@ -48,8 +50,9 @@ import { McpSchema, McpServer } from "effect/unstable/ai"; · 5 unmodified lines |
| 48 | import { ClaudeProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/ClaudeAdapterV2.ts"; | 50 | import { ClaudeProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/ClaudeAdapterV2.ts"; |
| 49 | import { CodexProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/CodexAdapterV2.ts"; | 51 | import { CodexProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/CodexAdapterV2.ts"; |
| 50 | import { CodexOrchestratorReplayHarness } from "../orchestration-v2/Adapters/CodexAdapterV2.testkit.ts"; | 52 | import { CodexOrchestratorReplayHarness } from "../orchestration-v2/Adapters/CodexAdapterV2.testkit.ts"; |
| | 53 | import * as ClientCommandDispatch from "../orchestration-v2/ClientCommandDispatch.ts"; |
| 51 | import { OrchestratorV2, type OrchestratorV2Shape } from "../orchestration-v2/Orchestrator.ts"; | 54 | import { OrchestratorV2, type OrchestratorV2Shape } from "../orchestration-v2/Orchestrator.ts"; |
| 52 | import { layer as threadManagementServiceLayer } from "../orchestration-v2/ThreadManagementService.ts"; | 55 | import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts"; |
| 53 | import { | 56 | import { |
| 54 | type ProviderAdapterV2Event, | 57 | type ProviderAdapterV2Event, |
| 55 | ProviderAdapterProtocolError, | 58 | ProviderAdapterProtocolError, |
| @@ -71,13 +74,18 @@ import { · 15 unmodified lines |
| 71 | materializeReplayTranscriptWorkspace, | 74 | materializeReplayTranscriptWorkspace, |
| 72 | } from "../orchestration-v2/testkit/ReplayTranscriptNdjson.ts"; | 75 | } from "../orchestration-v2/testkit/ReplayTranscriptNdjson.ts"; |
| 73 | import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts"; | 76 | import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts"; |
| | 77 | import * as ProjectService from "../project/ProjectService.ts"; |
| 74 | import { ScheduledTaskService } from "../scheduledTasks/ScheduledTaskService.ts"; | 78 | import { ScheduledTaskService } from "../scheduledTasks/ScheduledTaskService.ts"; |
| | 79 | import * as ServerSettings from "../serverSettings.ts"; |
| | 80 | import * as SourceControlRepositoryService from "../sourceControl/SourceControlRepositoryService.ts"; |
| 75 | import * as McpHttpServer from "./McpHttpServer.ts"; | 81 | import * as McpHttpServer from "./McpHttpServer.ts"; |
| 76 | import * as McpInvocationContext from "./McpInvocationContext.ts"; | 82 | import * as McpInvocationContext from "./McpInvocationContext.ts"; |
| 77 | import { delegatedTaskRun, hasPendingChildRuns } from "./OrchestratorMcpService.ts"; | 83 | import { delegatedTaskRun, hasPendingChildRuns } from "./OrchestratorMcpService.ts"; |
| 78 | | 84 | |
| 79 | const parentThreadId = ThreadId.make("thread:mcp-orchestrator-parent"); | 85 | const parentThreadId = ThreadId.make("thread:mcp-orchestrator-parent"); |
| 80 | const projectId = ProjectId.make("project:mcp-orchestrator"); | 86 | const projectId = ProjectId.make("project:mcp-orchestrator"); |
| | 87 | const deletionRaceProjectId = ProjectId.make("project:mcp-orchestrator-delete-race"); |
| | 88 | const deletionRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-delete-race-parent"); |
| 81 | const codexInstanceId = ProviderInstanceId.make("codex"); | 89 | const codexInstanceId = ProviderInstanceId.make("codex"); |
| 82 | const claudeInstanceId = ProviderInstanceId.make("claudeAgent"); | 90 | const claudeInstanceId = ProviderInstanceId.make("claudeAgent"); |
| 83 | const codexModel = "gpt-5.4"; | 91 | const codexModel = "gpt-5.4"; |
| @@ -472,6 +480,28 @@ describe("orchestrator MCP toolkit", () => { · 388 unmodified lines |
| 472 | Effect.scoped( | 480 | Effect.scoped( |
| 473 | Effect.gen(function* () { | 481 | Effect.gen(function* () { |
| 474 | const cwd = yield* checkpointWorkspace("orchestrator-mcp-toolkit"); | 482 | const cwd = yield* checkpointWorkspace("orchestrator-mcp-toolkit"); |
| | 483 | const deletionRaceProject = { |
| | 484 | id: deletionRaceProjectId, |
| | 485 | title: "MCP deletion race project", |
| | 486 | workspaceRoot: cwd, |
| | 487 | repositoryIdentity: null, |
| | 488 | faviconPath: null, |
| | 489 | defaultModelSelection: codexSelection, |
| | 490 | defaultThreadEnvMode: "local", |
| | 491 | scripts: [], |
| | 492 | createdAt: "2026-08-30T00:00:00.000Z", |
| | 493 | updatedAt: "2026-08-30T00:00:00.000Z", |
| | 494 | deletedAt: null, |
| | 495 | } satisfies Project; |
| | 496 | const deletionRaceProjectState = yield* Ref.make<Project>(deletionRaceProject); |
| | 497 | const creationAdmissionEntered = yield* Deferred.make<void>(); |
| | 498 | const allowCreationAdmission = yield* Deferred.make<void>(); |
| | 499 | const deletionLockAttempted = yield* Deferred.make<void>(); |
| | 500 | const deletionLockAcquired = yield* Deferred.make<void>(); |
| | 501 | const allowDeletion = yield* Deferred.make<void>(); |
| | 502 | const deletionRaceCommandId = CommandId.make( |
| | 503 | "command:mcp:mcp-provider-session-delete-race:create-thread:delete-create-race:0", |
| | 504 | ); |
| 475 | const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]); | 505 | const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]); |
| 476 | const parentTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>(); | 506 | const parentTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>(); |
| 477 | const deliveryTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>(); | 507 | const deliveryTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>(); |
| @@ -482,7 +512,9 @@ describe("orchestrator MCP toolkit", () => { · 4 unmodified lines |
| 482 | capabilities: CodexProviderCapabilitiesV2, | 512 | capabilities: CodexProviderCapabilitiesV2, |
| 483 | capturedTurns, | 513 | capturedTurns, |
| 484 | shouldComplete: (turn) => | 514 | shouldComplete: (turn) => |
| 485 | turn.threadId !== parentThreadId && turn.message.text !== cancellationPrompt, | 515 | turn.threadId !== parentThreadId && |
| | 516 | turn.threadId !== deletionRaceParentThreadId && |
| | 517 | turn.message.text !== cancellationPrompt, |
| 486 | terminalGate: (turn) => | 518 | terminalGate: (turn) => |
| 487 | turn.message.text.startsWith("Delegated task") || | 519 | turn.message.text.startsWith("Delegated task") || |
| 488 | turn.message.text.startsWith("Delegated tasks") | 520 | turn.message.text.startsWith("Delegated tasks") |
| @@ -554,10 +586,42 @@ describe("orchestrator MCP toolkit", () => { · 65 unmodified lines |
| 554 | }, | 586 | }, |
| 555 | registryLayer, | 587 | registryLayer, |
| 556 | ).pipe(Layer.provide(continuationProbeLayer)); | 588 | ).pipe(Layer.provide(continuationProbeLayer)); |
| 557 | const orchestrationLayer = Layer.merge( | 589 | const threadManagementLayer = ThreadManagement.layer.pipe( |
| 558 | orchestratorLayer, | 590 | Layer.provide(orchestratorLayer), |
| 559 | threadManagementServiceLayer.pipe(Layer.provide(orchestratorLayer)), | | |
| 560 | ); | 591 | ); |
| | 592 | const gatedThreadManagementLayer = Layer.effect( |
| | 593 | ThreadManagement.ThreadManagementService, |
| | 594 | Effect.gen(function* () { |
| | 595 | const actual = yield* ThreadManagement.ThreadManagementService; |
| | 596 | return ThreadManagement.ThreadManagementService.of({ |
| | 597 | ...actual, |
| | 598 | withProjectCreationAdmission: (input, effect) => |
| | 599 | actual.withProjectCreationAdmission(input, (receipt) => |
| | 600 | input.commandId === deletionRaceCommandId && Option.isNone(receipt) |
| | 601 | ? Deferred.succeed(creationAdmissionEntered, undefined).pipe( |
| | 602 | Effect.andThen(Deferred.await(allowCreationAdmission)), |
| | 603 | Effect.andThen(effect(receipt)), |
| | 604 | ) |
| | 605 | : effect(receipt), |
| | 606 | ), |
| | 607 | withProjectMutationLock: (lockedProjectId, effect) => |
| | 608 | lockedProjectId === deletionRaceProjectId |
| | 609 | ? Deferred.succeed(deletionLockAttempted, undefined).pipe( |
| | 610 | Effect.andThen( |
| | 611 | actual.withProjectMutationLock( |
| | 612 | lockedProjectId, |
| | 613 | Deferred.succeed(deletionLockAcquired, undefined).pipe( |
| | 614 | Effect.andThen(Deferred.await(allowDeletion)), |
| | 615 | Effect.andThen(effect), |
| | 616 | ), |
| | 617 | ), |
| | 618 | ), |
| | 619 | ) |
| | 620 | : actual.withProjectMutationLock(lockedProjectId, effect), |
| | 621 | }); |
| | 622 | }), |
| | 623 | ).pipe(Layer.provide(threadManagementLayer)); |
| | 624 | const orchestrationLayer = Layer.merge(orchestratorLayer, gatedThreadManagementLayer); |
| 561 | const providerRegistryLayer = makeProviderRegistryLayer([ | 625 | const providerRegistryLayer = makeProviderRegistryLayer([ |
| 562 | makeProviderSnapshot({ | 626 | makeProviderSnapshot({ |
| 563 | instanceId: codexInstanceId, | 627 | instanceId: codexInstanceId, |
| @@ -613,11 +677,40 @@ describe("orchestrator MCP toolkit", () => { · 49 unmodified lines |
| 613 | runNow: () => Effect.die("ScheduledTaskService.runNow is unused in this test"), | 677 | runNow: () => Effect.die("ScheduledTaskService.runNow is unused in this test"), |
| 614 | }), | 678 | }), |
| 615 | ); | 679 | ); |
| 616 | const testLayer = McpHttpServer.OrchestratorToolkitRegistrationLive.pipe( | 680 | const projectLayer = Layer.mock(ProjectService.ProjectService)({ |
| | 681 | getById: (requestedProjectId, options) => |
| | 682 | requestedProjectId === deletionRaceProjectId |
| | 683 | ? Ref.get(deletionRaceProjectState).pipe( |
| | 684 | Effect.map((project) => |
| | 685 | project.deletedAt === null || options?.includeDeleted === true |
| | 686 | ? Option.some(project) |
| | 687 | : Option.none(), |
| | 688 | ), |
| | 689 | ) |
| | 690 | : Effect.succeed(Option.none()), |
| | 691 | delete: ({ projectId: deletedProjectId }) => |
| | 692 | deletedProjectId === deletionRaceProjectId |
| | 693 | ? Ref.updateAndGet(deletionRaceProjectState, (project) => ({ |
| | 694 | ...project, |
| | 695 | deletedAt: "2026-08-30T00:01:00.000Z", |
| | 696 | updatedAt: "2026-08-30T00:01:00.000Z", |
| | 697 | })) |
| | 698 | : Effect.die(`Unexpected project deletion: ${deletedProjectId}`), |
| | 699 | }); |
| | 700 | const toolkitRegistrationLayer = Layer.merge( |
| | 701 | McpHttpServer.OrchestratorToolkitRegistrationLive, |
| | 702 | McpHttpServer.ProjectToolkitRegistrationLive, |
| | 703 | ); |
| | 704 | const testLayer = toolkitRegistrationLayer.pipe( |
| 617 | Layer.provideMerge(McpServer.McpServer.layer), | 705 | Layer.provideMerge(McpServer.McpServer.layer), |
| 618 | Layer.provideMerge(orchestrationLayer), | 706 | Layer.provideMerge(orchestrationLayer), |
| 619 | Layer.provide(providerRegistryLayer), | 707 | Layer.provide(providerRegistryLayer), |
| 620 | Layer.provide(scheduledTaskStubLayer), | 708 | Layer.provide(scheduledTaskStubLayer), |
| | 709 | Layer.provideMerge(projectLayer), |
| | 710 | Layer.provide( |
| | 711 | Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({}), |
| | 712 | ), |
| | 713 | Layer.provide(ServerSettings.layerTest({})), |
| 621 | Layer.provide(NodeServices.layer), | 714 | Layer.provide(NodeServices.layer), |
| 622 | ); | 715 | ); |
| 623 | | 716 | |
| @@ -2876,6 +2969,144 @@ describe("orchestrator MCP toolkit", () => { · 2252 unmodified lines |
| 2876 | removedDelivery.subagents.find((task) => task.id === removeTask.id), | 2969 | removedDelivery.subagents.find((task) => task.id === removeTask.id), |
| 2877 | ).toMatchObject({ result: expect.any(String), status: "completed" }); | 2970 | ).toMatchObject({ result: expect.any(String), status: "completed" }); |
| 2878 | yield* expectOffersToStay(0); | 2971 | yield* expectOffersToStay(0); |
| | 2972 | |
| | 2973 | yield* orchestrator.dispatch({ |
| | 2974 | type: "thread.create", |
| | 2975 | createdBy: "user", |
| | 2976 | creationSource: "web", |
| | 2977 | commandId: CommandId.make("command:mcp-delete-race-parent:create"), |
| | 2978 | threadId: deletionRaceParentThreadId, |
| | 2979 | projectId: deletionRaceProjectId, |
| | 2980 | title: "MCP deletion race parent", |
| | 2981 | modelSelection: codexSelection, |
| | 2982 | runtimeMode: "full-access", |
| | 2983 | interactionMode: "default", |
| | 2984 | branch: null, |
| | 2985 | worktreePath: cwd, |
| | 2986 | }); |
| | 2987 | yield* orchestrator.dispatch({ |
| | 2988 | type: "message.dispatch", |
| | 2989 | createdBy: "user", |
| | 2990 | creationSource: "web", |
| | 2991 | commandId: CommandId.make("command:mcp-delete-race-parent:start"), |
| | 2992 | threadId: deletionRaceParentThreadId, |
| | 2993 | messageId: MessageId.make("message:mcp-delete-race-parent:start"), |
| | 2994 | text: "Stay active while project deletion races thread creation.", |
| | 2995 | attachments: [], |
| | 2996 | modelSelection: codexSelection, |
| | 2997 | dispatchMode: { type: "start_immediately" }, |
| | 2998 | }); |
| | 2999 | yield* waitForProjection(orchestrator, deletionRaceParentThreadId, (projection) => |
| | 3000 | projection.runs.some((run) => run.status === "running"), |
| | 3001 | ); |
| | 3002 | |
| | 3003 | const deletionRaceInvocation: McpInvocationContext.McpInvocationScope = { |
| | 3004 | ...invocation, |
| | 3005 | threadId: deletionRaceParentThreadId, |
| | 3006 | providerSessionId: "mcp-provider-session-delete-race", |
| | 3007 | }; |
| | 3008 | const invokeDeletionRace = (name: string, args: Record<string, unknown>) => |
| | 3009 | server |
| | 3010 | .callTool({ name, arguments: args }) |
| | 3011 | .pipe( |
| | 3012 | Effect.provideService( |
| | 3013 | McpInvocationContext.McpInvocationContext, |
| | 3014 | deletionRaceInvocation, |
| | 3015 | ), |
| | 3016 | Effect.provideService(McpSchema.McpServerClient, client), |
| | 3017 | ); |
| | 3018 | const creation = yield* Effect.forkChild( |
| | 3019 | invokeDeletionRace("create_threads", { |
| | 3020 | clientRequestId: "delete-create-race", |
| | 3021 | threads: [{ title: "Racing child" }], |
| | 3022 | }), |
| | 3023 | { startImmediately: true }, |
| | 3024 | ); |
| | 3025 | yield* Deferred.await(creationAdmissionEntered); |
| | 3026 | const deletion = yield* Effect.forkChild( |
| | 3027 | invokeDeletionRace("t3_project_delete", { |
| | 3028 | projectId: deletionRaceProjectId, |
| | 3029 | cascadeThreads: true, |
| | 3030 | clientRequestId: "delete-create-race-project", |
| | 3031 | }), |
| | 3032 | { startImmediately: true }, |
| | 3033 | ); |
| | 3034 | yield* Deferred.await(deletionLockAttempted); |
| | 3035 | expect(yield* Deferred.isDone(deletionLockAcquired)).toBe(false); |
| | 3036 | |
| | 3037 | yield* Deferred.succeed(allowCreationAdmission, undefined); |
| | 3038 | yield* Deferred.await(deletionLockAcquired); |
| | 3039 | const creationCall = yield* Fiber.join(creation); |
| | 3040 | expect(creationCall.isError).toBe(false); |
| | 3041 | const creationResult = yield* decodeCreateThreadsResult( |
| | 3042 | creationCall.structuredContent, |
| | 3043 | ).pipe(Effect.orDie); |
| | 3044 | const createdThread = creationResult.threads[0]; |
| | 3045 | if (createdThread === undefined) { |
| | 3046 | return yield* Effect.die(new Error("Racing MCP thread was not created.")); |
| | 3047 | } |
| | 3048 | expect( |
| | 3049 | (yield* orchestrator.getThreadProjection(createdThread.threadId)).thread.projectId, |
| | 3050 | ).toBe(deletionRaceProjectId); |
| | 3051 | |
| | 3052 | yield* Deferred.succeed(allowDeletion, undefined); |
| | 3053 | const deletionCall = yield* Fiber.join(deletion); |
| | 3054 | expect(deletionCall.isError).toBe(false); |
| | 3055 | expect(deletionCall.structuredContent).toMatchObject({ |
| | 3056 | projectId: deletionRaceProjectId, |
| | 3057 | deleted: true, |
| | 3058 | alreadyDeleted: false, |
| | 3059 | deletedThreadCount: 2, |
| | 3060 | workspaceFilesDeleted: false, |
| | 3061 | }); |
| | 3062 | expect( |
| | 3063 | (yield* orchestrator.getThreadProjection(createdThread.threadId)).thread.deletedAt, |
| | 3064 | ).not.toBeNull(); |
| | 3065 | expect( |
| | 3066 | (yield* orchestrator.getThreadProjection(deletionRaceParentThreadId)).thread |
| | 3067 | .deletedAt, |
| | 3068 | ).not.toBeNull(); |
| | 3069 | |
| | 3070 | const threads = yield* ThreadManagement.ThreadManagementService; |
| | 3071 | const dispatchClientCommand = yield* ClientCommandDispatch.make; |
| | 3072 | const acceptedCreateCommand = { |
| | 3073 | type: "thread.create", |
| | 3074 | createdBy: "agent", |
| | 3075 | creationSource: "mcp", |
| | 3076 | commandId: deletionRaceCommandId, |
| | 3077 | threadId: createdThread.threadId, |
| | 3078 | projectId: deletionRaceProjectId, |
| | 3079 | title: "Racing child", |
| | 3080 | modelSelection: codexSelection, |
| | 3081 | runtimeMode: "full-access", |
| | 3082 | interactionMode: "default", |
| | 3083 | branch: null, |
| | 3084 | worktreePath: cwd, |
| | 3085 | } as const; |
| | 3086 | const sequenceBeforeReplay = yield* threads.getThreadEventSequence( |
| | 3087 | createdThread.threadId, |
| | 3088 | ); |
| | 3089 | const replayed = yield* dispatchClientCommand(acceptedCreateCommand); |
| | 3090 | expect( |
| | 3091 | replayed.storedEvents.some((stored) => stored.event.type === "thread.created"), |
| | 3092 | ).toBe(true); |
| | 3093 | expect(yield* threads.getThreadEventSequence(createdThread.threadId)).toBe( |
| | 3094 | sequenceBeforeReplay, |
| | 3095 | ); |
| | 3096 | expect( |
| | 3097 | (yield* orchestrator.getThreadProjection(createdThread.threadId)).thread.deletedAt, |
| | 3098 | ).not.toBeNull(); |
| | 3099 | |
| | 3100 | const freshThreadId = ThreadId.make("thread:mcp-delete-race:fresh-after-delete"); |
| | 3101 | const freshFailure = yield* dispatchClientCommand({ |
| | 3102 | ...acceptedCreateCommand, |
| | 3103 | commandId: CommandId.make("command:mcp-delete-race:fresh-after-delete"), |
| | 3104 | threadId: freshThreadId, |
| | 3105 | }).pipe(Effect.flip); |
| | 3106 | expect(freshFailure).toMatchObject({ _tag: "ProjectMutationError" }); |
| | 3107 | expect( |
| | 3108 | Option.isNone(yield* Effect.option(orchestrator.getThreadProjection(freshThreadId))), |
| | 3109 | ).toBe(true); |
| 2879 | }).pipe(Effect.provide(testLayer)); | 3110 | }).pipe(Effect.provide(testLayer)); |
| 2880 | }), | 3111 | }), |
| 2881 | ), | 3112 | ), |