Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions apps/server/src/mcp/PreviewAutomationBroker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
PreviewAutomationClientDisconnectedError,
PreviewAutomationMalformedResponseError,
PreviewAutomationNoAvailableHostError,
PreviewAutomationTimeoutError,
PreviewTabId,
ProviderInstanceId,
ThreadId,
Expand Down Expand Up @@ -1031,3 +1032,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<void>();
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<void>({ 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);
}),
),
);
3 changes: 3 additions & 0 deletions packages/contracts/src/previewAutomation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,9 @@ export class PreviewAutomationTimeoutError extends Schema.TaggedError<PreviewAut
},
) {
override get message(): string {
if (this.remoteTag === "PreviewAutomationTimeoutError") {
return `Preview automation ${this.operation} exceeded the browser's operation deadline. The browser responded; check preview_status on the same tab before retrying.`;
}
const summary = `Preview automation ${this.operation} timed out after ${this.timeoutMs}ms.`;
return summary;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = `

## T3 Code browser

When browser work was asked, \`preview_*\` is the browser you share with the user: \`preview_status\` first, then \`preview_open\` if no tab can be automated. Prefer snapshot locators over coordinates. Switch browsers only if the tools are absent or \`preview_open\` says unsupported, the user asks, or two preview calls fail on one step. Then quote the error and switch.
When browser work was asked, \`preview_*\` is the browser you share with the user: \`preview_status\` first, then \`preview_open\` if no tab can be automated. Prefer snapshot locators over coordinates.

An operation timeout or missing locator does not mean the browser is unavailable. Correct actionable inputs and check \`preview_status\` on the same tab. If a snapshot times out while status works, use \`preview_evaluate\` for DOM inspection when you control the tab. After repeated failures, report the exact error and recovery checks.

Keep recovery inside T3. Preview failures do not authorize opening another browser or incognito window, activating apps, or changing the user's focus. Switch browsers only when the user authorizes it or invokes a skill or documented repository workflow that names one. If tools are absent or no host is available after \`preview_status\` and \`preview_open\`, report the missing prerequisite.
`;

// ACP only: some ACP agents accept the injected MCP server but never expose
Expand Down
Loading