diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index 72d5d4b044d3..f8105c993238 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -1504,3 +1504,43 @@ it.effect("keeps the host connected when a background status read times out", () }), ), ); + +it.effect("keeps each thread's current tab when threads share a provider session", () => + Effect.scoped( + Effect.gen(function* () { + const broker = yield* makeBroker; + const routed: Array = []; + const requests = requestsFrom(yield* broker.connect(makeHost(), { preferred: true })); + yield* Stream.runForEach(requests, (request) => { + routed.push(request); + return broker.respond({ + clientId: "client-1", + connectionId: request.connectionId, + requestId: request.requestId, + ok: true, + result: { tabId: request.tabId ?? `tab-${request.threadId}` }, + }); + }).pipe(Effect.forkScoped); + yield* Effect.yieldNow; + // Codex runs every thread of an instance in one provider session. + const otherThread = { + ...scope, + thread: { ...scope.thread, threadId: ThreadId.make("thread-2") }, + }; + + yield* broker.invoke({ scope, operation: "open", input: {} }); + yield* broker.invoke({ scope: otherThread, operation: "snapshot", input: {} }); + yield* broker.invoke({ scope, operation: "snapshot", input: {} }); + yield* broker.invoke({ scope: otherThread, operation: "snapshot", input: {} }); + + expect(routed.map(({ threadId, tabId }) => [threadId, tabId])).toEqual([ + ["thread-1", undefined], + ["thread-2", undefined], + ["thread-1", "tab-thread-1"], + ["thread-2", "tab-thread-2"], + ]); + // Each thread's agent owns its own tabs and tab budget. + expect(routed[0]!.agentSessionId).not.toBe(routed[1]!.agentSessionId); + }), + ), +); diff --git a/apps/server/src/mcp/PreviewAutomationBroker.ts b/apps/server/src/mcp/PreviewAutomationBroker.ts index 478632422152..f93d6f102970 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.ts @@ -176,8 +176,13 @@ const selectorDiagnosticsFromInput = ( return {}; }; +/** + * One agent: a provider session on one thread. Codex and OpenCode run every + * thread of a provider instance in one session, so the session alone would + * share one current tab, tab budget, and tab ownership across chats. + */ const hostAssignmentKey = (scope: McpInvocationContext.McpThreadInvocationScope): string => - `${scope.environmentId}\u0000${scope.thread.providerSessionId}`; + `${scope.environmentId}\u0000${scope.thread.providerSessionId}\u0000${scope.thread.threadId}`; const isPreviewTabId = Schema.is(PreviewTabId); const decodeControlReason = Schema.decodeUnknownOption(PreviewAutomationControlReason);