Skip to content

Commit a530815

Browse files
fix(preview): a slow tool-icon status read no longer drops the browser host
After each action the MCP handler reads status with a 500ms budget to draw the tool's site icon. When the server browser was still settling, for example after preview_resize, that read timed out and the broker disconnected the host, so the agent's next call failed with NoAvailableHost until it reconnected. Background reads that cannot change the current tab now time out without disconnecting. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 26a2af6 commit a530815

2 files changed

Lines changed: 43 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
@@ -1529,3 +1529,41 @@ it.effect("keeps a host that responds with an operation timeout", () =>
15291529
}),
15301530
),
15311531
);
1532+
1533+
it.effect("keeps the host connected when a background status read times out", () =>
1534+
Effect.scoped(
1535+
Effect.gen(function* () {
1536+
const broker = yield* makeBroker;
1537+
const requests = requestsFrom(yield* broker.connect(makeHost()));
1538+
// A busy host answers its actions but not the metadata read behind them.
1539+
yield* Stream.runForEach(requests, (request) =>
1540+
request.operation === "status"
1541+
? Effect.void
1542+
: broker.respond({
1543+
clientId: "client-1",
1544+
connectionId: request.connectionId,
1545+
requestId: request.requestId,
1546+
ok: true,
1547+
result: { operation: request.operation },
1548+
}),
1549+
).pipe(Effect.forkScoped);
1550+
yield* Effect.yieldNow;
1551+
1552+
const status = yield* broker
1553+
.invoke<void>({
1554+
scope,
1555+
operation: "status",
1556+
input: {},
1557+
timeoutMs: 500,
1558+
updateCurrentTab: false,
1559+
})
1560+
.pipe(Effect.flip, Effect.forkScoped);
1561+
yield* TestClock.adjust(500);
1562+
expect(yield* Fiber.join(status)).toMatchObject({ _tag: "PreviewAutomationTimeoutError" });
1563+
1564+
expect(yield* broker.invoke({ scope, operation: "snapshot", input: {} })).toEqual({
1565+
operation: "snapshot",
1566+
});
1567+
}),
1568+
),
1569+
);

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,11 @@ export const make = Effect.gen(function* PreviewAutomationBrokerMake() {
648648
Effect.gen(function* () {
649649
// An unanswered request invalidates this connection. Do not replay
650650
// actions: the client may have applied them before becoming unreachable.
651-
yield* disconnect(connection.clientId, connection.queue, true);
651+
// A background metadata read has a short budget and changes nothing,
652+
// so a slow one must not cut the host off from the agent's next call.
653+
if (input.updateCurrentTab !== false) {
654+
yield* disconnect(connection.clientId, connection.queue, true);
655+
}
652656
return yield* new PreviewAutomationTimeoutError(requestContext);
653657
}),
654658
onSome: (value) => Effect.succeed(value as A),

0 commit comments

Comments
 (0)