History

fix(mcp): preserve inactive caller failure codes

Julius Marminge committed 2 months ago

Commit aa61af01 parent b08e3392 files · +99 −2Browse files

Unified · Split

2 files
  1. apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts +90 −1
  2. apps/server/src/orchestration-v2/Orchestrator.ts +9 −1
apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts+90−1
@@ -101,6 +101,7 @@ const projectId = ProjectId.make("project:mcp-orchestrator"); · 100 unmodified lines
101const deletionRaceProjectId = ProjectId.make("project:mcp-orchestrator-delete-race");101const deletionRaceProjectId = ProjectId.make("project:mcp-orchestrator-delete-race");
102const deletionRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-delete-race-parent");102const deletionRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-delete-race-parent");
103const ownershipRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-ownership-race-parent");103const ownershipRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-ownership-race-parent");
104const lifecycleRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-lifecycle-race-parent");
104const targetProjectId = ProjectId.make("project:mcp-orchestrator-target");105const targetProjectId = ProjectId.make("project:mcp-orchestrator-target");
105const codexInstanceId = ProviderInstanceId.make("codex");106const codexInstanceId = ProviderInstanceId.make("codex");
106const claudeInstanceId = ProviderInstanceId.make("claudeAgent");107const claudeInstanceId = ProviderInstanceId.make("claudeAgent");
@@ -525,6 +526,7 @@ describe("orchestrator MCP toolkit", () => { · 418 unmodified lines
525 const allowDeletion = yield* Deferred.make<void>();526 const allowDeletion = yield* Deferred.make<void>();
526 const deletionRaceProviderStarted = yield* Deferred.make<ProviderAdapterV2TurnInput>();527 const deletionRaceProviderStarted = yield* Deferred.make<ProviderAdapterV2TurnInput>();
527 const ownershipRaceProviderStarted = yield* Deferred.make<ProviderAdapterV2TurnInput>();528 const ownershipRaceProviderStarted = yield* Deferred.make<ProviderAdapterV2TurnInput>();
529 const lifecycleRaceProviderStarted = yield* Deferred.make<ProviderAdapterV2TurnInput>();
528 const deletionRaceCommandId = CommandId.make(530 const deletionRaceCommandId = CommandId.make(
529 "command:mcp:mcp-provider-session-delete-race:create-thread:delete-create-race:0",531 "command:mcp:mcp-provider-session-delete-race:create-thread:delete-create-race:0",
530 );532 );
@@ -547,13 +549,16 @@ describe("orchestrator MCP toolkit", () => { · 16 unmodified lines
547 turn.threadId !== parentThreadId &&549 turn.threadId !== parentThreadId &&
548 turn.threadId !== deletionRaceParentThreadId &&550 turn.threadId !== deletionRaceParentThreadId &&
549 turn.threadId !== ownershipRaceParentThreadId &&551 turn.threadId !== ownershipRaceParentThreadId &&
552 turn.threadId !== lifecycleRaceParentThreadId &&
550 turn.message.text !== cancellationPrompt,553 turn.message.text !== cancellationPrompt,
551 startedSignal: (turn) =>554 startedSignal: (turn) =>
552 turn.threadId === deletionRaceParentThreadId555 turn.threadId === deletionRaceParentThreadId
553 ? deletionRaceProviderStarted556 ? deletionRaceProviderStarted
554 : turn.threadId === ownershipRaceParentThreadId557 : turn.threadId === ownershipRaceParentThreadId
555 ? ownershipRaceProviderStarted558 ? ownershipRaceProviderStarted
556 : undefined,559 : turn.threadId === lifecycleRaceParentThreadId
560 ? lifecycleRaceProviderStarted
561 : undefined,
557 terminalGate: (turn) =>562 terminalGate: (turn) =>
558 turn.message.text.startsWith("Delegated task") ||563 turn.message.text.startsWith("Delegated task") ||
559 turn.message.text.startsWith("Delegated tasks")564 turn.message.text.startsWith("Delegated tasks")
@@ -3736,6 +3741,90 @@ describe("orchestrator MCP toolkit", () => { · 3176 unmodified lines
3736 ).toBe(true);3741 ).toBe(true);
3737 }).pipe(Effect.ensuring(Deferred.succeed(inactiveParentGate.release, undefined)));3742 }).pipe(Effect.ensuring(Deferred.succeed(inactiveParentGate.release, undefined)));
3738 3743
3744 yield* orchestrator.dispatch({
3745 type: "thread.create",
3746 createdBy: "user",
3747 creationSource: "web",
3748 commandId: CommandId.make("command:mcp-lifecycle-race-parent:create"),
3749 threadId: lifecycleRaceParentThreadId,
3750 projectId,
3751 title: "MCP lifecycle race parent",
3752 modelSelection: codexSelection,
3753 runtimeMode: "full-access",
3754 interactionMode: "default",
3755 branch: null,
3756 worktreePath: cwd,
3757 });
3758 yield* orchestrator.dispatch({
3759 type: "message.dispatch",
3760 createdBy: "user",
3761 creationSource: "web",
3762 commandId: CommandId.make("command:mcp-lifecycle-race-parent:start"),
3763 threadId: lifecycleRaceParentThreadId,
3764 messageId: MessageId.make("message:mcp-lifecycle-race-parent:start"),
3765 text: "Stay active until the caller lifecycle is rechecked.",
3766 attachments: [],
3767 modelSelection: codexSelection,
3768 dispatchMode: { type: "start_immediately" },
3769 });
3770 const startedLifecycleRaceTurn = yield* Deferred.await(lifecycleRaceProviderStarted);
3771 expect(startedLifecycleRaceTurn.threadId).toBe(lifecycleRaceParentThreadId);
3772 const lifecycleRaceInvocation: McpInvocationContext.McpInvocationScope = {
3773 ...invocation,
3774 threadId: lifecycleRaceParentThreadId,
3775 providerSessionId: "mcp-provider-session-lifecycle-race",
3776 };
3777 const invokeLifecycleRace = (name: string, args: Record<string, unknown>) =>
3778 server
3779 .callTool({ name, arguments: args })
3780 .pipe(
3781 Effect.provideService(
3782 McpInvocationContext.McpInvocationContext,
3783 lifecycleRaceInvocation,
3784 ),
3785 Effect.provideService(McpSchema.McpServerClient, client),
3786 );
3787 const archivedParentKey = "create-after-parent-archived";
3788 const archivedParentGate = yield* gateLaunchReceipt(
3789 launchCommandId(archivedParentKey, lifecycleRaceInvocation.providerSessionId),
3790 );
3791 yield* Effect.gen(function* () {
3792 const creation = yield* Effect.forkChild(
3793 invokeLifecycleRace("create_threads", {
3794 clientRequestId: archivedParentKey,
3795 threads: [
3796 {
3797 projectId: targetProjectId,
3798 title: "Must not outlive its archived caller",
3799 prompt: "Do not launch after the caller is archived.",
3800 },
3801 ],
3802 }),
3803 { startImmediately: true },
3804 );
3805 yield* Deferred.await(archivedParentGate.entered);
3806 yield* orchestrator.dispatch({
3807 type: "thread.archive",
3808 commandId: CommandId.make("command:mcp-lifecycle-race-parent:archive"),
3809 threadId: lifecycleRaceParentThreadId,
3810 });
3811 yield* Deferred.succeed(archivedParentGate.release, undefined);
3812 const creationCall = yield* Fiber.join(creation);
3813 expect(creationCall.structuredContent).toMatchObject({
3814 code: "parent_not_active",
3815 message:
3816 "Thread creation requires an active run owned by this MCP provider session.",
3817 });
3818 const childThreadId = ThreadId.make(
3819 `thread:mcp:${lifecycleRaceInvocation.providerSessionId}:${archivedParentKey}:0`,
3820 );
3821 expect(
3822 Option.isNone(
3823 yield* Effect.option(orchestrator.getThreadProjection(childThreadId)),
3824 ),
3825 ).toBe(true);
3826 }).pipe(Effect.ensuring(Deferred.succeed(archivedParentGate.release, undefined)));
3827
3739 yield* orchestrator.dispatch({3828 yield* orchestrator.dispatch({
3740 type: "thread.create",3829 type: "thread.create",
3741 createdBy: "user",3830 createdBy: "user",
apps/server/src/orchestration-v2/Orchestrator.ts+9−1
@@ -738,10 +738,18 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio · 737 unmodified lines
738 ),738 ),
739 );739 );
740 if (caller.thread.deletedAt !== null || caller.thread.archivedAt !== null) {740 if (caller.thread.deletedAt !== null || caller.thread.archivedAt !== null) {
741 const cause =
742 ceiling.callerRunId !== undefined && ceiling.callerProviderInstanceId !== undefined
743 ? new OrchestratorCallerRunCeilingError({
744 callerThreadId: ceiling.callerThreadId,
745 callerRunId: ceiling.callerRunId,
746 callerProviderInstanceId: ceiling.callerProviderInstanceId,
747 })
748 : `Caller thread ${ceiling.callerThreadId} is not active.`;
741 return yield* new OrchestratorDispatchError({749 return yield* new OrchestratorDispatchError({
742 commandId: input.command.commandId,750 commandId: input.command.commandId,
743 commandType: input.command.type,751 commandType: input.command.type,
744 cause: `Caller thread ${ceiling.callerThreadId} is not active.`,752 cause,
745 });753 });
746 }754 }
747 if (ceiling.callerRunId !== undefined && ceiling.callerProviderInstanceId !== undefined) {755 if (ceiling.callerRunId !== undefined && ceiling.callerProviderInstanceId !== undefined) {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t