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
101101 const deletionRaceProjectId = ProjectId.make("project:mcp-orchestrator-delete-race");
102102 const deletionRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-delete-race-parent");
103103 const ownershipRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-ownership-race-parent");
104+const lifecycleRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-lifecycle-race-parent");
104105 const targetProjectId = ProjectId.make("project:mcp-orchestrator-target");
105106 const codexInstanceId = ProviderInstanceId.make("codex");
106107 const claudeInstanceId = ProviderInstanceId.make("claudeAgent");
@@ -525,6 +526,7 @@ describe("orchestrator MCP toolkit", () => { · 418 unmodified lines
525526 const allowDeletion = yield* Deferred.make<void>();
526527 const deletionRaceProviderStarted = yield* Deferred.make<ProviderAdapterV2TurnInput>();
527528 const ownershipRaceProviderStarted = yield* Deferred.make<ProviderAdapterV2TurnInput>();
529+ const lifecycleRaceProviderStarted = yield* Deferred.make<ProviderAdapterV2TurnInput>();
528530 const deletionRaceCommandId = CommandId.make(
529531 "command:mcp:mcp-provider-session-delete-race:create-thread:delete-create-race:0",
530532 );
@@ -547,13 +549,16 @@ describe("orchestrator MCP toolkit", () => { · 16 unmodified lines
547549 turn.threadId !== parentThreadId &&
548550 turn.threadId !== deletionRaceParentThreadId &&
549551 turn.threadId !== ownershipRaceParentThreadId &&
552+ turn.threadId !== lifecycleRaceParentThreadId &&
550553 turn.message.text !== cancellationPrompt,
551554 startedSignal: (turn) =>
552555 turn.threadId === deletionRaceParentThreadId
553556 ? deletionRaceProviderStarted
554557 : turn.threadId === ownershipRaceParentThreadId
555558 ? ownershipRaceProviderStarted
556− : undefined,
559+ : turn.threadId === lifecycleRaceParentThreadId
560+ ? lifecycleRaceProviderStarted
561+ : undefined,
557562 terminalGate: (turn) =>
558563 turn.message.text.startsWith("Delegated task") ||
559564 turn.message.text.startsWith("Delegated tasks")
@@ -3736,6 +3741,90 @@ describe("orchestrator MCP toolkit", () => { · 3176 unmodified lines
37363741 ).toBe(true);
37373742 }).pipe(Effect.ensuring(Deferred.succeed(inactiveParentGate.release, undefined)));
37383743
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+
37393828 yield* orchestrator.dispatch({
37403829 type: "thread.create",
37413830 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
738738 ),
739739 );
740740 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.`;
741749 return yield* new OrchestratorDispatchError({
742750 commandId: input.command.commandId,
743751 commandType: input.command.type,
744− cause: `Caller thread ${ceiling.callerThreadId} is not active.`,
752+ cause,
745753 });
746754 }
747755 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