From 3ee43b688bd8466bb512ea8fb55c53e6a71a666c Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Wed, 21 Oct 2026 14:41:47 -0700 Subject: [PATCH] fix(providers): keep preview timeout recovery inside T3 --- .../src/mcp/PreviewAutomationBroker.test.ts | 49 +++++++++++++++++++ packages/contracts/src/previewAutomation.ts | 3 ++ .../src/server/orchestrationInstructions.ts | 7 ++- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index 7ae8bfbefead5..58e2fb2f7bb1f 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -5,6 +5,7 @@ import { PreviewAutomationClientDisconnectedError, PreviewAutomationMalformedResponseError, PreviewAutomationNoAvailableHostError, + PreviewAutomationTimeoutError, PreviewTabId, ProviderInstanceId, ThreadId, @@ -1032,3 +1033,51 @@ it.effect("keeps each thread's current tab when threads share a provider session }), ), ); + +it.effect("reports a browser operation timeout without claiming the broker deadline elapsed", () => + Effect.scoped( + Effect.gen(function* () { + const broker = yield* makeBroker; + const connected = yield* Deferred.make(); + const requests = requestsFrom(yield* broker.connect(makeHost()), () => { + Deferred.doneUnsafe(connected, Effect.void); + }); + const connections: string[] = []; + yield* Stream.runForEach(requests, (request) => { + connections.push(request.connectionId); + return broker.respond({ + clientId: "client-1", + connectionId: request.connectionId, + requestId: request.requestId, + ...(request.operation === "snapshot" + ? { + ok: false, + error: { + _tag: "PreviewAutomationTimeoutError", + message: "The page was too busy to capture a snapshot in time.", + }, + } + : { ok: true, result: "responsive" }), + }); + }).pipe(Effect.forkScoped); + yield* Deferred.await(connected); + + const tabId = PreviewTabId.make("tab-1"); + const error = yield* broker + .invoke({ scope, operation: "snapshot", input: {}, tabId }) + .pipe(Effect.flip); + + expect(error).toBeInstanceOf(PreviewAutomationTimeoutError); + expect(error.message).toContain("browser's operation deadline"); + expect(error.message).not.toContain("15000ms"); + expect(error.message).toContain("preview_status"); + expect(yield* broker.invoke({ scope, operation: "status", input: {}, tabId })).toBe( + "responsive", + ); + expect(yield* broker.invoke({ scope, operation: "evaluate", input: {}, tabId })).toBe( + "responsive", + ); + expect(new Set(connections).size).toBe(1); + }), + ), +); diff --git a/packages/contracts/src/previewAutomation.ts b/packages/contracts/src/previewAutomation.ts index 0e9ff94d6ffe5..d63e75903837e 100644 --- a/packages/contracts/src/previewAutomation.ts +++ b/packages/contracts/src/previewAutomation.ts @@ -949,6 +949,9 @@ export class PreviewAutomationTimeoutError extends Schema.TaggedError