Skip to content

Commit aaf0004

Browse files
TDanks2000claude
andcommitted
fix(server): preview agents on threads sharing a provider session keep their own tabs
Codex and OpenCode run every thread of a provider instance in one provider session. The broker keyed an agent by that session alone, so parallel chats shared one current tab, one tab budget, and tab ownership: a call from one chat was routed to another chat's tab and failed. An agent is now a provider session on one thread. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 6a6f078 commit aaf0004

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

‎apps/server/src/mcp/PreviewAutomationBroker.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,3 +1559,41 @@ it.effect("keeps the server's own browser and the agent's tab when an action tim
15591559
}),
15601560
),
15611561
);
1562+
1563+
it.effect("keeps each thread's current tab when threads share a provider session", () =>
1564+
Effect.scoped(
1565+
Effect.gen(function* () {
1566+
const broker = yield* makeBroker;
1567+
const routed: Array<RoutedRequest> = [];
1568+
const requests = requestsFrom(yield* broker.connect(makeHost(), { preferred: true }));
1569+
yield* Stream.runForEach(requests, (request) => {
1570+
routed.push(request);
1571+
return broker.respond({
1572+
clientId: "client-1",
1573+
connectionId: request.connectionId,
1574+
requestId: request.requestId,
1575+
ok: true,
1576+
result: { tabId: request.tabId ?? `tab-${request.threadId}` },
1577+
});
1578+
}).pipe(Effect.forkScoped);
1579+
yield* Effect.yieldNow;
1580+
// Codex runs every thread of an instance in one provider session.
1581+
const otherThread = {
1582+
...scope,
1583+
thread: { ...scope.thread, threadId: ThreadId.make("thread-2") },
1584+
};
1585+
1586+
yield* broker.invoke({ scope, operation: "open", input: {} });
1587+
yield* broker.invoke({ scope: otherThread, operation: "snapshot", input: {} });
1588+
yield* broker.invoke({ scope, operation: "snapshot", input: {} });
1589+
1590+
expect(routed.map(({ threadId, tabId }) => [threadId, tabId])).toEqual([
1591+
["thread-1", undefined],
1592+
["thread-2", undefined],
1593+
["thread-1", "tab-thread-1"],
1594+
]);
1595+
// Each thread's agent owns its own tabs and tab budget.
1596+
expect(routed[0]!.agentSessionId).not.toBe(routed[1]!.agentSessionId);
1597+
}),
1598+
),
1599+
);

‎apps/server/src/mcp/PreviewAutomationBroker.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,13 @@ const selectorDiagnosticsFromInput = (
177177
return {};
178178
};
179179

180+
/**
181+
* One agent: a provider session on one thread. Codex and OpenCode run every
182+
* thread of a provider instance in one session, so the session alone would
183+
* share one current tab, tab budget, and tab ownership across chats.
184+
*/
180185
const hostAssignmentKey = (scope: McpInvocationContext.McpThreadInvocationScope): string =>
181-
`${scope.environmentId}\u0000${scope.thread.providerSessionId}`;
186+
`${scope.environmentId}\u0000${scope.thread.providerSessionId}\u0000${scope.thread.threadId}`;
182187

183188
const isPreviewTabId = Schema.is(PreviewTabId);
184189
const decodeControlReason = Schema.decodeUnknownOption(PreviewAutomationControlReason);

0 commit comments

Comments
 (0)