@quanta-intellect/vessel-browser 0.1.143 → 0.1.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/out/main/index.js CHANGED
@@ -8817,6 +8817,106 @@ function buildFlightPriceEvidenceRecoveryPrompt(userMessage, assistantText, late
8817
8817
  `Last unsupported answer: ${assistantText.replace(/\s+/g, " ").trim().slice(0, 500) || "(empty)"}`
8818
8818
  ].join("\n");
8819
8819
  }
8820
+ const SEARCH_HISTORY_LIMIT = 4;
8821
+ function normalizeSearchToolQuery(name, args) {
8822
+ if (name !== "search" && name !== "web_search") return null;
8823
+ const raw = typeof args.query === "string" ? args.query : typeof args.text === "string" ? args.text : typeof args.term === "string" ? args.term : "";
8824
+ const normalized = raw.replace(/\s+/g, " ").trim().toLowerCase();
8825
+ return normalized || null;
8826
+ }
8827
+ function buildLatestStateReminder(toolResultPreview) {
8828
+ const text = (toolResultPreview || "").trim();
8829
+ if (!text) return "";
8830
+ const existingReminder = text.match(
8831
+ /\bLatest browser state:\s*URL\s+.+?(?:Trust the latest tool result over the initial page context\.|$)/i
8832
+ )?.[0]?.trim();
8833
+ if (existingReminder) return existingReminder;
8834
+ const stateMatch = text.match(
8835
+ /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
8836
+ );
8837
+ if (stateMatch) {
8838
+ const url = stateMatch[1]?.trim();
8839
+ const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
8840
+ if (url) {
8841
+ return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
8842
+ }
8843
+ }
8844
+ const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8845
+ const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8846
+ if (structuredUrl) {
8847
+ return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8848
+ }
8849
+ const navigatedUrl = text.match(
8850
+ /\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i
8851
+ )?.[1]?.trim() ?? text.match(
8852
+ /\b(?:web\s+)?searched "[^"]+"[^\n]*?(?:->|→)\s+([^\s\n]+)/i
8853
+ )?.[1]?.trim();
8854
+ const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
8855
+ if (navigatedUrl) {
8856
+ return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8857
+ }
8858
+ return "";
8859
+ }
8860
+ function buildRepeatedSearchError(previousTool, previousQuery, latestToolResultPreview, mode) {
8861
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8862
+ const lines = [
8863
+ mode === "drifted" ? `Error: You already performed ${previousTool} successfully for this task.` : `Error: You already searched for "${previousQuery}" successfully with ${previousTool}.`,
8864
+ mode === "drifted" ? `Do not rewrite or broaden the query with another ${previousTool}; use the current search results instead.` : `Do not search the same query again with ${previousTool} or its search/web_search alias; use the current search results instead.`,
8865
+ `For named venues, businesses, organizations, schools, or local places, prefer opening the official site or clearly direct result from the current results page before answering. Do not switch to a site: restricted web_search when an official or direct result is already available.`,
8866
+ `Take the next action from the results you already have: click a result, inspect a specific item, or provide the final answer to the user. Do not call any search tool again as preparation, and do not call read_page as preparation for another search.`
8867
+ ];
8868
+ if (stateReminder) {
8869
+ lines.push(stateReminder);
8870
+ }
8871
+ return lines.join(" ");
8872
+ }
8873
+ class SearchLoopGuard {
8874
+ recentSuccessfulSearchQueries = [];
8875
+ recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
8876
+ lastSuccessfulWebSearchQuery = null;
8877
+ isContextResettingTool;
8878
+ constructor(isContextResettingTool) {
8879
+ this.isContextResettingTool = isContextResettingTool;
8880
+ }
8881
+ /**
8882
+ * Check whether a search/web_search call should be suppressed. Returns the
8883
+ * details needed to build the corrective error, or null if the call is OK.
8884
+ */
8885
+ check(toolName, query) {
8886
+ const isRepeatedSearchAcrossTools = query !== null && this.recentSuccessfulSearchQueries.includes(query);
8887
+ const isQueryDriftedWebSearch = toolName === "web_search" && this.lastSuccessfulWebSearchQuery !== null && query !== null && query !== this.lastSuccessfulWebSearchQuery;
8888
+ if (!isRepeatedSearchAcrossTools && !isQueryDriftedWebSearch) return null;
8889
+ const mode = isRepeatedSearchAcrossTools ? "repeated" : "drifted";
8890
+ const previousTool = isRepeatedSearchAcrossTools ? this.recentSuccessfulSearchToolByQuery.get(query ?? "") ?? (toolName === "web_search" ? "search" : "web_search") : "web_search";
8891
+ const previousQuery = isRepeatedSearchAcrossTools ? query ?? "" : this.lastSuccessfulWebSearchQuery ?? "";
8892
+ return { mode, previousTool, previousQuery };
8893
+ }
8894
+ /**
8895
+ * Record a successfully executed tool. Search queries are added to the
8896
+ * recent-history ring buffer, and real-progress tools clear the drift anchor
8897
+ * so a later distinct search is not flagged as drift.
8898
+ */
8899
+ recordSuccess(toolName, query, wasSuccessful) {
8900
+ if (wasSuccessful && this.isContextResettingTool(toolName)) {
8901
+ this.lastSuccessfulWebSearchQuery = null;
8902
+ }
8903
+ if (wasSuccessful && query) {
8904
+ if (!this.recentSuccessfulSearchQueries.includes(query)) {
8905
+ this.recentSuccessfulSearchQueries.push(query);
8906
+ this.recentSuccessfulSearchToolByQuery.set(query, toolName);
8907
+ if (this.recentSuccessfulSearchQueries.length > SEARCH_HISTORY_LIMIT) {
8908
+ const dropped = this.recentSuccessfulSearchQueries.shift();
8909
+ if (dropped) {
8910
+ this.recentSuccessfulSearchToolByQuery.delete(dropped);
8911
+ }
8912
+ }
8913
+ }
8914
+ }
8915
+ if (wasSuccessful && toolName === "web_search" && query) {
8916
+ this.lastSuccessfulWebSearchQuery = query;
8917
+ }
8918
+ }
8919
+ }
8820
8920
  const logger$v = createLogger("OpenAIProvider");
8821
8921
  function shouldDebugAgentLoop() {
8822
8922
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
@@ -8886,7 +8986,7 @@ function buildOpenRouterAttributionHeaders() {
8886
8986
  function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
8887
8987
  if (profile !== "compact") return null;
8888
8988
  const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
8889
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
8989
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8890
8990
  return {
8891
8991
  role: "user",
8892
8992
  content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
@@ -8896,13 +8996,13 @@ ${phaseReminder}` : "")
8896
8996
  };
8897
8997
  }
8898
8998
  function extractSingleGoalDomain(goal) {
8899
- const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.(?:com|org|net|io|dev|app|ai|co|edu|gov))\b/g);
8999
+ const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.[a-z]{2,})\b/g);
8900
9000
  if (!matches || matches.length !== 1) return null;
8901
9001
  return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
8902
9002
  }
8903
9003
  function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
8904
9004
  const phaseReminder = buildPhaseReminder(userMessage, assistantText);
8905
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
9005
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8906
9006
  const goalDomain = extractSingleGoalDomain(userMessage);
8907
9007
  const latest = (latestToolResultPreview || "").toLowerCase();
8908
9008
  const assistant = assistantText.toLowerCase();
@@ -8999,38 +9099,7 @@ function buildPhaseReminder(userMessage, assistantText) {
8999
9099
  }
9000
9100
  return "";
9001
9101
  }
9002
- function buildLatestStateReminder(toolResultPreview) {
9003
- const text = toolResultPreview.trim();
9004
- if (!text) return "";
9005
- const stateMatch = text.match(
9006
- /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
9007
- );
9008
- if (stateMatch) {
9009
- const url = stateMatch[1]?.trim();
9010
- const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
9011
- if (url) {
9012
- return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
9013
- }
9014
- }
9015
- const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
9016
- const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
9017
- if (structuredUrl) {
9018
- return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
9019
- }
9020
- const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i)?.[1]?.trim();
9021
- const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
9022
- if (navigatedUrl) {
9023
- return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
9024
- }
9025
- return "";
9026
- }
9027
- function normalizedSearchToolQuery$1(name, args) {
9028
- if (name !== "search" && name !== "web_search") return null;
9029
- const raw = typeof args.query === "string" ? args.query : typeof args.text === "string" ? args.text : typeof args.term === "string" ? args.term : "";
9030
- const normalized = raw.replace(/\s+/g, " ").trim().toLowerCase();
9031
- return normalized || null;
9032
- }
9033
- function isOpenAIRealProgressToolForSearch(name) {
9102
+ function isSearchContextResettingTool(name) {
9034
9103
  return ![
9035
9104
  "read_page",
9036
9105
  "current_tab",
@@ -9043,18 +9112,6 @@ function isOpenAIRealProgressToolForSearch(name) {
9043
9112
  "search"
9044
9113
  ].includes(name);
9045
9114
  }
9046
- function buildOpenAIRepeatedSearchError(previousTool, previousQuery, latestToolResultPreview, mode) {
9047
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
9048
- const lines = [
9049
- mode === "drifted" ? `Error: You already performed ${previousTool} successfully for this task.` : `Error: You already searched for "${previousQuery}" successfully with ${previousTool}.`,
9050
- mode === "drifted" ? `Do not rewrite or broaden the query with another ${previousTool}; use the current search results instead.` : `Do not search the same query again with ${previousTool} or its search/web_search alias; use the current search results instead.`,
9051
- `For named venues, businesses, organizations, schools, or local places, prefer opening the official site or clearly direct result from the current results page before answering. Do not switch to a site: restricted web_search when an official or direct result is already available. Next action: click a result, inspect a specific result, or answer from the result you already opened. Do not call any search tool again as preparation.`
9052
- ];
9053
- if (stateReminder) {
9054
- lines.push(stateReminder);
9055
- }
9056
- return lines.join(" ");
9057
- }
9058
9115
  function shouldRecoverCompactStall(text, userMessage) {
9059
9116
  const trimmed = text.trim().toLowerCase();
9060
9117
  if (!trimmed) return true;
@@ -9138,15 +9195,24 @@ function logAgentLoopDebug(payload) {
9138
9195
  }
9139
9196
  }
9140
9197
  function formatOpenAICompatErrorMessage(providerId, message) {
9141
- if (providerId === "openrouter" && /(timed out after \d+(?:\.\d+)? seconds|request timed out|returned none after all retries|no content|empty response)/i.test(
9198
+ if (providerId === "openrouter" && /(timed out after \d+(?:\.\d+)? seconds|request timed out|returned none after all retries|returned no content|empty response)/i.test(
9142
9199
  message
9143
9200
  )) {
9144
- return `${message} OpenRouter reported an upstream model timeout/no-content failure. If this persists, retry or pin a specific low-latency tool-calling model instead of the free router.`;
9201
+ return [
9202
+ message,
9203
+ "OpenRouter reported an upstream model timeout/no-content failure.",
9204
+ "If this persists, retry or pin a specific low-latency tool-calling model instead of the free router."
9205
+ ].join(" ");
9145
9206
  }
9146
9207
  if (providerId === "llama_cpp" && /(available context size|context size exceeded|exceeds the available context size|try increasing it)/i.test(
9147
9208
  message
9148
9209
  )) {
9149
- return `${message} llama.cpp sets context size at server startup, not per request. Vessel's agent prompt plus tool schema is about 6.5k tokens before browsing history, so run llama-server with --ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended).`;
9210
+ return [
9211
+ message,
9212
+ "llama.cpp sets context size at server startup, not per request.",
9213
+ `Vessel's agent prompt plus tool schema is about 6.5k tokens before browsing history, so run llama-server with`,
9214
+ `--ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended).`
9215
+ ].join(" ");
9150
9216
  }
9151
9217
  return message;
9152
9218
  }
@@ -9256,9 +9322,7 @@ class OpenAICompatProvider {
9256
9322
  const recentCompactToolSignatures = [];
9257
9323
  const recentToolNames = [];
9258
9324
  const successfulToolNames = [];
9259
- const recentSuccessfulSearchQueries = [];
9260
- const recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
9261
- let lastSuccessfulWebSearchQuery = null;
9325
+ const searchLoopGuard = new SearchLoopGuard(isSearchContextResettingTool);
9262
9326
  let clickReadLoopNudged = false;
9263
9327
  for (let i = 0; i < maxIterations; i++) {
9264
9328
  iterationsUsed = i + 1;
@@ -9493,24 +9557,20 @@ class OpenAICompatProvider {
9493
9557
  args = repairedArgs.args;
9494
9558
  args = coerceToolArgsForExecution(tc.name, args);
9495
9559
  const toolSignature = stableToolSignature(tc.name, args);
9496
- const searchToolQuery = normalizedSearchToolQuery$1(tc.name, args);
9497
- const isRepeatedSearchAcrossTools = searchToolQuery !== null && recentSuccessfulSearchQueries.includes(searchToolQuery);
9498
- const isQueryDriftedWebSearch = tc.name === "web_search" && lastSuccessfulWebSearchQuery !== null && searchToolQuery !== null && searchToolQuery !== lastSuccessfulWebSearchQuery;
9499
- if (isRepeatedSearchAcrossTools || isQueryDriftedWebSearch) {
9560
+ const searchToolQuery = normalizeSearchToolQuery(tc.name, args);
9561
+ const searchLoopCheck = searchLoopGuard.check(tc.name, searchToolQuery);
9562
+ if (searchLoopCheck) {
9500
9563
  onChunk(`
9501
9564
  <<tool:${tc.name}:↻ duplicate suppressed>>
9502
9565
  `);
9503
- const previousTool = isRepeatedSearchAcrossTools ? recentSuccessfulSearchToolByQuery.get(searchToolQuery ?? "") ?? (tc.name === "web_search" ? "search" : "web_search") : "web_search";
9504
- const previousQuery = isRepeatedSearchAcrossTools ? searchToolQuery ?? "" : lastSuccessfulWebSearchQuery ?? "";
9505
- const mode = isRepeatedSearchAcrossTools ? "repeated" : "drifted";
9506
9566
  messages.push({
9507
9567
  role: "tool",
9508
9568
  tool_call_id: tc.id,
9509
- content: buildOpenAIRepeatedSearchError(
9510
- previousTool,
9511
- previousQuery,
9569
+ content: buildRepeatedSearchError(
9570
+ searchLoopCheck.previousTool,
9571
+ searchLoopCheck.previousQuery,
9512
9572
  latestToolMessage ? String(latestToolMessage.content || "") : null,
9513
- mode
9573
+ searchLoopCheck.mode
9514
9574
  )
9515
9575
  });
9516
9576
  compactCorrectionCount += 1;
@@ -9590,25 +9650,15 @@ class OpenAICompatProvider {
9590
9650
  recentCompactToolSignatures.shift();
9591
9651
  }
9592
9652
  }
9593
- if (!/^Error:/i.test(toolContent.trim())) {
9653
+ const toolSucceeded = !/^Error:/i.test(toolContent.trim());
9654
+ if (toolSucceeded) {
9594
9655
  successfulToolNames.push(tc.name);
9595
- if (isOpenAIRealProgressToolForSearch(tc.name)) {
9596
- lastSuccessfulWebSearchQuery = null;
9597
- }
9598
- if (searchToolQuery && !recentSuccessfulSearchQueries.includes(searchToolQuery)) {
9599
- recentSuccessfulSearchQueries.push(searchToolQuery);
9600
- recentSuccessfulSearchToolByQuery.set(searchToolQuery, tc.name);
9601
- if (recentSuccessfulSearchQueries.length > 4) {
9602
- const dropped = recentSuccessfulSearchQueries.shift();
9603
- if (dropped) {
9604
- recentSuccessfulSearchToolByQuery.delete(dropped);
9605
- }
9606
- }
9607
- }
9608
- if (tc.name === "web_search" && searchToolQuery) {
9609
- lastSuccessfulWebSearchQuery = searchToolQuery;
9610
- }
9611
9656
  }
9657
+ searchLoopGuard.recordSuccess(
9658
+ tc.name,
9659
+ searchToolQuery,
9660
+ toolSucceeded
9661
+ );
9612
9662
  recentToolNames.push(tc.name);
9613
9663
  if (recentToolNames.length > 8) recentToolNames.shift();
9614
9664
  if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
@@ -10138,59 +10188,13 @@ function previewToolResult(text, maxLength = 800) {
10138
10188
  if (normalized.length <= maxLength) return normalized;
10139
10189
  return `${normalized.slice(0, maxLength)}...`;
10140
10190
  }
10141
- function normalizedSearchToolQuery(name, args) {
10142
- if (name !== "search" && name !== "web_search") return null;
10143
- const raw = typeof args.query === "string" ? args.query : typeof args.text === "string" ? args.text : typeof args.term === "string" ? args.term : "";
10144
- const normalized = raw.replace(/\s+/g, " ").trim().toLowerCase();
10145
- return normalized || null;
10146
- }
10147
10191
  function hasBlockingOverlaySignal(text) {
10148
10192
  if (!text) return false;
10149
10193
  if (/\bno blocking overlays detected\b/i.test(text)) return false;
10150
10194
  return /\bwarning:\s*blocking overlay detected\b/i.test(text) || /\bblocked-by-overlay\b/i.test(text) || /###\s*immediate blockers\b/i.test(text) || /\bblocking overlays\W+[1-9]\d*\b/i.test(text);
10151
10195
  }
10152
- function buildCodexLatestStateReminder(toolResultPreview) {
10153
- const text = (toolResultPreview || "").trim();
10154
- if (!text) return "";
10155
- const existingReminder = text.match(
10156
- /\bLatest browser state:\s*URL\s+.+?(?:Trust the latest tool result over the initial page context\.|$)/i
10157
- )?.[0]?.trim();
10158
- if (existingReminder) return existingReminder;
10159
- const stateMatch = text.match(
10160
- /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
10161
- );
10162
- if (stateMatch) {
10163
- const url = stateMatch[1]?.trim();
10164
- const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
10165
- if (url) {
10166
- return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
10167
- }
10168
- }
10169
- const navigatedUrl = text.match(/\b(?:navigated to|went back to|went forward to)\s+([^\s\n]+)/i)?.[1]?.trim() ?? text.match(/\b(?:web\s+)?searched "[^"]+"[^\n]*?(?:->|→)\s+([^\s\n]+)/i)?.[1]?.trim();
10170
- const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
10171
- if (navigatedUrl) {
10172
- return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
10173
- }
10174
- return "";
10175
- }
10176
- function buildCodexRepeatedSearchError(previousTool, previousQuery, latestToolResultPreview, mode) {
10177
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10178
- const header = mode === "drifted" ? `Error: You already performed ${previousTool} successfully for this task.` : `Error: You already searched for "${previousQuery}" successfully with ${previousTool}.`;
10179
- const lines = [
10180
- header,
10181
- mode === "drifted" ? `Do not rewrite or broaden the query with another ${previousTool}. The latest results from your previous ${previousTool} are already in the conversation context.` : `Do not search the same query again with ${previousTool} (or its alias search/web_search). The latest results from your previous ${previousTool} are already in the conversation context.`,
10182
- // The key change: do NOT suggest read_page as a "recovery" action.
10183
- // The model was using read_page as a no-op to reset the strike counter
10184
- // and then issue another web_search. The prior results are sufficient.
10185
- `Take the next action from the results you already have: click a result link, call inspect_element on a specific item, highlight, or provide the final answer to the user. Do not call any search tool again, and do not call read_page as preparation for another search.`
10186
- ];
10187
- if (stateReminder) {
10188
- lines.push(stateReminder);
10189
- }
10190
- return lines.join(" ");
10191
- }
10192
10196
  function buildCodexUnsupportedClearOverlayError(latestToolResultPreview) {
10193
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10197
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10194
10198
  const lines = [
10195
10199
  `Error: No blocking overlay signal is present in the latest browser state.`,
10196
10200
  `Do not call clear_overlays unless read_page or the page context explicitly reports a blocking overlay.`,
@@ -10310,7 +10314,7 @@ function shouldRetryCodexToolLoop(text, hasToolHistory) {
10310
10314
  return false;
10311
10315
  }
10312
10316
  function buildCodexRecoveryInput(userMessage, assistantText, latestToolResultPreview) {
10313
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10317
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10314
10318
  const lines = [
10315
10319
  `[System] The task is still in progress: ${userMessage}`,
10316
10320
  `Do not ask the user what they want next unless the original request is genuinely ambiguous or blocked.`,
@@ -10343,7 +10347,7 @@ function buildCodexFlightPriceEvidenceRecoveryInput(userMessage, assistantText,
10343
10347
  };
10344
10348
  }
10345
10349
  function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1) {
10346
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10350
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10347
10351
  const lines = [
10348
10352
  `[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`,
10349
10353
  `Take the next step yourself: try a different target, refresh the page state with read_page, call inspect_element on the intended element, or answer from the results already visible in the conversation. Do not ask the user to inspect or click the result for you.`
@@ -10562,9 +10566,7 @@ class CodexProvider {
10562
10566
  let clickReadLoopNudged = false;
10563
10567
  let latestToolResultPreview = null;
10564
10568
  let failedClickCountSinceProgress = 0;
10565
- const recentSuccessfulSearchQueries = [];
10566
- const recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
10567
- let lastSuccessfulWebSearchQuery = null;
10569
+ const searchLoopGuard = new SearchLoopGuard(isRealProgressTool);
10568
10570
  try {
10569
10571
  for (let i = 0; i < maxIterations; i++) {
10570
10572
  iterationsUsed = i + 1;
@@ -10654,30 +10656,29 @@ class CodexProvider {
10654
10656
  prepared.prepared.name,
10655
10657
  prepared.prepared.args
10656
10658
  );
10657
- const searchToolQuery = normalizedSearchToolQuery(
10659
+ const searchToolQuery = normalizeSearchToolQuery(
10658
10660
  prepared.prepared.name,
10659
10661
  prepared.prepared.args
10660
10662
  );
10661
- const isRepeatedSearchAcrossTools = searchToolQuery !== null && recentSuccessfulSearchQueries.includes(searchToolQuery);
10662
- const isQueryDriftedWebSearch = prepared.prepared.name === "web_search" && lastSuccessfulWebSearchQuery !== null && searchToolQuery !== null && searchToolQuery !== lastSuccessfulWebSearchQuery;
10663
+ const searchLoopCheck = searchLoopGuard.check(
10664
+ prepared.prepared.name,
10665
+ searchToolQuery
10666
+ );
10663
10667
  const isUnsupportedClearOverlay = prepared.prepared.name === "clear_overlays" && !hasBlockingOverlaySignal(
10664
10668
  `${systemPrompt}
10665
10669
  ${latestToolResultPreview || ""}`
10666
10670
  );
10667
- if (isRepeatedSearchAcrossTools || isQueryDriftedWebSearch) {
10671
+ if (searchLoopCheck) {
10668
10672
  onChunk(`
10669
10673
  <<tool:${prepared.prepared.name}:↻ duplicate suppressed>>
10670
10674
  `);
10671
- const previousTool = isRepeatedSearchAcrossTools ? recentSuccessfulSearchToolByQuery.get(searchToolQuery ?? "") ?? (prepared.prepared.name === "web_search" ? "search" : "web_search") : "web_search";
10672
- const previousQuery = isRepeatedSearchAcrossTools ? searchToolQuery ?? "" : lastSuccessfulWebSearchQuery ?? "";
10673
- const mode = isRepeatedSearchAcrossTools ? "repeated" : "drifted";
10674
10675
  const output2 = createCodexToolOutput(
10675
10676
  prepared.prepared.callId,
10676
- buildCodexRepeatedSearchError(
10677
- previousTool,
10678
- previousQuery,
10677
+ buildRepeatedSearchError(
10678
+ searchLoopCheck.previousTool,
10679
+ searchLoopCheck.previousQuery,
10679
10680
  latestToolResultPreview,
10680
- mode
10681
+ searchLoopCheck.mode
10681
10682
  )
10682
10683
  );
10683
10684
  currentInput.push(output2);
@@ -10731,30 +10732,15 @@ ${latestToolResultPreview || ""}`
10731
10732
  toolHistoryCount += 1;
10732
10733
  latestToolResultPreview = previewToolResult(output.output);
10733
10734
  const outputText = toolResultTextContent(output.output);
10734
- if (!looksLikeFailedToolOutput(outputText)) {
10735
- if (isRealProgressTool(prepared.prepared.name)) {
10736
- lastSuccessfulWebSearchQuery = null;
10737
- failedClickCountSinceProgress = 0;
10738
- }
10739
- }
10740
- if (searchToolQuery && !looksLikeFailedToolOutput(outputText) && !recentSuccessfulSearchQueries.includes(searchToolQuery)) {
10741
- recentSuccessfulSearchQueries.push(searchToolQuery);
10742
- recentSuccessfulSearchToolByQuery.set(
10743
- searchToolQuery,
10744
- prepared.prepared.name
10745
- );
10746
- if (recentSuccessfulSearchQueries.length > 4) {
10747
- const dropped = recentSuccessfulSearchQueries.shift();
10748
- if (dropped) {
10749
- recentSuccessfulSearchToolByQuery.delete(dropped);
10750
- }
10751
- }
10752
- }
10753
- if (prepared.prepared.name === "web_search" && !looksLikeFailedToolOutput(outputText)) {
10754
- if (searchToolQuery) {
10755
- lastSuccessfulWebSearchQuery = searchToolQuery;
10756
- }
10735
+ const toolSucceeded = !looksLikeFailedToolOutput(outputText);
10736
+ if (toolSucceeded && isRealProgressTool(prepared.prepared.name)) {
10737
+ failedClickCountSinceProgress = 0;
10757
10738
  }
10739
+ searchLoopGuard.recordSuccess(
10740
+ prepared.prepared.name,
10741
+ searchToolQuery,
10742
+ toolSucceeded
10743
+ );
10758
10744
  if (prepared.prepared.name === "click" && looksLikeFailedToolOutput(outputText)) {
10759
10745
  failedClickCountSinceProgress += 1;
10760
10746
  currentInput.push(
@@ -12711,17 +12697,23 @@ function formatCartSnapshot(page) {
12711
12697
  function isVisibleToUser(el) {
12712
12698
  return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
12713
12699
  }
12700
+ function elementSearchText(el, extraFields = []) {
12701
+ const fields = [
12702
+ el.text,
12703
+ el.label,
12704
+ el.name,
12705
+ el.placeholder,
12706
+ el.description,
12707
+ el.href,
12708
+ ...extraFields.map((field) => el[field])
12709
+ ];
12710
+ return normalizeComparable(fields.filter(Boolean).join(" "));
12711
+ }
12712
+ function formatElementOptions(options, maxOptions) {
12713
+ return options?.slice(0, maxOptions).map((o) => typeof o === "string" ? o : o.label || o.value).join("|") ?? "";
12714
+ }
12714
12715
  function purchaseActionPriority(el) {
12715
- const haystack = normalizeComparable(
12716
- [
12717
- el.text,
12718
- el.label,
12719
- el.name,
12720
- el.placeholder,
12721
- el.description,
12722
- el.href
12723
- ].filter(Boolean).join(" ")
12724
- );
12716
+ const haystack = elementSearchText(el);
12725
12717
  if (!haystack) return Number.POSITIVE_INFINITY;
12726
12718
  if (/\badd(?: item)? to (?:cart|bag|basket)\b/.test(haystack)) return 0;
12727
12719
  if (/\b(?:buy now|preorder|pre-order|reserve now|shop now)\b/.test(haystack)) {
@@ -12733,17 +12725,7 @@ function purchaseActionPriority(el) {
12733
12725
  return Number.POSITIVE_INFINITY;
12734
12726
  }
12735
12727
  function dateOrShowtimeControlPriority(el) {
12736
- const haystack = normalizeComparable(
12737
- [
12738
- el.text,
12739
- el.label,
12740
- el.name,
12741
- el.placeholder,
12742
- el.description,
12743
- el.href,
12744
- el.role
12745
- ].filter(Boolean).join(" ")
12746
- );
12728
+ const haystack = elementSearchText(el, ["role"]);
12747
12729
  if (!haystack) return Number.POSITIVE_INFINITY;
12748
12730
  if (/\b(today|tomorrow|mon(?:day)?|tue(?:s|sday)?|wed(?:nesday)?|thu(?:rs|rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b/.test(
12749
12731
  haystack
@@ -12852,24 +12834,34 @@ function formatDialogFocus(page) {
12852
12834
  }
12853
12835
  function formatInteractiveElements(elements) {
12854
12836
  if (elements.length === 0) return "None";
12837
+ const DIALOG_PRIORITY_BONUS = 40;
12838
+ const HIDDEN_VISIBILITY_PENALTY = 100;
12839
+ const OFFSCREEN_PENALTY = 50;
12840
+ const NAVIGATION_CONTEXT_PENALTY = 30;
12841
+ const OBSCURED_PENALTY = 20;
12842
+ const LINK_TYPE_PENALTY = 5;
12843
+ const PURCHASE_BASE_WEIGHT = 25;
12844
+ const PURCHASE_PRIORITY_MULTIPLIER = 5;
12845
+ const DATE_BASE_WEIGHT = 18;
12846
+ const DATE_PRIORITY_MULTIPLIER = 4;
12855
12847
  const sorted = [...elements].sort((a, b) => {
12856
12848
  const scoreEl = (el) => {
12857
12849
  let s = 0;
12858
- if (el.context === "dialog") s -= 40;
12850
+ if (el.context === "dialog") s -= DIALOG_PRIORITY_BONUS;
12859
12851
  const purchasePriority = purchaseActionPriority(el);
12860
12852
  if (Number.isFinite(purchasePriority)) {
12861
- s -= 25 - purchasePriority * 5;
12853
+ s -= PURCHASE_BASE_WEIGHT - purchasePriority * PURCHASE_PRIORITY_MULTIPLIER;
12862
12854
  }
12863
12855
  const datePriority = dateOrShowtimeControlPriority(el);
12864
12856
  if (Number.isFinite(datePriority)) {
12865
- s -= 18 - datePriority * 4;
12857
+ s -= DATE_BASE_WEIGHT - datePriority * DATE_PRIORITY_MULTIPLIER;
12866
12858
  }
12867
- if (el.visible === false) s += 100;
12868
- if (el.inViewport === false) s += 50;
12859
+ if (el.visible === false) s += HIDDEN_VISIBILITY_PENALTY;
12860
+ if (el.inViewport === false) s += OFFSCREEN_PENALTY;
12869
12861
  if (el.context === "nav" || el.context === "footer" || el.context === "sidebar")
12870
- s += 30;
12871
- if (el.obscured) s += 20;
12872
- if (el.type === "link") s += 5;
12862
+ s += NAVIGATION_CONTEXT_PENALTY;
12863
+ if (el.obscured) s += OBSCURED_PENALTY;
12864
+ if (el.type === "link") s += LINK_TYPE_PENALTY;
12873
12865
  return s;
12874
12866
  };
12875
12867
  return scoreEl(a) - scoreEl(b);
@@ -12901,9 +12893,7 @@ function formatInteractiveElements(elements) {
12901
12893
  appendFieldAffordances(parts, el);
12902
12894
  if (el.options?.length) {
12903
12895
  const maxOptions = isDateOrShowtimeControl(el) ? 10 : 5;
12904
- parts.push(
12905
- `options=${el.options.slice(0, maxOptions).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
12906
- );
12896
+ parts.push(`options=${formatElementOptions(el.options, maxOptions)}`);
12907
12897
  }
12908
12898
  } else if (el.type === "textarea") {
12909
12899
  parts.push(`[${el.label || "Text Area"}]`);
@@ -12966,7 +12956,7 @@ function formatForms(forms) {
12966
12956
  if (field.options?.length) {
12967
12957
  const maxOptions = isDateOrShowtimeControl(field) ? 10 : 5;
12968
12958
  fieldParts.push(
12969
- `options=${field.options.slice(0, maxOptions).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
12959
+ `options=${formatElementOptions(field.options, maxOptions)}`
12970
12960
  );
12971
12961
  }
12972
12962
  } else if (field.type === "textarea") {
@@ -13231,26 +13221,10 @@ function chooseAgentReadMode(page) {
13231
13221
  return "visible_only";
13232
13222
  }
13233
13223
  }
13234
- function isSearchOrListingPage(page) {
13235
- if (isHackerNewsListingPage(page.url)) return true;
13236
- const haystack = normalizeComparable(
13237
- [
13238
- page.url,
13239
- page.title,
13240
- page.excerpt,
13241
- page.headings.map((heading) => heading.text).join(" ")
13242
- ].filter(Boolean).join(" ")
13243
- );
13244
- return /\b(search|results|find|discover|browse|repositories|repository|issues|pull requests|prs|users|events|listings)\b/.test(
13245
- haystack
13246
- );
13247
- }
13248
- function isHackerNewsListingPage(url) {
13249
- try {
13250
- const parsed = new URL(url);
13251
- if (parsed.hostname !== "news.ycombinator.com") return false;
13252
- const pathname = parsed.pathname.replace(/\/+$/, "") || "/";
13253
- return [
13224
+ const SITE_RESULT_FILTERS = [
13225
+ {
13226
+ hostname: "news.ycombinator.com",
13227
+ listingPaths: [
13254
13228
  "/",
13255
13229
  "/news",
13256
13230
  "/newest",
@@ -13262,30 +13236,62 @@ function isHackerNewsListingPage(url) {
13262
13236
  "/active",
13263
13237
  "/classic",
13264
13238
  "/noobstories"
13265
- ].includes(pathname);
13266
- } catch {
13267
- return false;
13239
+ ],
13240
+ utilityPathnames: ["/hide", "/user"],
13241
+ utilityTextPatterns: [
13242
+ /^(hide|past|favorite|unfavorite|flag|unflag|discuss|reply|parent|more)$/,
13243
+ /^\d+\s+(?:comments?|points?)$/
13244
+ ]
13268
13245
  }
13269
- }
13270
- function isHackerNewsUtilityLink(element) {
13271
- if (!element.href) return false;
13272
- let url;
13246
+ ];
13247
+ function matchesSiteFilter(url, filter, baseHostname) {
13273
13248
  try {
13274
- url = new URL(element.href);
13249
+ const parsed = new URL(url, baseHostname ? `https://${baseHostname}` : void 0);
13250
+ return parsed.hostname === filter.hostname;
13275
13251
  } catch {
13276
13252
  return false;
13277
13253
  }
13278
- if (url.hostname !== "news.ycombinator.com") return false;
13279
- const text = normalizeComparable(element.text || "");
13280
- const pathname = url.pathname.replace(/\/+$/, "") || "/";
13281
- if (/^(hide|past|favorite|unfavorite|flag|unflag|discuss|reply|parent|more)$/.test(
13282
- text
13283
- )) {
13284
- return true;
13254
+ }
13255
+ function isSiteListingPage(url) {
13256
+ for (const filter of SITE_RESULT_FILTERS) {
13257
+ if (!matchesSiteFilter(url, filter, "")) continue;
13258
+ try {
13259
+ const pathname = new URL(url).pathname.replace(/\/+$/, "") || "/";
13260
+ if (filter.listingPaths?.includes(pathname)) return true;
13261
+ } catch {
13262
+ }
13263
+ }
13264
+ return false;
13265
+ }
13266
+ function isSiteUtilityLink(element) {
13267
+ if (!element.href) return false;
13268
+ for (const filter of SITE_RESULT_FILTERS) {
13269
+ if (!matchesSiteFilter(element.href, filter, "")) continue;
13270
+ const text = normalizeComparable(element.text || "");
13271
+ for (const pattern of filter.utilityTextPatterns ?? []) {
13272
+ if (pattern.test(text)) return true;
13273
+ }
13274
+ try {
13275
+ const pathname = new URL(element.href).pathname.replace(/\/+$/, "") || "/";
13276
+ if (filter.utilityPathnames?.includes(pathname)) return true;
13277
+ } catch {
13278
+ }
13285
13279
  }
13286
- if (/^\d+\s+(?:comments?|points?)$/.test(text)) return true;
13287
- if (pathname === "/hide" || pathname === "/user") return true;
13288
- return pathname === "/item" && /^(?:discuss|\d+\s+comments?)$/.test(text);
13280
+ return false;
13281
+ }
13282
+ function isSearchOrListingPage(page) {
13283
+ if (isSiteListingPage(page.url)) return true;
13284
+ const haystack = normalizeComparable(
13285
+ [
13286
+ page.url,
13287
+ page.title,
13288
+ page.excerpt,
13289
+ page.headings.map((heading) => heading.text).join(" ")
13290
+ ].filter(Boolean).join(" ")
13291
+ );
13292
+ return /\b(search|results|find|discover|browse|repositories|repository|issues|pull requests|prs|users|events|listings)\b/.test(
13293
+ haystack
13294
+ );
13289
13295
  }
13290
13296
  function collectJsonLdEntityItems(input, results = []) {
13291
13297
  if (!input) return results;
@@ -13326,7 +13332,7 @@ function getResultCandidates(page) {
13326
13332
  const pageHost = normalizeUrlForMatch(page.url);
13327
13333
  const searchOrListingPage = isSearchOrListingPage(page);
13328
13334
  const scored = page.interactiveElements.filter(
13329
- (element) => element.type === "link" && element.text?.trim() && element.href && !isHackerNewsUtilityLink(element)
13335
+ (element) => element.type === "link" && element.text?.trim() && element.href && !isSiteUtilityLink(element)
13330
13336
  ).map((element) => {
13331
13337
  const text = element.text?.trim() || "";
13332
13338
  const comparableText = normalizeComparable(text);
@@ -13786,7 +13792,7 @@ function detectPageType(page) {
13786
13792
  if (hasResults && hasSearchInput && listingLike) return "SEARCH_RESULTS";
13787
13793
  if (hasCart) return "SHOPPING";
13788
13794
  if (formCount > 0 && !hasPasswordField) return "FORM";
13789
- if (isHackerNewsListingPage(page.url)) return "PAGINATED_LIST";
13795
+ if (isSiteListingPage(page.url)) return "PAGINATED_LIST";
13790
13796
  if (hasPagination && listingLike) return "PAGINATED_LIST";
13791
13797
  if (hasSearchInput && !listingLike) return "SEARCH_READY";
13792
13798
  if (page.content.length > 3e3 && page.interactiveElements.length < 10)
@@ -3085,7 +3085,7 @@ function buildBaseMetadata(el) {
3085
3085
  }
3086
3086
  function isNavigableEmbeddedSrc(src) {
3087
3087
  const normalized = src.trim().toLowerCase();
3088
- return Boolean(normalized) && !/^(about:blank|javascript:|data:)/.test(normalized);
3088
+ return Boolean(normalized) && !/^(about:blank|javascript:|data:|blob:|file:)/i.test(normalized);
3089
3089
  }
3090
3090
  function getEmbeddedFrameLabel(iframe) {
3091
3091
  const explicitLabel = getTrimmedText(iframe.getAttribute("title")) || getTrimmedText(iframe.getAttribute("aria-label")) || getTrimmedText(iframe.name) || getTrimmedText(iframe.id);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quanta-intellect/vessel-browser",
3
3
  "mcpName": "io.github.unmodeled-tyler/vessel-browser",
4
- "version": "0.1.143",
4
+ "version": "0.1.144",
5
5
  "description": "AI-native web browser runtime for autonomous agents with human supervision",
6
6
  "main": "./out/main/index.js",
7
7
  "bin": {