Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6a6f078
fix(server): a slow preview action no longer disconnects every chat's…
TDanks2000 Oct 10, 2026
aaf0004
fix(server): preview agents on threads sharing a provider session kee…
TDanks2000 Oct 10, 2026
d9196b3
fix(server): idle sweep no longer closes agent tabs the desktop app s…
TDanks2000 Oct 10, 2026
ec993ad
fix(server): a page that never answers no longer freezes its preview tab
TDanks2000 Oct 10, 2026
1862b8b
fix(web): report browser profiles only to connected environments, onc…
TDanks2000 Oct 10, 2026
c007f35
fix(server): exempt only the server's own browser from timeout eviction
TDanks2000 Oct 10, 2026
7d683ac
fix(web): retry a failed browser profile report
TDanks2000 Oct 10, 2026
4758c8c
Revert "fix(web): retry a failed browser profile report"
TDanks2000 Oct 11, 2026
5311fc0
Revert "fix(web): report browser profiles only to connected environme…
TDanks2000 Oct 11, 2026
07d82da
Revert "fix(server): idle sweep no longer closes agent tabs the deskt…
TDanks2000 Oct 11, 2026
695f58d
Revert "fix(server): preview agents on threads sharing a provider ses…
TDanks2000 Oct 11, 2026
dfca2a6
Merge main into preview timeout recovery
juliusmarminge Oct 19, 2026
a0d94fa
test(server): cover preview timeout retention and queue recovery
juliusmarminge Oct 19, 2026
c12a5f0
fix(server): recover preview captures after screencast timeouts
juliusmarminge Oct 19, 2026
2484482
fix(server): bound preview snapshot capture lock waits
juliusmarminge Oct 19, 2026
94bd7ca
fix(server): retire stalled preview recording sessions
juliusmarminge Oct 19, 2026
3bd2412
fix(server): release held preview input after session timeouts
juliusmarminge Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions apps/server/src/mcp/PreviewAutomationBroker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1540,3 +1540,84 @@ it.effect("keeps each thread's current tab when threads share a provider session
}),
),
);

it.effect.each([
{ clientId: SERVER_BROWSER_AUTOMATION_CLIENT_ID, retained: true },
{ clientId: "remote-preferred", retained: false },
])(
"an unanswered action preserves other threads only on the in-process host ($clientId)",
({ clientId, retained }) =>
Effect.scoped(
Effect.gen(function* () {
const broker = yield* makeBroker;
const otherScope = {
...scope,
thread: {
...scope.thread,
threadId: ThreadId.make("thread-2"),
providerSessionId: "provider-session-2",
},
};
const connected = yield* Deferred.make<void>();
const slowReceived = yield* Deferred.make<void>();
const otherReceived = yield* Deferred.make<RoutedRequest>();
const requests = requestsFrom(
yield* broker.connect(makeHost({ clientId }), { preferred: true }),
() => Deferred.doneUnsafe(connected, Effect.void),
);
yield* Stream.runForEach(requests, (request) => {
if (request.operation === "snapshot") return Deferred.succeed(slowReceived, undefined);
if (request.operation === "evaluate") return Deferred.succeed(otherReceived, request);
return broker.respond({
clientId,
connectionId: request.connectionId,
requestId: request.requestId,
ok: true,
result: {
tabId: request.operation === "open" ? `tab-${request.threadId}` : request.tabId,
},
});
}).pipe(Effect.forkScoped);

yield* Deferred.await(connected);
for (const caller of [scope, otherScope])
yield* broker.invoke({ scope: caller, operation: "open", input: {} });
const pending = yield* broker
.invoke({ scope: otherScope, operation: "evaluate", input: {}, timeoutMs: 10_000 })
.pipe(Effect.result, Effect.forkScoped);
const otherRequest = yield* Deferred.await(otherReceived);
const timedOut = yield* broker
.invoke<void>({ scope, operation: "snapshot", input: {}, timeoutMs: 1_000 })
.pipe(Effect.flip, Effect.forkScoped);
yield* Deferred.await(slowReceived);
yield* TestClock.adjust(2_000);
expect(yield* Fiber.join(timedOut)).toMatchObject({
_tag: "PreviewAutomationTimeoutError",
});

yield* broker.respond({
clientId,
connectionId: otherRequest.connectionId,
requestId: otherRequest.requestId,
ok: true,
result: { tabId: otherRequest.tabId },
});
const otherResult = yield* Fiber.join(pending);
expect(otherResult).toMatchObject(
retained
? { _tag: "Success", success: { tabId: "tab-thread-2" } }
: { _tag: "Failure", failure: { _tag: "PreviewAutomationClientDisconnectedError" } },
);
for (const caller of [scope, otherScope]) {
const next = yield* broker
.invoke({ scope: caller, operation: "click", input: {} })
.pipe(Effect.result);
expect(next).toMatchObject(
retained
? { _tag: "Success", success: { tabId: `tab-${caller.thread.threadId}` } }
: { _tag: "Failure", failure: { _tag: "PreviewAutomationNoAvailableHostError" } },
);
}
}),
),
);
6 changes: 5 additions & 1 deletion apps/server/src/mcp/PreviewAutomationBroker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,11 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {
// becoming unreachable. A background metadata read has a short budget
// and changes nothing, so a slow one must not cut the host off from the
// agent's next call.
if (input.updateCurrentTab !== false) {
// A slow in-process page must not drop other threads' requests and tab assignments.
if (
input.updateCurrentTab !== false &&
connection.clientId !== SERVER_BROWSER_AUTOMATION_CLIENT_ID
) {
yield* disconnect(connection.clientId, connection.queue, true);
}
return yield* new PreviewAutomationTimeoutError(requestContext);
Expand Down
Loading
Loading