Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -10859,7 +10859,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
nativeThreadId: string,
) {
const queryPolicy = yield* queryPolicyForTurn(turnInput);
const mcpSession = yield* mcpSessions.read(turnInput.threadId);
const mcpSession = yield* mcpSessions.read(turnInput.threadId, adapterOptions.instanceId);
const mcpOverrides = claudeMcpQueryOverrides({
mcpSession,
readOnlySandbox:
Expand Down
8 changes: 4 additions & 4 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1725,7 +1725,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
) {
const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
const readMcpSession = (threadId: ThreadId | null) =>
threadId === null ? Effect.undefined : mcpSessions.read(threadId);
threadId === null ? Effect.undefined : mcpSessions.read(threadId, adapterOptions.instanceId);
const { clientFactory, digest, fileSystem, idAllocator, serverConfig } = adapterOptions;
const continuationRequests = adapterOptions.continuationRequests;

Expand Down Expand Up @@ -6991,7 +6991,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
codexInput: ReadonlyArray<CodexSchema.V2TurnStartParams__UserInput>,
) {
const threadId = yield* getNativeThreadId(turnInput.providerThread);
const mcpSession = yield* mcpSessions.read(turnInput.threadId);
const mcpSession = yield* readMcpSession(turnInput.threadId);
const turnStartParams = yield* buildCodexTurnStartParams({
nativeThreadId: threadId,
codexInput,
Expand Down Expand Up @@ -7303,7 +7303,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
}),
ensureThread: (threadInput) =>
ensureInitialized.pipe(
Effect.andThen(mcpSessions.read(threadInput.threadId)),
Effect.andThen(readMcpSession(threadInput.threadId)),
Effect.flatMap((mcpSession) =>
client.request(
"thread/start",
Expand Down Expand Up @@ -8165,7 +8165,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
...(boundary.lastTurnId === undefined ? {} : { lastTurnId: boundary.lastTurnId }),
...codexThreadRuntimeParams({
threadId: threadInput.targetThreadId,
mcpSession: yield* mcpSessions.read(threadInput.targetThreadId),
mcpSession: yield* readMcpSession(threadInput.targetThreadId),
...(threadInput.modelSelection === undefined
? {}
: { modelSelection: threadInput.modelSelection }),
Expand Down
195 changes: 151 additions & 44 deletions apps/server/src/orchestration-v2/ProviderSessionManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ function unimplemented(detail: string) {
const makeProviderAdapter = Effect.fnUntraced(function* (
state: Ref.Ref<TestProviderRuntimeState>,
options: {
readonly instanceId?: ProviderInstanceId;
readonly failEventStream?: boolean;
readonly capabilities?: OrchestrationV2ProviderCapabilities;
readonly extensionStatuses?: OrchestrationV2ProviderSession["extensionStatuses"];
Expand All @@ -443,6 +444,7 @@ const makeProviderAdapter = Effect.fnUntraced(function* (
readonly scopeCloseReached?: Deferred.Deferred<void>;
} = {},
) {
const instanceId = options.instanceId ?? modelSelection.instanceId;
const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
const countClose = Effect.addFinalizer(() =>
Ref.update(state, (current) => ({
Expand All @@ -458,7 +460,7 @@ const makeProviderAdapter = Effect.fnUntraced(function* (
).pipe(Effect.andThen(Effect.never)),
);
return ProviderAdapter.ProviderAdapterV2.of({
instanceId: ProviderInstanceId.make("codex"),
instanceId,
driver: CODEX_DRIVER,
getCapabilities: () => Effect.succeed(options.capabilities ?? CodexCapabilities),
planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }),
Expand All @@ -469,14 +471,14 @@ const makeProviderAdapter = Effect.fnUntraced(function* (
if (options.hangSessionScopeClose === true) yield* hangClose;
}
if (options.mcpConfigs !== undefined && options.spawnBeforeOpen === true) {
const mcpConfig = yield* mcpSessions.read(input.threadId);
const mcpConfig = yield* mcpSessions.read(input.threadId, instanceId);
yield* Ref.update(options.mcpConfigs, (configs) => [...configs, mcpConfig]);
}
if (options.beforeOpen !== undefined) {
yield* options.beforeOpen(input);
}
if (options.mcpConfigs !== undefined && options.spawnBeforeOpen !== true) {
const mcpConfig = yield* mcpSessions.read(input.threadId);
const mcpConfig = yield* mcpSessions.read(input.threadId, instanceId);
yield* Ref.update(options.mcpConfigs, (configs) => [...configs, mcpConfig]);
}
const now = yield* DateTime.now;
Expand Down Expand Up @@ -506,7 +508,7 @@ const makeProviderAdapter = Effect.fnUntraced(function* (
}

return {
instanceId: ProviderInstanceId.make("codex"),
instanceId,
driver: CODEX_DRIVER,
providerSessionId: input.providerSessionId,
get providerSession() {
Expand Down Expand Up @@ -605,6 +607,8 @@ function layerTest(input: {
readonly scopeCloseReached?: Deferred.Deferred<void>;
readonly serverSettingsLayer?: ReturnType<typeof ServerSettings.layerTest>;
readonly projectServiceLayer?: Layer.Layer<ProjectService.ProjectService>;
/** Registers a second fake adapter under this instance, sharing the same state. */
readonly secondInstanceId?: ProviderInstanceId;
}) {
const layerConfiguredEventSink =
input.flakyReleaseWrites !== undefined
Expand All @@ -616,35 +620,46 @@ function layerTest(input: {
: input.failReleaseEventWrites
? layerFailingReleaseEventSink
: layerTestEventSink;
const adapterOptions = {
failEventStream: input.failEventStream ?? false,
...(input.capabilities === undefined ? {} : { capabilities: input.capabilities }),
...(input.extensionStatuses === undefined
? {}
: { extensionStatuses: input.extensionStatuses }),
...(input.mcpConfigs === undefined ? {} : { mcpConfigs: input.mcpConfigs }),
...(input.beforeOpen === undefined ? {} : { beforeOpen: input.beforeOpen }),
...(input.hasPendingBackgroundWork === undefined
? {}
: { hasPendingBackgroundWork: input.hasPendingBackgroundWork }),
...(input.hasPendingBackgroundWorkForThread === undefined
? {}
: { hasPendingBackgroundWorkForThread: input.hasPendingBackgroundWorkForThread }),
...(input.backgroundTasksEndWithProcess === undefined
? {}
: { backgroundTasksEndWithProcess: input.backgroundTasksEndWithProcess }),
...(input.hangSessionScopeClose === undefined
? {}
: { hangSessionScopeClose: input.hangSessionScopeClose }),
...(input.startTurn === undefined ? {} : { startTurn: input.startTurn }),
...(input.interruptTurn === undefined ? {} : { interruptTurn: input.interruptTurn }),
...(input.beforeUnload === undefined ? {} : { beforeUnload: input.beforeUnload }),
...(input.spawnBeforeOpen === undefined ? {} : { spawnBeforeOpen: input.spawnBeforeOpen }),
...(input.scopeCloseReached === undefined
? {}
: { scopeCloseReached: input.scopeCloseReached }),
};
const layerRegistry = Layer.unwrap(
makeProviderAdapter(input.state, {
failEventStream: input.failEventStream ?? false,
...(input.capabilities === undefined ? {} : { capabilities: input.capabilities }),
...(input.extensionStatuses === undefined
? {}
: { extensionStatuses: input.extensionStatuses }),
...(input.mcpConfigs === undefined ? {} : { mcpConfigs: input.mcpConfigs }),
...(input.beforeOpen === undefined ? {} : { beforeOpen: input.beforeOpen }),
...(input.hasPendingBackgroundWork === undefined
? {}
: { hasPendingBackgroundWork: input.hasPendingBackgroundWork }),
...(input.hasPendingBackgroundWorkForThread === undefined
? {}
: { hasPendingBackgroundWorkForThread: input.hasPendingBackgroundWorkForThread }),
...(input.backgroundTasksEndWithProcess === undefined
? {}
: { backgroundTasksEndWithProcess: input.backgroundTasksEndWithProcess }),
...(input.hangSessionScopeClose === undefined
? {}
: { hangSessionScopeClose: input.hangSessionScopeClose }),
...(input.startTurn === undefined ? {} : { startTurn: input.startTurn }),
...(input.interruptTurn === undefined ? {} : { interruptTurn: input.interruptTurn }),
...(input.beforeUnload === undefined ? {} : { beforeUnload: input.beforeUnload }),
...(input.spawnBeforeOpen === undefined ? {} : { spawnBeforeOpen: input.spawnBeforeOpen }),
...(input.scopeCloseReached === undefined
? {}
: { scopeCloseReached: input.scopeCloseReached }),
}).pipe(Effect.map(ProviderAdapterRegistry.layerSingle)),
Effect.all([
makeProviderAdapter(input.state, adapterOptions),
...(input.secondInstanceId === undefined
? []
: [
makeProviderAdapter(input.state, {
...adapterOptions,
instanceId: input.secondInstanceId,
}),
]),
]).pipe(Effect.map(ProviderAdapterRegistry.layerFromAdapters)),
);
const layerConfiguredMcpRegistry =
input.pauseResolve === undefined
Expand Down Expand Up @@ -1235,7 +1250,7 @@ it.effect("ProviderSessionManagerV2 cleans up an open interrupted mid-handshake"

assert.equal((yield* Ref.get(state)).closeCount, 1);
assert.isUndefined(yield* registry.resolve(token!));
assert.isUndefined(yield* mcpSessions.read(threadId));
assert.isUndefined(yield* mcpSessions.read(threadId, modelSelection.instanceId));
assert.isTrue(Option.isNone(yield* manager.get(providerSessionId)));

yield* Ref.set(holdHandshake, false);
Expand Down Expand Up @@ -2069,7 +2084,7 @@ it.effect(
);

yield* manager.close(providerSessionId);
assert.isUndefined(yield* mcpSessions.read(threadId));
assert.isUndefined(yield* mcpSessions.read(threadId, modelSelection.instanceId));
assert.isUndefined(yield* registry.resolve(token!));
});

Expand Down Expand Up @@ -2255,7 +2270,7 @@ it.effect("ProviderSessionManagerV2 revokes MCP credentials when release persist

const closeError = yield* manager.close(providerSessionId).pipe(Effect.flip);
assert.equal(closeError._tag, "ProviderSessionCloseError");
assert.isUndefined(yield* mcpSessions.read(threadId));
assert.isUndefined(yield* mcpSessions.read(threadId, modelSelection.instanceId));
assert.isUndefined(yield* registry.resolve(token!));
});

Expand Down Expand Up @@ -2321,7 +2336,7 @@ it.effect(
yield* Fiber.interrupt(first);
yield* Fiber.join(second);

const config = yield* mcpSessions.read(threadId);
const config = yield* mcpSessions.read(threadId, modelSelection.instanceId);
assert.isDefined(config);
const token = config!.authorizationHeader.replace(/^Bearer\s+/, "");
assert.equal((yield* registry.resolve(token))?.thread.threadId, threadId);
Expand Down Expand Up @@ -2389,13 +2404,13 @@ it.effect(
modelSelection,
runtimePolicy,
});
const config = yield* mcpSessions.read(threadId);
const config = yield* mcpSessions.read(threadId, modelSelection.instanceId);
assert.isDefined(config);

yield* Fiber.interrupt(stale);

assert.equal(
(yield* mcpSessions.read(threadId))?.providerSessionId,
(yield* mcpSessions.read(threadId, modelSelection.instanceId))?.providerSessionId,
config!.providerSessionId,
);
const token = config!.authorizationHeader.replace(/^Bearer\s+/, "");
Expand Down Expand Up @@ -2470,7 +2485,7 @@ it.effect(
});
yield* resume;
yield* manager.detach({ providerSessionId, threadId });
const config = yield* mcpSessions.read(threadId);
const config = yield* mcpSessions.read(threadId, modelSelection.instanceId);
assert.isDefined(config);
const token = config!.authorizationHeader.replace(/^Bearer\s+/, "");

Expand Down Expand Up @@ -2534,14 +2549,14 @@ it.effect("ProviderSessionManagerV2 duplicate detach preserves replacement MCP c
const replacementToken = replacement?.authorizationHeader.replace(/^Bearer\s+/, "");
assert.isDefined(replacementToken);
assert.equal(
(yield* mcpSessions.read(threadId))?.providerSessionId,
(yield* mcpSessions.read(threadId, modelSelection.instanceId))?.providerSessionId,
replacement?.providerSessionId,
);

yield* manager.detach({ providerSessionId: oldSessionId, threadId });

assert.equal(
(yield* mcpSessions.read(threadId))?.providerSessionId,
(yield* mcpSessions.read(threadId, modelSelection.instanceId))?.providerSessionId,
replacement?.providerSessionId,
);
assert.equal((yield* registry.resolve(replacementToken!))?.thread.threadId, threadId);
Expand All @@ -2560,6 +2575,98 @@ it.effect("ProviderSessionManagerV2 duplicate detach preserves replacement MCP c
}),
);

it.effect(
"ProviderSessionManagerV2 preserves MCP access through A → B → A and isolates cleanup",
() =>
Effect.gen(function* () {
const state = yield* Ref.make(emptyState);
const mcpConfigs = yield* Ref.make<
ReadonlyArray<McpProviderSession.McpProviderSessionConfig | undefined>
>([]);
const otherSelection = {
...modelSelection,
instanceId: ProviderInstanceId.make("codex-other"),
};
const effect = Effect.gen(function* () {
const eventSink = yield* EventSink.EventSinkV2;
const idAllocator = yield* IdAllocator.IdAllocatorV2;
const manager = yield* ProviderSessionManager.ProviderSessionManagerV2;
const registry = yield* McpSessionRegistry.McpSessionRegistry;
const mcpSessions = yield* McpProviderSessions.McpProviderSessions;
const now = yield* DateTime.now;
const threadId = ThreadId.make("thread-provider-session-manager-switch-back");
const firstSessionId = yield* idAllocator.allocate.providerSession({
providerInstanceId: modelSelection.instanceId,
threadId,
});
const otherSessionId = yield* idAllocator.allocate.providerSession({
providerInstanceId: otherSelection.instanceId,
threadId,
});
const firstInput = {
threadId,
providerSessionId: firstSessionId,
modelSelection,
runtimePolicy,
};
const otherInput = {
...firstInput,
providerSessionId: otherSessionId,
modelSelection: otherSelection,
};
const assertAccess = Effect.fnUntraced(function* (
config: McpProviderSession.McpProviderSessionConfig,
) {
const token = config.authorizationHeader.replace(/^Bearer\s+/, "");
const caller = yield* registry.resolve(token);
assert.equal(caller?.thread.threadId, threadId);
assert.equal(caller?.thread.providerInstanceId, config.providerInstanceId);
assert.deepEqual(yield* mcpSessions.read(threadId, config.providerInstanceId), config);
return token;
});

yield* eventSink.write({
events: [yield* makeThreadCreatedEvent({ idAllocator, threadId, now })],
});
const firstRuntime = yield* manager.open(firstInput);
yield* manager.open(otherInput);
const [firstConfig, otherConfig] = yield* Ref.get(mcpConfigs);
assert.equal(firstConfig?.providerInstanceId, modelSelection.instanceId);
assert.equal(otherConfig?.providerInstanceId, otherSelection.instanceId);
yield* assertAccess(firstConfig!);
const otherToken = yield* assertAccess(otherConfig!);

assert.strictEqual(yield* manager.open(firstInput), firstRuntime);
assert.equal((yield* Ref.get(state)).openCount, 2);
yield* assertAccess(firstConfig!);
yield* assertAccess(otherConfig!);

yield* manager.closeInstance(otherSelection.instanceId);
assert.isUndefined(yield* registry.resolve(otherToken));
assert.isUndefined(yield* mcpSessions.read(threadId, otherSelection.instanceId));
yield* assertAccess(firstConfig!);

yield* manager.open(otherInput);
const reopenedConfig = (yield* Ref.get(mcpConfigs)).at(-1);
assert.isDefined(reopenedConfig);
assert.notEqual(yield* assertAccess(reopenedConfig!), otherToken);
yield* assertAccess(firstConfig!);
});

yield* effect.pipe(
Effect.provide(
layerTest({
state,
idleTimeoutMs: 1_000,
capabilities: ExclusiveCapabilities,
mcpConfigs,
secondInstanceId: otherSelection.instanceId,
}),
),
);
}),
);

it.effect(
"ProviderSessionManagerV2 keeps a thread's MCP credential stable across detach and re-attach on a shared session",
() =>
Expand Down Expand Up @@ -2610,7 +2717,7 @@ it.effect(
runtimePolicy,
});
assert.equal(
(yield* mcpSessions.read(threadId))?.providerSessionId,
(yield* mcpSessions.read(threadId, modelSelection.instanceId))?.providerSessionId,
original?.providerSessionId,
"re-attach must reuse the existing credential, not rotate it",
);
Expand Down Expand Up @@ -2671,7 +2778,7 @@ it.effect(
yield* Ref.set(duringOpen, manager.close(s1).pipe(Effect.orDie));
yield* manager.open({ threadId, providerSessionId: s2, modelSelection, runtimePolicy });

const slot = yield* mcpSessions.read(threadId);
const slot = yield* mcpSessions.read(threadId, modelSelection.instanceId);
assert.equal(
slot?.providerSessionId,
original?.providerSessionId,
Expand Down Expand Up @@ -2738,7 +2845,7 @@ it.effect("ProviderSessionManagerV2 terminal detach revokes the thread's MCP cre
revokeMcpCredential: true,
});
assert.isUndefined(yield* registry.resolve(token!));
assert.isUndefined(yield* mcpSessions.read(threadId));
assert.isUndefined(yield* mcpSessions.read(threadId, modelSelection.instanceId));
});

yield* effect.pipe(Effect.provide(layerTest({ state, idleTimeoutMs: 1_000, mcpConfigs })));
Expand Down
Loading
Loading