Skip to content

Commit 33c5140

Browse files
committed
fix: open new cdp on browser fail
1 parent 685d5fc commit 33c5140

4 files changed

Lines changed: 89 additions & 45 deletions

File tree

src/lib/agent/browser-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
} from "playwright-core";
77

88
const SCREENSHOT_TIMEOUT_MS = 5_000;
9-
export const NAVIGATION_TIMEOUT_MS = 90_000;
9+
export const NAVIGATION_TIMEOUT_MS = 120_000;
1010
const CAPTCHA_SOLVE_TIMEOUT_MS = 45_000;
1111
const CAPTCHA_POLL_MS = 500;
1212
const CAPTCHA_SETTLE_MS = 1_500;

src/lib/agent/nodes/browser-pipeline.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { extractTicketsWithOpenRouter } from "@/lib/openrouter-client";
1717
import type { TicketResult } from "@/lib/types";
1818

1919
const VIEWPORT = { width: 1280, height: 800 };
20-
const PER_SOURCE_TIMEOUT_MS = 120_000;
20+
const PER_SOURCE_TIMEOUT_MS = 150_000;
2121

2222
function withSource(platform: string, message: string): string {
2323
return `[${platform}] ${message}`;
@@ -130,8 +130,8 @@ export async function browserPipelineNode(
130130
try {
131131
logStatus(`Connecting browser to ${taskState.url}`);
132132
browser = await chromium.connectOverCDP(cdpUrl);
133-
const context = browser.contexts()[0] ?? (await browser.newContext());
134-
const page = context.pages()[0] ?? (await context.newPage());
133+
let context = browser.contexts()[0] ?? (await browser.newContext());
134+
let page = context.pages()[0] ?? (await context.newPage());
135135

136136
applyNavigationTimeouts(context, page);
137137
await installSingleTabNavigation(context);
@@ -141,6 +141,18 @@ export async function browserPipelineNode(
141141
() => page.goto(taskState.url, { waitUntil: "domcontentloaded" }),
142142
{
143143
onStatus: (message) => logStatus(message),
144+
signal,
145+
async recoverPage({ attempt }) {
146+
logStatus(`Opening fresh browser session (attempt ${attempt + 2})...`);
147+
await browser?.close().catch(() => {});
148+
browser = await chromium.connectOverCDP(cdpUrl);
149+
context = browser.contexts()[0] ?? (await browser.newContext());
150+
page = context.pages()[0] ?? (await context.newPage());
151+
applyNavigationTimeouts(context, page);
152+
await installSingleTabNavigation(context);
153+
await page.setViewportSize(VIEWPORT);
154+
return page;
155+
},
144156
},
145157
);
146158

@@ -194,15 +206,11 @@ export async function browserPipelineNode(
194206
};
195207
}
196208

197-
if (signal.aborted) {
198-
logStatus("Timeout reached after browse loop; skipping ticket extraction.");
199-
return {
200-
tickets: [buildFallbackResult(platform, fallbackUrl, finalAnswer)],
201-
statusLog,
202-
};
203-
}
204-
209+
// Always attempt structured extraction if we have a finalAnswer,
210+
// even after timeout — the OpenRouter call is a fast API request
211+
// that doesn't need the browser.
205212
try {
213+
logStatus("Extracting structured ticket data from browse results...");
206214
const tickets = await extractTicketsWithOpenRouter({
207215
query: taskState.query,
208216
finalAnswer,

src/lib/agent/nodes/n1-browse-core.ts

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const SYSTEM_PROMPT = [
2020
"",
2121
"IMPORTANT: Do NOT give up after a single 'wait' action. If you don't see ticket prices yet, scroll down, click on ticket sections, or interact with the page to reveal them.",
2222
"",
23+
"CRITICAL: Do NOT navigate away from the current page to a different event or URL. Stay on this page and extract ticket information from it. If a navigation fails, do NOT retry — just continue gathering data from the current page.",
24+
"",
2325
"When you provide your final findings, include event metadata: event name, event date, venue, and city.",
2426
"For each ticket option, include ticket type, section, row, seats, quantity, price, currency, platform, URL, and notes.",
2527
"If any field is unavailable, explicitly write Unknown.",
@@ -149,14 +151,32 @@ export async function runN1BrowseLoop(
149151
}
150152

151153
const actionDescription = describeToolCall(toolCall);
152-
activePage = await executeN1Action(activePage, toolCall, VIEWPORT, {
153-
onStatus: (message) =>
154-
emitAgentEvent({
155-
type: "status",
156-
message,
157-
source,
158-
}),
159-
});
154+
let actionError: string | null = null;
155+
try {
156+
activePage = await executeN1Action(activePage, toolCall, VIEWPORT, {
157+
onStatus: (message) =>
158+
emitAgentEvent({
159+
type: "status",
160+
message,
161+
source,
162+
}),
163+
signal,
164+
});
165+
} catch (navError) {
166+
if (
167+
navError instanceof Error &&
168+
navError.name === "NavigationAbortedError"
169+
) {
170+
break;
171+
}
172+
actionError =
173+
navError instanceof Error ? navError.message : String(navError);
174+
emitAgentEvent({
175+
type: "status",
176+
message: `Action "${actionDescription}" failed: ${actionError}. Continuing browse loop.`,
177+
source,
178+
});
179+
}
160180
activePage = await ensureUsablePage(activePage);
161181
stepCount += 1;
162182
currentUrl = activePage.url();
@@ -169,8 +189,15 @@ export async function runN1BrowseLoop(
169189
role: "tool",
170190
tool_call_id: toolCallId,
171191
content: JSON.stringify({
172-
status: "ok",
192+
status: actionError ? "error" : "ok",
173193
current_url: currentUrl,
194+
...(actionError
195+
? {
196+
error: actionError,
197+
instruction:
198+
"Navigation failed. Do NOT retry this URL. Continue gathering ticket information from the current page instead.",
199+
}
200+
: {}),
174201
}),
175202
});
176203
}
@@ -216,27 +243,34 @@ export async function runN1BrowseLoop(
216243
}
217244
}
218245

219-
if (!finalAnswer && stepCount >= taskState.maxSteps && !signal?.aborted) {
220-
const finalizeResponse = await callN1([
221-
...messages,
222-
{
223-
role: "user",
224-
content:
225-
"[Steps remaining: 0] Stop browsing now and provide your final ticket findings.",
226-
},
227-
]);
246+
if (!finalAnswer && (stepCount >= taskState.maxSteps || signal?.aborted)) {
247+
try {
248+
const finalizeResponse = await callN1([
249+
...messages,
250+
{
251+
role: "user",
252+
content:
253+
"[Steps remaining: 0] Stop browsing now and provide your final ticket findings based on everything you have seen so far.",
254+
},
255+
]);
228256

229-
messages.push({
230-
role: "assistant",
231-
content: finalizeResponse.content,
232-
tool_calls: finalizeResponse.tool_calls,
233-
});
257+
messages.push({
258+
role: "assistant",
259+
content: finalizeResponse.content,
260+
tool_calls: finalizeResponse.tool_calls,
261+
});
234262

235-
finalAnswer =
236-
finalizeResponse.content ||
237-
"Maximum step limit reached before a final answer was produced.";
238-
status = `Reached max browsing steps (${taskState.maxSteps}). Finalized response.`;
239-
emitAgentEvent({ type: "status", message: status, source });
263+
finalAnswer =
264+
finalizeResponse.content ||
265+
"Maximum step limit reached before a final answer was produced.";
266+
const reason = signal?.aborted ? "timeout" : "max steps";
267+
status = `Finalized response due to ${reason} after ${stepCount} steps.`;
268+
emitAgentEvent({ type: "status", message: status, source });
269+
} catch (finalizeError) {
270+
const msg = finalizeError instanceof Error ? finalizeError.message : "Unknown error";
271+
status = `Failed to finalize after ${stepCount} steps: ${msg}`;
272+
emitAgentEvent({ type: "status", message: status, source });
273+
}
240274
}
241275

242276
return {

src/lib/agent/tools.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ViewportSize {
88

99
interface NavigationStatusOptions {
1010
onStatus?: (message: string) => void;
11+
signal?: AbortSignal;
1112
}
1213

1314
const N1_COORDINATE_SPACE = 1000;
@@ -380,7 +381,7 @@ async function resolveActivePageAfterPopup(
380381
await navigateWithRecovery(
381382
page,
382383
() => page.goto(popupUrl, { waitUntil: "domcontentloaded" }),
383-
{ onStatus: options?.onStatus },
384+
{ onStatus: options?.onStatus, signal: options?.signal, retries: 0 },
384385
);
385386
}
386387
await popup.close().catch(() => {});
@@ -478,7 +479,7 @@ async function clickInCurrentPage(
478479
usablePage,
479480
() =>
480481
usablePage.goto(anchorCandidate.href, { waitUntil: "domcontentloaded" }),
481-
{ onStatus: options.onStatus },
482+
{ onStatus: options.onStatus, signal: options.signal, retries: 0 },
482483
);
483484
return ensureUsablePage(usablePage);
484485
}
@@ -504,6 +505,7 @@ async function clickInCurrentPage(
504505
if (popup) {
505506
return resolveActivePageAfterPopup(usablePage, popup, {
506507
onStatus: options.onStatus,
508+
signal: options.signal,
507509
});
508510
}
509511
return ensureUsablePage(usablePage);
@@ -704,7 +706,7 @@ export async function executeN1Action(
704706
await navigateWithRecovery(
705707
activePage,
706708
() => activePage.goto(url, { waitUntil: "domcontentloaded" }),
707-
{ onStatus: options?.onStatus },
709+
{ onStatus: options?.onStatus, signal: options?.signal, retries: 0 },
708710
);
709711
return ensureUsablePage(activePage);
710712
}
@@ -713,7 +715,7 @@ export async function executeN1Action(
713715
await navigateWithRecovery(
714716
activePage,
715717
() => activePage.goBack({ waitUntil: "domcontentloaded" }),
716-
{ onStatus: options?.onStatus },
718+
{ onStatus: options?.onStatus, signal: options?.signal, retries: 0 },
717719
);
718720
return ensureUsablePage(activePage);
719721
}
@@ -722,7 +724,7 @@ export async function executeN1Action(
722724
await navigateWithRecovery(
723725
activePage,
724726
() => activePage.reload({ waitUntil: "domcontentloaded" }),
725-
{ onStatus: options?.onStatus },
727+
{ onStatus: options?.onStatus, signal: options?.signal, retries: 0 },
726728
);
727729
return ensureUsablePage(activePage);
728730
}

0 commit comments

Comments
 (0)