@quanta-intellect/vessel-browser 0.1.143 → 0.1.145

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
@@ -1467,7 +1467,12 @@ function loadJsonFile({
1467
1467
  const decoded = decodeStoredData(raw, secure);
1468
1468
  return parse(JSON.parse(decoded));
1469
1469
  } catch (err) {
1470
- logger$A.warn(`Failed to load ${filePath2}, using fallback:`, err);
1470
+ const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
1471
+ if (isMissingFile) {
1472
+ logger$A.info(`Persistence file not found; using fallback defaults: ${filePath2}`);
1473
+ } else {
1474
+ logger$A.warn(`Failed to load ${filePath2}, using fallback:`, err);
1475
+ }
1471
1476
  return fallback;
1472
1477
  }
1473
1478
  }
@@ -3867,7 +3872,14 @@ const AIChannels = {
3867
3872
  AGENT_CHECKPOINT_UPDATE_NOTE: "agent:checkpoint-update-note",
3868
3873
  AGENT_UNDO_LAST_ACTION: "agent:undo-last-action",
3869
3874
  AGENT_SESSION_CAPTURE: "agent:session-capture",
3870
- AGENT_SESSION_RESTORE: "agent:session-restore"
3875
+ AGENT_SESSION_RESTORE: "agent:session-restore",
3876
+ AGENT_TASK_START: "agent:task-start",
3877
+ AGENT_TASK_UPDATE: "agent:task-update",
3878
+ AGENT_TASK_NOTE: "agent:task-note",
3879
+ AGENT_TASK_BLOCKER: "agent:task-blocker",
3880
+ AGENT_TASK_RESOLVE: "agent:task-resolve",
3881
+ AGENT_TASK_ABANDON: "agent:task-abandon",
3882
+ AGENT_TASK_CLEAR: "agent:task-clear"
3871
3883
  };
3872
3884
  const AutofillChannels = {
3873
3885
  AUTOFILL_LIST: "autofill:list",
@@ -8683,6 +8695,106 @@ function recoverTextEncodedToolCalls(text, availableToolNames) {
8683
8695
  }
8684
8696
  return recovered;
8685
8697
  }
8698
+ function findInlineToolMarkerBodies(text) {
8699
+ const bodies = [];
8700
+ const lowerText = text.toLowerCase();
8701
+ let searchIndex = 0;
8702
+ while (searchIndex < text.length) {
8703
+ const start = lowerText.indexOf("<<tool", searchIndex);
8704
+ if (start === -1) break;
8705
+ let quote = null;
8706
+ let escaped = false;
8707
+ let end = -1;
8708
+ for (let index = start + 2; index < text.length - 1; index += 1) {
8709
+ const char = text[index];
8710
+ if (quote) {
8711
+ if (escaped) {
8712
+ escaped = false;
8713
+ } else if (char === "\\") {
8714
+ escaped = true;
8715
+ } else if (char === quote) {
8716
+ quote = null;
8717
+ }
8718
+ continue;
8719
+ }
8720
+ if (char === '"' || char === "'") {
8721
+ quote = char;
8722
+ continue;
8723
+ }
8724
+ if (char === ">" && text[index + 1] === ">") {
8725
+ end = index;
8726
+ break;
8727
+ }
8728
+ }
8729
+ if (end === -1) {
8730
+ searchIndex = start + 2;
8731
+ continue;
8732
+ }
8733
+ bodies.push(text.slice(start + 2, end));
8734
+ searchIndex = end + 2;
8735
+ }
8736
+ return bodies;
8737
+ }
8738
+ function recoverInlineToolMarkerToolCalls(text, availableToolNames) {
8739
+ const recovered = [];
8740
+ for (const markerBody of findInlineToolMarkerBodies(text)) {
8741
+ const match = markerBody.trim().match(/^tool[:=]([a-z_][a-z0-9_]*)(?:[:=]([\s\S]*))?$/i);
8742
+ if (!match) continue;
8743
+ const rawName = match[1] ?? "";
8744
+ const rawArgs = (match[2] ?? "").trim();
8745
+ const resolvedName = resolveToolCallName(
8746
+ rawName,
8747
+ {},
8748
+ availableToolNames
8749
+ );
8750
+ if (!availableToolNames.has(resolvedName)) continue;
8751
+ let parsedArgs = null;
8752
+ if (rawArgs.startsWith("{")) {
8753
+ const repaired = parseToolArgsWithRepair(resolvedName, rawArgs);
8754
+ if (repaired) {
8755
+ parsedArgs = repaired.args;
8756
+ }
8757
+ }
8758
+ if (!parsedArgs) {
8759
+ const kvArgs = {};
8760
+ const kvPattern = /([a-z_][a-z0-9_]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^,\s]*))/gi;
8761
+ let kvMatch;
8762
+ while ((kvMatch = kvPattern.exec(rawArgs)) !== null) {
8763
+ const key2 = kvMatch[1];
8764
+ const value = kvMatch[2] ?? kvMatch[3] ?? kvMatch[4] ?? "";
8765
+ kvArgs[key2] = value;
8766
+ }
8767
+ if (Object.keys(kvArgs).length > 0) {
8768
+ parsedArgs = kvArgs;
8769
+ }
8770
+ }
8771
+ if (!parsedArgs && rawArgs) {
8772
+ const repaired = parseToolArgsWithRepair(resolvedName, rawArgs);
8773
+ if (repaired) {
8774
+ parsedArgs = repaired.args;
8775
+ }
8776
+ }
8777
+ if (!parsedArgs) {
8778
+ parsedArgs = {};
8779
+ }
8780
+ recovered.push({
8781
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8782
+ name: resolvedName,
8783
+ argsJson: JSON.stringify(parsedArgs)
8784
+ });
8785
+ }
8786
+ return recovered;
8787
+ }
8788
+ function recoverAssistantTextToolCalls(text, availableToolNames) {
8789
+ const textEncodedCalls = recoverTextEncodedToolCalls(text, availableToolNames);
8790
+ if (textEncodedCalls.length > 0) return textEncodedCalls;
8791
+ const inlineMarkerCalls = recoverInlineToolMarkerToolCalls(
8792
+ text,
8793
+ availableToolNames
8794
+ );
8795
+ if (inlineMarkerCalls.length > 0) return inlineMarkerCalls;
8796
+ return recoverNarratedActionToolCalls(text, availableToolNames);
8797
+ }
8686
8798
  function recoverNarratedActionToolCalls(text, availableToolNames) {
8687
8799
  const trimmed = text.trim();
8688
8800
  if (!trimmed) return [];
@@ -8817,6 +8929,106 @@ function buildFlightPriceEvidenceRecoveryPrompt(userMessage, assistantText, late
8817
8929
  `Last unsupported answer: ${assistantText.replace(/\s+/g, " ").trim().slice(0, 500) || "(empty)"}`
8818
8930
  ].join("\n");
8819
8931
  }
8932
+ const SEARCH_HISTORY_LIMIT = 4;
8933
+ function normalizeSearchToolQuery(name, args) {
8934
+ if (name !== "search" && name !== "web_search") return null;
8935
+ const raw = typeof args.query === "string" ? args.query : typeof args.text === "string" ? args.text : typeof args.term === "string" ? args.term : "";
8936
+ const normalized = raw.replace(/\s+/g, " ").trim().toLowerCase();
8937
+ return normalized || null;
8938
+ }
8939
+ function buildLatestStateReminder(toolResultPreview) {
8940
+ const text = (toolResultPreview || "").trim();
8941
+ if (!text) return "";
8942
+ const existingReminder = text.match(
8943
+ /\bLatest browser state:\s*URL\s+.+?(?:Trust the latest tool result over the initial page context\.|$)/i
8944
+ )?.[0]?.trim();
8945
+ if (existingReminder) return existingReminder;
8946
+ const stateMatch = text.match(
8947
+ /\[state:\s+url=([^,\]\n]+),\s+title=(?:"([^"]*)"|([^,\]\n]+))/i
8948
+ );
8949
+ if (stateMatch) {
8950
+ const url = stateMatch[1]?.trim();
8951
+ const title = (stateMatch[2] ?? stateMatch[3] ?? "").trim();
8952
+ if (url) {
8953
+ return `Latest browser state: URL ${url}${title ? `, title "${title}"` : ""}. Trust the latest tool result over the initial page context.`;
8954
+ }
8955
+ }
8956
+ const structuredUrl = text.match(/\*\*URL:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8957
+ const structuredTitle = text.match(/\*\*Title:\*\*\s*([^\n]+)/i)?.[1]?.trim();
8958
+ if (structuredUrl) {
8959
+ return `Latest browser state: URL ${structuredUrl}${structuredTitle ? `, title "${structuredTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8960
+ }
8961
+ const navigatedUrl = text.match(
8962
+ /\b(?:navigated to|went back to|went forward to|searched "[^"]+"(?: \(via search button\))? →)\s+([^\s\n]+)/i
8963
+ )?.[1]?.trim() ?? text.match(
8964
+ /\b(?:web\s+)?searched "[^"]+"[^\n]*?(?:->|→)\s+([^\s\n]+)/i
8965
+ )?.[1]?.trim();
8966
+ const pageTitle = text.match(/\bPage title:\s*([^\n]+)/i)?.[1]?.trim();
8967
+ if (navigatedUrl) {
8968
+ return `Latest browser state: URL ${navigatedUrl}${pageTitle ? `, title "${pageTitle}"` : ""}. Trust the latest tool result over the initial page context.`;
8969
+ }
8970
+ return "";
8971
+ }
8972
+ function buildRepeatedSearchError(previousTool, previousQuery, latestToolResultPreview, mode) {
8973
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8974
+ const lines = [
8975
+ mode === "drifted" ? `Error: You already performed ${previousTool} successfully for this task.` : `Error: You already searched for "${previousQuery}" successfully with ${previousTool}.`,
8976
+ 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.`,
8977
+ `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.`,
8978
+ `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.`
8979
+ ];
8980
+ if (stateReminder) {
8981
+ lines.push(stateReminder);
8982
+ }
8983
+ return lines.join(" ");
8984
+ }
8985
+ class SearchLoopGuard {
8986
+ recentSuccessfulSearchQueries = [];
8987
+ recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
8988
+ lastSuccessfulWebSearchQuery = null;
8989
+ isContextResettingTool;
8990
+ constructor(isContextResettingTool) {
8991
+ this.isContextResettingTool = isContextResettingTool;
8992
+ }
8993
+ /**
8994
+ * Check whether a search/web_search call should be suppressed. Returns the
8995
+ * details needed to build the corrective error, or null if the call is OK.
8996
+ */
8997
+ check(toolName, query) {
8998
+ const isRepeatedSearchAcrossTools = query !== null && this.recentSuccessfulSearchQueries.includes(query);
8999
+ const isQueryDriftedWebSearch = toolName === "web_search" && this.lastSuccessfulWebSearchQuery !== null && query !== null && query !== this.lastSuccessfulWebSearchQuery;
9000
+ if (!isRepeatedSearchAcrossTools && !isQueryDriftedWebSearch) return null;
9001
+ const mode = isRepeatedSearchAcrossTools ? "repeated" : "drifted";
9002
+ const previousTool = isRepeatedSearchAcrossTools ? this.recentSuccessfulSearchToolByQuery.get(query ?? "") ?? (toolName === "web_search" ? "search" : "web_search") : "web_search";
9003
+ const previousQuery = isRepeatedSearchAcrossTools ? query ?? "" : this.lastSuccessfulWebSearchQuery ?? "";
9004
+ return { mode, previousTool, previousQuery };
9005
+ }
9006
+ /**
9007
+ * Record a successfully executed tool. Search queries are added to the
9008
+ * recent-history ring buffer, and real-progress tools clear the drift anchor
9009
+ * so a later distinct search is not flagged as drift.
9010
+ */
9011
+ recordSuccess(toolName, query, wasSuccessful) {
9012
+ if (wasSuccessful && this.isContextResettingTool(toolName)) {
9013
+ this.lastSuccessfulWebSearchQuery = null;
9014
+ }
9015
+ if (wasSuccessful && query) {
9016
+ if (!this.recentSuccessfulSearchQueries.includes(query)) {
9017
+ this.recentSuccessfulSearchQueries.push(query);
9018
+ this.recentSuccessfulSearchToolByQuery.set(query, toolName);
9019
+ if (this.recentSuccessfulSearchQueries.length > SEARCH_HISTORY_LIMIT) {
9020
+ const dropped = this.recentSuccessfulSearchQueries.shift();
9021
+ if (dropped) {
9022
+ this.recentSuccessfulSearchToolByQuery.delete(dropped);
9023
+ }
9024
+ }
9025
+ }
9026
+ }
9027
+ if (wasSuccessful && toolName === "web_search" && query) {
9028
+ this.lastSuccessfulWebSearchQuery = query;
9029
+ }
9030
+ }
9031
+ }
8820
9032
  const logger$v = createLogger("OpenAIProvider");
8821
9033
  function shouldDebugAgentLoop() {
8822
9034
  const value = process.env.VESSEL_DEBUG_AGENT_LOOP;
@@ -8886,7 +9098,7 @@ function buildOpenRouterAttributionHeaders() {
8886
9098
  function followUpReminderForProfile(profile, userMessage, assistantText, latestToolResultPreview) {
8887
9099
  if (profile !== "compact") return null;
8888
9100
  const phaseReminder = buildPhaseReminder(userMessage, assistantText || "");
8889
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
9101
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8890
9102
  return {
8891
9103
  role: "user",
8892
9104
  content: `[System] Task reminder: Continue working on the user's original request until it is completed: ${userMessage}
@@ -8896,13 +9108,13 @@ ${phaseReminder}` : "")
8896
9108
  };
8897
9109
  }
8898
9110
  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);
9111
+ const matches = goal.toLowerCase().match(/\b(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.[a-z]{2,})\b/g);
8900
9112
  if (!matches || matches.length !== 1) return null;
8901
9113
  return matches[0].replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
8902
9114
  }
8903
9115
  function buildCompactRecoveryPrompt(userMessage, assistantText, latestToolResultPreview) {
8904
9116
  const phaseReminder = buildPhaseReminder(userMessage, assistantText);
8905
- const stateReminder = buildLatestStateReminder(latestToolResultPreview || "");
9117
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
8906
9118
  const goalDomain = extractSingleGoalDomain(userMessage);
8907
9119
  const latest = (latestToolResultPreview || "").toLowerCase();
8908
9120
  const assistant = assistantText.toLowerCase();
@@ -8999,38 +9211,7 @@ function buildPhaseReminder(userMessage, assistantText) {
8999
9211
  }
9000
9212
  return "";
9001
9213
  }
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) {
9214
+ function isSearchContextResettingTool(name) {
9034
9215
  return ![
9035
9216
  "read_page",
9036
9217
  "current_tab",
@@ -9043,18 +9224,6 @@ function isOpenAIRealProgressToolForSearch(name) {
9043
9224
  "search"
9044
9225
  ].includes(name);
9045
9226
  }
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
9227
  function shouldRecoverCompactStall(text, userMessage) {
9059
9228
  const trimmed = text.trim().toLowerCase();
9060
9229
  if (!trimmed) return true;
@@ -9138,15 +9307,24 @@ function logAgentLoopDebug(payload) {
9138
9307
  }
9139
9308
  }
9140
9309
  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(
9310
+ 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
9311
  message
9143
9312
  )) {
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.`;
9313
+ return [
9314
+ message,
9315
+ "OpenRouter reported an upstream model timeout/no-content failure.",
9316
+ "If this persists, retry or pin a specific low-latency tool-calling model instead of the free router."
9317
+ ].join(" ");
9145
9318
  }
9146
9319
  if (providerId === "llama_cpp" && /(available context size|context size exceeded|exceeds the available context size|try increasing it)/i.test(
9147
9320
  message
9148
9321
  )) {
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).`;
9322
+ return [
9323
+ message,
9324
+ "llama.cpp sets context size at server startup, not per request.",
9325
+ `Vessel's agent prompt plus tool schema is about 6.5k tokens before browsing history, so run llama-server with`,
9326
+ `--ctx-size ${LLAMA_CPP_MIN_CTX_TOKENS} minimum (${LLAMA_CPP_RECOMMENDED_CTX_TOKENS} recommended).`
9327
+ ].join(" ");
9150
9328
  }
9151
9329
  return message;
9152
9330
  }
@@ -9256,9 +9434,7 @@ class OpenAICompatProvider {
9256
9434
  const recentCompactToolSignatures = [];
9257
9435
  const recentToolNames = [];
9258
9436
  const successfulToolNames = [];
9259
- const recentSuccessfulSearchQueries = [];
9260
- const recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
9261
- let lastSuccessfulWebSearchQuery = null;
9437
+ const searchLoopGuard = new SearchLoopGuard(isSearchContextResettingTool);
9262
9438
  let clickReadLoopNudged = false;
9263
9439
  for (let i = 0; i < maxIterations; i++) {
9264
9440
  iterationsUsed = i + 1;
@@ -9334,22 +9510,13 @@ class OpenAICompatProvider {
9334
9510
  tc.name = resolveToolCallName(tc.name, parsedArgs, availableToolNames);
9335
9511
  }
9336
9512
  if (toolCalls.length === 0) {
9337
- const recoveredToolCalls = recoverTextEncodedToolCalls(
9513
+ const recoveredToolCalls = recoverAssistantTextToolCalls(
9338
9514
  textAccum,
9339
9515
  availableToolNames
9340
9516
  );
9341
9517
  if (recoveredToolCalls.length > 0) {
9342
9518
  toolCalls = recoveredToolCalls;
9343
9519
  if (textAccum.trim()) onChunk("<<erase_prev>>");
9344
- } else {
9345
- const narratedToolCalls = recoverNarratedActionToolCalls(
9346
- textAccum,
9347
- availableToolNames
9348
- );
9349
- if (narratedToolCalls.length > 0) {
9350
- toolCalls = narratedToolCalls;
9351
- if (textAccum.trim()) onChunk("<<erase_prev>>");
9352
- }
9353
9520
  }
9354
9521
  }
9355
9522
  logAgentLoopDebug({
@@ -9493,24 +9660,20 @@ class OpenAICompatProvider {
9493
9660
  args = repairedArgs.args;
9494
9661
  args = coerceToolArgsForExecution(tc.name, args);
9495
9662
  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) {
9663
+ const searchToolQuery = normalizeSearchToolQuery(tc.name, args);
9664
+ const searchLoopCheck = searchLoopGuard.check(tc.name, searchToolQuery);
9665
+ if (searchLoopCheck) {
9500
9666
  onChunk(`
9501
9667
  <<tool:${tc.name}:↻ duplicate suppressed>>
9502
9668
  `);
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
9669
  messages.push({
9507
9670
  role: "tool",
9508
9671
  tool_call_id: tc.id,
9509
- content: buildOpenAIRepeatedSearchError(
9510
- previousTool,
9511
- previousQuery,
9672
+ content: buildRepeatedSearchError(
9673
+ searchLoopCheck.previousTool,
9674
+ searchLoopCheck.previousQuery,
9512
9675
  latestToolMessage ? String(latestToolMessage.content || "") : null,
9513
- mode
9676
+ searchLoopCheck.mode
9514
9677
  )
9515
9678
  });
9516
9679
  compactCorrectionCount += 1;
@@ -9590,25 +9753,15 @@ class OpenAICompatProvider {
9590
9753
  recentCompactToolSignatures.shift();
9591
9754
  }
9592
9755
  }
9593
- if (!/^Error:/i.test(toolContent.trim())) {
9756
+ const toolSucceeded = !/^Error:/i.test(toolContent.trim());
9757
+ if (toolSucceeded) {
9594
9758
  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
9759
  }
9760
+ searchLoopGuard.recordSuccess(
9761
+ tc.name,
9762
+ searchToolQuery,
9763
+ toolSucceeded
9764
+ );
9612
9765
  recentToolNames.push(tc.name);
9613
9766
  if (recentToolNames.length > 8) recentToolNames.shift();
9614
9767
  if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
@@ -10138,59 +10291,13 @@ function previewToolResult(text, maxLength = 800) {
10138
10291
  if (normalized.length <= maxLength) return normalized;
10139
10292
  return `${normalized.slice(0, maxLength)}...`;
10140
10293
  }
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
10294
  function hasBlockingOverlaySignal(text) {
10148
10295
  if (!text) return false;
10149
10296
  if (/\bno blocking overlays detected\b/i.test(text)) return false;
10150
10297
  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
10298
  }
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
10299
  function buildCodexUnsupportedClearOverlayError(latestToolResultPreview) {
10193
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10300
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10194
10301
  const lines = [
10195
10302
  `Error: No blocking overlay signal is present in the latest browser state.`,
10196
10303
  `Do not call clear_overlays unless read_page or the page context explicitly reports a blocking overlay.`,
@@ -10310,7 +10417,7 @@ function shouldRetryCodexToolLoop(text, hasToolHistory) {
10310
10417
  return false;
10311
10418
  }
10312
10419
  function buildCodexRecoveryInput(userMessage, assistantText, latestToolResultPreview) {
10313
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10420
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10314
10421
  const lines = [
10315
10422
  `[System] The task is still in progress: ${userMessage}`,
10316
10423
  `Do not ask the user what they want next unless the original request is genuinely ambiguous or blocked.`,
@@ -10343,7 +10450,7 @@ function buildCodexFlightPriceEvidenceRecoveryInput(userMessage, assistantText,
10343
10450
  };
10344
10451
  }
10345
10452
  function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1) {
10346
- const stateReminder = buildCodexLatestStateReminder(latestToolResultPreview);
10453
+ const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10347
10454
  const lines = [
10348
10455
  `[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`,
10349
10456
  `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 +10669,7 @@ class CodexProvider {
10562
10669
  let clickReadLoopNudged = false;
10563
10670
  let latestToolResultPreview = null;
10564
10671
  let failedClickCountSinceProgress = 0;
10565
- const recentSuccessfulSearchQueries = [];
10566
- const recentSuccessfulSearchToolByQuery = /* @__PURE__ */ new Map();
10567
- let lastSuccessfulWebSearchQuery = null;
10672
+ const searchLoopGuard = new SearchLoopGuard(isRealProgressTool);
10568
10673
  try {
10569
10674
  for (let i = 0; i < maxIterations; i++) {
10570
10675
  iterationsUsed = i + 1;
@@ -10585,11 +10690,10 @@ class CodexProvider {
10585
10690
  (item) => item.type === "function_call"
10586
10691
  );
10587
10692
  if (functionCalls.length === 0) {
10588
- const recoveredTextCalls = recoverTextEncodedToolCalls(
10693
+ const recoveredCalls = recoverAssistantTextToolCalls(
10589
10694
  result.text,
10590
10695
  availableToolNames
10591
10696
  );
10592
- const recoveredCalls = recoveredTextCalls.length > 0 ? recoveredTextCalls : recoverNarratedActionToolCalls(result.text, availableToolNames);
10593
10697
  if (recoveredCalls.length > 0) {
10594
10698
  if (result.text.trim()) onChunk("<<erase_prev>>");
10595
10699
  functionCalls = recoveredCalls.map((toolCall) => ({
@@ -10654,30 +10758,29 @@ class CodexProvider {
10654
10758
  prepared.prepared.name,
10655
10759
  prepared.prepared.args
10656
10760
  );
10657
- const searchToolQuery = normalizedSearchToolQuery(
10761
+ const searchToolQuery = normalizeSearchToolQuery(
10658
10762
  prepared.prepared.name,
10659
10763
  prepared.prepared.args
10660
10764
  );
10661
- const isRepeatedSearchAcrossTools = searchToolQuery !== null && recentSuccessfulSearchQueries.includes(searchToolQuery);
10662
- const isQueryDriftedWebSearch = prepared.prepared.name === "web_search" && lastSuccessfulWebSearchQuery !== null && searchToolQuery !== null && searchToolQuery !== lastSuccessfulWebSearchQuery;
10765
+ const searchLoopCheck = searchLoopGuard.check(
10766
+ prepared.prepared.name,
10767
+ searchToolQuery
10768
+ );
10663
10769
  const isUnsupportedClearOverlay = prepared.prepared.name === "clear_overlays" && !hasBlockingOverlaySignal(
10664
10770
  `${systemPrompt}
10665
10771
  ${latestToolResultPreview || ""}`
10666
10772
  );
10667
- if (isRepeatedSearchAcrossTools || isQueryDriftedWebSearch) {
10773
+ if (searchLoopCheck) {
10668
10774
  onChunk(`
10669
10775
  <<tool:${prepared.prepared.name}:↻ duplicate suppressed>>
10670
10776
  `);
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
10777
  const output2 = createCodexToolOutput(
10675
10778
  prepared.prepared.callId,
10676
- buildCodexRepeatedSearchError(
10677
- previousTool,
10678
- previousQuery,
10779
+ buildRepeatedSearchError(
10780
+ searchLoopCheck.previousTool,
10781
+ searchLoopCheck.previousQuery,
10679
10782
  latestToolResultPreview,
10680
- mode
10783
+ searchLoopCheck.mode
10681
10784
  )
10682
10785
  );
10683
10786
  currentInput.push(output2);
@@ -10731,30 +10834,15 @@ ${latestToolResultPreview || ""}`
10731
10834
  toolHistoryCount += 1;
10732
10835
  latestToolResultPreview = previewToolResult(output.output);
10733
10836
  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
- }
10837
+ const toolSucceeded = !looksLikeFailedToolOutput(outputText);
10838
+ if (toolSucceeded && isRealProgressTool(prepared.prepared.name)) {
10839
+ failedClickCountSinceProgress = 0;
10757
10840
  }
10841
+ searchLoopGuard.recordSuccess(
10842
+ prepared.prepared.name,
10843
+ searchToolQuery,
10844
+ toolSucceeded
10845
+ );
10758
10846
  if (prepared.prepared.name === "click" && looksLikeFailedToolOutput(outputText)) {
10759
10847
  failedClickCountSinceProgress += 1;
10760
10848
  currentInput.push(
@@ -12711,17 +12799,23 @@ function formatCartSnapshot(page) {
12711
12799
  function isVisibleToUser(el) {
12712
12800
  return el.visible === true && el.inViewport === true && el.obscured !== true && el.blockedByOverlay !== true;
12713
12801
  }
12802
+ function elementSearchText(el, extraFields = []) {
12803
+ const fields = [
12804
+ el.text,
12805
+ el.label,
12806
+ el.name,
12807
+ el.placeholder,
12808
+ el.description,
12809
+ el.href,
12810
+ ...extraFields.map((field) => el[field])
12811
+ ];
12812
+ return normalizeComparable(fields.filter(Boolean).join(" "));
12813
+ }
12814
+ function formatElementOptions(options, maxOptions) {
12815
+ return options?.slice(0, maxOptions).map((o) => typeof o === "string" ? o : o.label || o.value).join("|") ?? "";
12816
+ }
12714
12817
  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
- );
12818
+ const haystack = elementSearchText(el);
12725
12819
  if (!haystack) return Number.POSITIVE_INFINITY;
12726
12820
  if (/\badd(?: item)? to (?:cart|bag|basket)\b/.test(haystack)) return 0;
12727
12821
  if (/\b(?:buy now|preorder|pre-order|reserve now|shop now)\b/.test(haystack)) {
@@ -12733,17 +12827,7 @@ function purchaseActionPriority(el) {
12733
12827
  return Number.POSITIVE_INFINITY;
12734
12828
  }
12735
12829
  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
- );
12830
+ const haystack = elementSearchText(el, ["role"]);
12747
12831
  if (!haystack) return Number.POSITIVE_INFINITY;
12748
12832
  if (/\b(today|tomorrow|mon(?:day)?|tue(?:s|sday)?|wed(?:nesday)?|thu(?:rs|rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b/.test(
12749
12833
  haystack
@@ -12852,24 +12936,34 @@ function formatDialogFocus(page) {
12852
12936
  }
12853
12937
  function formatInteractiveElements(elements) {
12854
12938
  if (elements.length === 0) return "None";
12939
+ const DIALOG_PRIORITY_BONUS = 40;
12940
+ const HIDDEN_VISIBILITY_PENALTY = 100;
12941
+ const OFFSCREEN_PENALTY = 50;
12942
+ const NAVIGATION_CONTEXT_PENALTY = 30;
12943
+ const OBSCURED_PENALTY = 20;
12944
+ const LINK_TYPE_PENALTY = 5;
12945
+ const PURCHASE_BASE_WEIGHT = 25;
12946
+ const PURCHASE_PRIORITY_MULTIPLIER = 5;
12947
+ const DATE_BASE_WEIGHT = 18;
12948
+ const DATE_PRIORITY_MULTIPLIER = 4;
12855
12949
  const sorted = [...elements].sort((a, b) => {
12856
12950
  const scoreEl = (el) => {
12857
12951
  let s = 0;
12858
- if (el.context === "dialog") s -= 40;
12952
+ if (el.context === "dialog") s -= DIALOG_PRIORITY_BONUS;
12859
12953
  const purchasePriority = purchaseActionPriority(el);
12860
12954
  if (Number.isFinite(purchasePriority)) {
12861
- s -= 25 - purchasePriority * 5;
12955
+ s -= PURCHASE_BASE_WEIGHT - purchasePriority * PURCHASE_PRIORITY_MULTIPLIER;
12862
12956
  }
12863
12957
  const datePriority = dateOrShowtimeControlPriority(el);
12864
12958
  if (Number.isFinite(datePriority)) {
12865
- s -= 18 - datePriority * 4;
12959
+ s -= DATE_BASE_WEIGHT - datePriority * DATE_PRIORITY_MULTIPLIER;
12866
12960
  }
12867
- if (el.visible === false) s += 100;
12868
- if (el.inViewport === false) s += 50;
12961
+ if (el.visible === false) s += HIDDEN_VISIBILITY_PENALTY;
12962
+ if (el.inViewport === false) s += OFFSCREEN_PENALTY;
12869
12963
  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;
12964
+ s += NAVIGATION_CONTEXT_PENALTY;
12965
+ if (el.obscured) s += OBSCURED_PENALTY;
12966
+ if (el.type === "link") s += LINK_TYPE_PENALTY;
12873
12967
  return s;
12874
12968
  };
12875
12969
  return scoreEl(a) - scoreEl(b);
@@ -12901,9 +12995,7 @@ function formatInteractiveElements(elements) {
12901
12995
  appendFieldAffordances(parts, el);
12902
12996
  if (el.options?.length) {
12903
12997
  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
- );
12998
+ parts.push(`options=${formatElementOptions(el.options, maxOptions)}`);
12907
12999
  }
12908
13000
  } else if (el.type === "textarea") {
12909
13001
  parts.push(`[${el.label || "Text Area"}]`);
@@ -12966,7 +13058,7 @@ function formatForms(forms) {
12966
13058
  if (field.options?.length) {
12967
13059
  const maxOptions = isDateOrShowtimeControl(field) ? 10 : 5;
12968
13060
  fieldParts.push(
12969
- `options=${field.options.slice(0, maxOptions).map((o) => typeof o === "string" ? o : o.label || o.value).join("|")}`
13061
+ `options=${formatElementOptions(field.options, maxOptions)}`
12970
13062
  );
12971
13063
  }
12972
13064
  } else if (field.type === "textarea") {
@@ -13231,26 +13323,10 @@ function chooseAgentReadMode(page) {
13231
13323
  return "visible_only";
13232
13324
  }
13233
13325
  }
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 [
13326
+ const SITE_RESULT_FILTERS = [
13327
+ {
13328
+ hostname: "news.ycombinator.com",
13329
+ listingPaths: [
13254
13330
  "/",
13255
13331
  "/news",
13256
13332
  "/newest",
@@ -13262,30 +13338,62 @@ function isHackerNewsListingPage(url) {
13262
13338
  "/active",
13263
13339
  "/classic",
13264
13340
  "/noobstories"
13265
- ].includes(pathname);
13266
- } catch {
13267
- return false;
13341
+ ],
13342
+ utilityPathnames: ["/hide", "/user"],
13343
+ utilityTextPatterns: [
13344
+ /^(hide|past|favorite|unfavorite|flag|unflag|discuss|reply|parent|more)$/,
13345
+ /^\d+\s+(?:comments?|points?)$/
13346
+ ]
13268
13347
  }
13269
- }
13270
- function isHackerNewsUtilityLink(element) {
13271
- if (!element.href) return false;
13272
- let url;
13348
+ ];
13349
+ function matchesSiteFilter(url, filter, baseHostname) {
13273
13350
  try {
13274
- url = new URL(element.href);
13351
+ const parsed = new URL(url, baseHostname ? `https://${baseHostname}` : void 0);
13352
+ return parsed.hostname === filter.hostname;
13275
13353
  } catch {
13276
13354
  return false;
13277
13355
  }
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;
13356
+ }
13357
+ function isSiteListingPage(url) {
13358
+ for (const filter of SITE_RESULT_FILTERS) {
13359
+ if (!matchesSiteFilter(url, filter, "")) continue;
13360
+ try {
13361
+ const pathname = new URL(url).pathname.replace(/\/+$/, "") || "/";
13362
+ if (filter.listingPaths?.includes(pathname)) return true;
13363
+ } catch {
13364
+ }
13365
+ }
13366
+ return false;
13367
+ }
13368
+ function isSiteUtilityLink(element) {
13369
+ if (!element.href) return false;
13370
+ for (const filter of SITE_RESULT_FILTERS) {
13371
+ if (!matchesSiteFilter(element.href, filter, "")) continue;
13372
+ const text = normalizeComparable(element.text || "");
13373
+ for (const pattern of filter.utilityTextPatterns ?? []) {
13374
+ if (pattern.test(text)) return true;
13375
+ }
13376
+ try {
13377
+ const pathname = new URL(element.href).pathname.replace(/\/+$/, "") || "/";
13378
+ if (filter.utilityPathnames?.includes(pathname)) return true;
13379
+ } catch {
13380
+ }
13285
13381
  }
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);
13382
+ return false;
13383
+ }
13384
+ function isSearchOrListingPage(page) {
13385
+ if (isSiteListingPage(page.url)) return true;
13386
+ const haystack = normalizeComparable(
13387
+ [
13388
+ page.url,
13389
+ page.title,
13390
+ page.excerpt,
13391
+ page.headings.map((heading) => heading.text).join(" ")
13392
+ ].filter(Boolean).join(" ")
13393
+ );
13394
+ return /\b(search|results|find|discover|browse|repositories|repository|issues|pull requests|prs|users|events|listings)\b/.test(
13395
+ haystack
13396
+ );
13289
13397
  }
13290
13398
  function collectJsonLdEntityItems(input, results = []) {
13291
13399
  if (!input) return results;
@@ -13326,7 +13434,7 @@ function getResultCandidates(page) {
13326
13434
  const pageHost = normalizeUrlForMatch(page.url);
13327
13435
  const searchOrListingPage = isSearchOrListingPage(page);
13328
13436
  const scored = page.interactiveElements.filter(
13329
- (element) => element.type === "link" && element.text?.trim() && element.href && !isHackerNewsUtilityLink(element)
13437
+ (element) => element.type === "link" && element.text?.trim() && element.href && !isSiteUtilityLink(element)
13330
13438
  ).map((element) => {
13331
13439
  const text = element.text?.trim() || "";
13332
13440
  const comparableText = normalizeComparable(text);
@@ -13786,7 +13894,7 @@ function detectPageType(page) {
13786
13894
  if (hasResults && hasSearchInput && listingLike) return "SEARCH_RESULTS";
13787
13895
  if (hasCart) return "SHOPPING";
13788
13896
  if (formCount > 0 && !hasPasswordField) return "FORM";
13789
- if (isHackerNewsListingPage(page.url)) return "PAGINATED_LIST";
13897
+ if (isSiteListingPage(page.url)) return "PAGINATED_LIST";
13790
13898
  if (hasPagination && listingLike) return "PAGINATED_LIST";
13791
13899
  if (hasSearchInput && !listingLike) return "SEARCH_READY";
13792
13900
  if (page.content.length > 3e3 && page.interactiveElements.length < 10)
@@ -23306,7 +23414,10 @@ Recent checkpoints:
23306
23414
  ${input.recentCheckpoints || "- none"}
23307
23415
 
23308
23416
  Task tracker:
23309
- ${input.taskTrackerContext || "- none"}`;
23417
+ ${input.taskTrackerContext || "- none"}
23418
+
23419
+ Task memory:
23420
+ ${input.taskMemoryContext || "- none"}`;
23310
23421
  }
23311
23422
  function buildAgentSystemPrompt(input) {
23312
23423
  const instructionBlocks = input.profile === "compact" ? [
@@ -23687,6 +23798,7 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
23687
23798
  const runtimeState = runtime2.getState();
23688
23799
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
23689
23800
  const taskTrackerContext = runtime2.getTaskTrackerContext();
23801
+ const taskMemoryContext = runtime2.getTaskMemoryContext();
23690
23802
  const activeTabTitle = pageContent.title || "(untitled)";
23691
23803
  const activeTabUrl = pageContent.url || activeWebContents.getURL();
23692
23804
  const allTabs = tabManager.getAllStates();
@@ -23705,7 +23817,8 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
23705
23817
  approvalMode: runtimeState.supervisor.approvalMode,
23706
23818
  pendingApprovals: runtimeState.supervisor.pendingApprovals.length,
23707
23819
  recentCheckpoints: recentCheckpoints || "- none",
23708
- taskTrackerContext: taskTrackerContext || "- none"
23820
+ taskTrackerContext: taskTrackerContext || "- none",
23821
+ taskMemoryContext: taskMemoryContext || "- none"
23709
23822
  });
23710
23823
  const actionCtx = {
23711
23824
  tabManager,
@@ -24305,6 +24418,17 @@ function setMcpHealth(update) {
24305
24418
  }
24306
24419
  const ApprovalModeSchema = zod.z.enum(["auto", "confirm-dangerous", "manual"]);
24307
24420
  const CheckpointIdSchema = zod.z.string().min(1);
24421
+ const TaskTextSchema = zod.z.string().trim().min(1).max(2e4);
24422
+ const OptionalTaskTextSchema = zod.z.string().trim().max(2e4).optional();
24423
+ const OptionalNullableTaskTextSchema = zod.z.string().trim().max(2e4).nullable().optional();
24424
+ const TaskFactsSchema = zod.z.record(
24425
+ zod.z.string().trim().min(1).max(200),
24426
+ zod.z.string().max(2e4)
24427
+ );
24428
+ const TaskMemoryPatchSchema = zod.z.object({
24429
+ nextStep: OptionalNullableTaskTextSchema,
24430
+ facts: TaskFactsSchema.optional()
24431
+ });
24308
24432
  function registerAgentRuntimeHandlers(runtime2, chromeView, sidebarView, sendToRendererViews) {
24309
24433
  let runtimeUpdateTimer = null;
24310
24434
  let pendingRuntimeState = null;
@@ -24397,6 +24521,45 @@ function registerAgentRuntimeHandlers(runtime2, chromeView, sidebarView, sendToR
24397
24521
  return runtime2.restoreSession(snapshot2);
24398
24522
  }
24399
24523
  );
24524
+ electron.ipcMain.handle(Channels.AGENT_TASK_START, (event, goal) => {
24525
+ assertTrustedIpcSender(event);
24526
+ return runtime2.startTaskMemory(parseIpc(TaskTextSchema, goal, "goal"));
24527
+ });
24528
+ electron.ipcMain.handle(
24529
+ Channels.AGENT_TASK_UPDATE,
24530
+ (event, patch) => {
24531
+ assertTrustedIpcSender(event);
24532
+ return runtime2.updateTaskMemory(
24533
+ parseIpc(TaskMemoryPatchSchema, patch ?? {}, "patch")
24534
+ );
24535
+ }
24536
+ );
24537
+ electron.ipcMain.handle(Channels.AGENT_TASK_NOTE, (event, text) => {
24538
+ assertTrustedIpcSender(event);
24539
+ return runtime2.addTaskNote(parseIpc(TaskTextSchema, text, "text"));
24540
+ });
24541
+ electron.ipcMain.handle(Channels.AGENT_TASK_BLOCKER, (event, blocker) => {
24542
+ assertTrustedIpcSender(event);
24543
+ const validated = blocker == null ? null : parseIpc(OptionalNullableTaskTextSchema, blocker, "blocker");
24544
+ return runtime2.setTaskBlocker(validated ?? null);
24545
+ });
24546
+ electron.ipcMain.handle(Channels.AGENT_TASK_RESOLVE, (event, summary) => {
24547
+ assertTrustedIpcSender(event);
24548
+ return runtime2.resolveTaskMemory(
24549
+ summary == null ? void 0 : parseIpc(OptionalTaskTextSchema, summary, "summary")
24550
+ );
24551
+ });
24552
+ electron.ipcMain.handle(Channels.AGENT_TASK_ABANDON, (event, reason) => {
24553
+ assertTrustedIpcSender(event);
24554
+ return runtime2.abandonTaskMemory(
24555
+ reason == null ? void 0 : parseIpc(OptionalTaskTextSchema, reason, "reason")
24556
+ );
24557
+ });
24558
+ electron.ipcMain.handle(Channels.AGENT_TASK_CLEAR, (event) => {
24559
+ assertTrustedIpcSender(event);
24560
+ runtime2.clearTaskMemory();
24561
+ return null;
24562
+ });
24400
24563
  }
24401
24564
  function asTextResponse$1(text) {
24402
24565
  return { content: [{ type: "text", text }] };
@@ -28123,6 +28286,137 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
28123
28286
  }
28124
28287
  );
28125
28288
  }
28289
+ function registerTaskMemoryTools(server, _tabManager, runtime2) {
28290
+ server.registerTool(
28291
+ "task_start",
28292
+ {
28293
+ title: "Start Task",
28294
+ description: "Start tracking a task. Creates a task memory record with a goal that persists across actions and browser navigation. Use this at the beginning of a multi-step task so the human supervisor can see what you are working on.",
28295
+ inputSchema: {
28296
+ goal: zod.z.string().describe("What this task aims to accomplish"),
28297
+ nextStep: zod.z.string().optional().describe("The first step you plan to take"),
28298
+ facts: zod.z.record(zod.z.string()).optional().describe("Key-value facts relevant to this task (e.g. { username: alice })")
28299
+ }
28300
+ },
28301
+ async ({ goal, nextStep, facts }) => {
28302
+ const task = runtime2.startTaskMemory(goal, {
28303
+ nextStep: nextStep ?? null,
28304
+ facts: facts ?? {}
28305
+ });
28306
+ return asTextResponse(
28307
+ `Task started: ${task.goal}
28308
+ Status: ${task.status}${task.nextStep ? `
28309
+ Next step: ${task.nextStep}` : ""}`
28310
+ );
28311
+ }
28312
+ );
28313
+ server.registerTool(
28314
+ "task_update",
28315
+ {
28316
+ title: "Update Task",
28317
+ description: "Update the current task's next step or facts. Facts are merged with existing facts. Use this to record progress and keep the human supervisor informed.",
28318
+ inputSchema: {
28319
+ nextStep: zod.z.string().optional().describe("The next step you plan to take"),
28320
+ facts: zod.z.record(zod.z.string()).optional().describe("Key-value facts to merge into the task (existing keys are overwritten)")
28321
+ }
28322
+ },
28323
+ async ({ nextStep, facts }) => {
28324
+ const updated = runtime2.updateTaskMemory({
28325
+ nextStep,
28326
+ facts
28327
+ });
28328
+ if (!updated) return asTextResponse("No active task to update. Start one with task_start first.");
28329
+ return asTextResponse(
28330
+ `Task updated: ${updated.goal}
28331
+ Status: ${updated.status}${updated.nextStep ? `
28332
+ Next step: ${updated.nextStep}` : ""}${Object.keys(updated.facts).length > 0 ? `
28333
+ Facts: ${Object.entries(updated.facts).map(([k, v]) => `${k}=${v}`).join(", ")}` : ""}`
28334
+ );
28335
+ }
28336
+ );
28337
+ server.registerTool(
28338
+ "task_note",
28339
+ {
28340
+ title: "Add Task Note",
28341
+ description: "Add a note to the current task. Use this to record observations, intermediate results, or context for the human supervisor.",
28342
+ inputSchema: {
28343
+ text: zod.z.string().describe("The note text to add")
28344
+ }
28345
+ },
28346
+ async ({ text }) => {
28347
+ const updated = runtime2.addTaskNote(text);
28348
+ if (!updated) return asTextResponse("No active task to add a note to. Start one with task_start first.");
28349
+ return asTextResponse(`Note added to task: ${updated.goal}`);
28350
+ }
28351
+ );
28352
+ server.registerTool(
28353
+ "task_blocker",
28354
+ {
28355
+ title: "Set or Clear Task Blocker",
28356
+ description: "Mark the task as blocked with a reason, or clear a blocker to resume. Use this when you are stuck and need human input to continue.",
28357
+ inputSchema: {
28358
+ blocker: zod.z.string().optional().describe("Description of what is blocking progress. Omit or empty string to clear a blocker.")
28359
+ }
28360
+ },
28361
+ async ({ blocker }) => {
28362
+ const updated = runtime2.setTaskBlocker(blocker?.trim() || null);
28363
+ if (!updated) return asTextResponse("No active task. Start one with task_start first.");
28364
+ if (updated.blocker) {
28365
+ return asTextResponse(`Task blocked: ${updated.blocker}
28366
+ Status: ${updated.status}`);
28367
+ }
28368
+ return asTextResponse(`Blocker cleared. Task: ${updated.goal}
28369
+ Status: ${updated.status}`);
28370
+ }
28371
+ );
28372
+ server.registerTool(
28373
+ "task_resolve",
28374
+ {
28375
+ title: "Resolve Task",
28376
+ description: "Mark the current task as completed. Optionally add a summary note. Use this when the task goal has been achieved.",
28377
+ inputSchema: {
28378
+ summary: zod.z.string().optional().describe("Brief summary of the completed task")
28379
+ }
28380
+ },
28381
+ async ({ summary }) => {
28382
+ const resolved = runtime2.resolveTaskMemory(summary);
28383
+ if (!resolved) return asTextResponse("No active task to resolve. Start one with task_start first.");
28384
+ return asTextResponse(
28385
+ `Task completed: ${resolved.goal}${resolved.notes.length > 0 ? `
28386
+ Notes: ${resolved.notes.length} note(s)` : ""}`
28387
+ );
28388
+ }
28389
+ );
28390
+ server.registerTool(
28391
+ "task_abandon",
28392
+ {
28393
+ title: "Abandon Task",
28394
+ description: "Mark the current task as abandoned. Use this when the task cannot be completed or is no longer relevant.",
28395
+ inputSchema: {
28396
+ reason: zod.z.string().optional().describe("Reason for abandoning the task")
28397
+ }
28398
+ },
28399
+ async ({ reason }) => {
28400
+ const abandoned = runtime2.abandonTaskMemory(reason);
28401
+ if (!abandoned) return asTextResponse("No active task to abandon. Start one with task_start first.");
28402
+ return asTextResponse(
28403
+ `Task abandoned: ${abandoned.goal}${reason ? ` (${reason})` : ""}`
28404
+ );
28405
+ }
28406
+ );
28407
+ server.registerTool(
28408
+ "task_status",
28409
+ {
28410
+ title: "Task Status",
28411
+ description: "Check the current task memory status including goal, progress, notes, and blocker."
28412
+ },
28413
+ async () => {
28414
+ const ctx = runtime2.getTaskMemoryContext();
28415
+ if (!ctx) return asTextResponse("No active task. Start one with task_start.");
28416
+ return asTextResponse(ctx);
28417
+ }
28418
+ );
28419
+ }
28126
28420
  function registerMacroTools(server, tabManager, runtime2) {
28127
28421
  server.registerTool(
28128
28422
  "fill_form",
@@ -29423,6 +29717,7 @@ function registerTools(server, tabManager, runtime2) {
29423
29717
  registerSessionTools(server, tabManager, runtime2);
29424
29718
  registerMemoryTools(server, tabManager, runtime2);
29425
29719
  registerFlowTools(server, tabManager, runtime2);
29720
+ registerTaskMemoryTools(server, tabManager, runtime2);
29426
29721
  registerMacroTools(server, tabManager, runtime2);
29427
29722
  registerVaultTools(server, tabManager);
29428
29723
  registerMetricsTools(server, tabManager, runtime2);
@@ -32666,6 +32961,121 @@ function formatTaskTracker(state2) {
32666
32961
  ${lines.join("\n")}
32667
32962
  ---`;
32668
32963
  }
32964
+ function createTaskMemory(goal, options = {}) {
32965
+ const now = (/* @__PURE__ */ new Date()).toISOString();
32966
+ return {
32967
+ id: crypto$1.randomUUID(),
32968
+ goal: goal.trim(),
32969
+ status: "active",
32970
+ blocker: null,
32971
+ notes: [],
32972
+ nextStep: options.nextStep?.trim() || null,
32973
+ facts: { ...options.facts ?? {} },
32974
+ startedAt: now,
32975
+ updatedAt: now,
32976
+ completedAt: null
32977
+ };
32978
+ }
32979
+ function updateTaskMemory(task, patch) {
32980
+ const updated = {
32981
+ ...task,
32982
+ nextStep: patch.nextStep !== void 0 ? patch.nextStep : task.nextStep,
32983
+ facts: {
32984
+ ...task.facts,
32985
+ ...patch.facts ?? {}
32986
+ },
32987
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
32988
+ };
32989
+ return updated;
32990
+ }
32991
+ function addTaskNote(task, text) {
32992
+ const note = {
32993
+ id: crypto$1.randomUUID(),
32994
+ text: text.trim(),
32995
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
32996
+ };
32997
+ const notes = [...task.notes, note].slice(-50);
32998
+ return {
32999
+ ...task,
33000
+ notes,
33001
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
33002
+ };
33003
+ }
33004
+ function setTaskBlocker(task, blocker) {
33005
+ const status = blocker ? "blocked" : task.status === "blocked" ? "active" : task.status;
33006
+ return {
33007
+ ...task,
33008
+ status,
33009
+ blocker,
33010
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
33011
+ };
33012
+ }
33013
+ function resolveTaskMemory(task, summary) {
33014
+ const now = (/* @__PURE__ */ new Date()).toISOString();
33015
+ let notes = task.notes;
33016
+ if (summary?.trim()) {
33017
+ const note = {
33018
+ id: crypto$1.randomUUID(),
33019
+ text: summary.trim(),
33020
+ createdAt: now
33021
+ };
33022
+ notes = [...task.notes, note].slice(-50);
33023
+ }
33024
+ return {
33025
+ ...task,
33026
+ status: "completed",
33027
+ blocker: null,
33028
+ notes,
33029
+ completedAt: now,
33030
+ updatedAt: now
33031
+ };
33032
+ }
33033
+ function abandonTaskMemory(task, reason) {
33034
+ const now = (/* @__PURE__ */ new Date()).toISOString();
33035
+ let notes = task.notes;
33036
+ if (reason?.trim()) {
33037
+ const note = {
33038
+ id: crypto$1.randomUUID(),
33039
+ text: `Abandoned: ${reason.trim()}`,
33040
+ createdAt: now
33041
+ };
33042
+ notes = [...task.notes, note].slice(-50);
33043
+ }
33044
+ return {
33045
+ ...task,
33046
+ status: "abandoned",
33047
+ blocker: null,
33048
+ notes,
33049
+ completedAt: now,
33050
+ updatedAt: now
33051
+ };
33052
+ }
33053
+ function formatTaskMemory(task) {
33054
+ if (!task) return "";
33055
+ const lines = [
33056
+ "--- Task Memory ---",
33057
+ `Goal: ${task.goal}`,
33058
+ `Status: ${task.status}${task.blocker ? ` (blocked: ${task.blocker})` : ""}`
33059
+ ];
33060
+ if (task.nextStep) {
33061
+ lines.push(`Next step: ${task.nextStep}`);
33062
+ }
33063
+ if (Object.keys(task.facts).length > 0) {
33064
+ lines.push("Facts:");
33065
+ for (const [key2, value] of Object.entries(task.facts)) {
33066
+ lines.push(` ${key2}: ${value}`);
33067
+ }
33068
+ }
33069
+ if (task.notes.length > 0) {
33070
+ lines.push("Notes:");
33071
+ for (const note of task.notes.slice(-10)) {
33072
+ const time = note.createdAt.slice(11, 16);
33073
+ lines.push(` [${time}] ${note.text}`);
33074
+ }
33075
+ }
33076
+ lines.push("---");
33077
+ return lines.join("\n");
33078
+ }
32669
33079
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
32670
33080
  const PERSIST_DEBOUNCE_MS = 500;
32671
33081
  const INTERRUPTED_ACTION_STATUSES = /* @__PURE__ */ new Set([
@@ -32695,6 +33105,7 @@ function getRuntimeStatePath() {
32695
33105
  }
32696
33106
  function sanitizePersistence(persisted) {
32697
33107
  const recoveredAt = (/* @__PURE__ */ new Date()).toISOString();
33108
+ const persistedTaskMemory = persisted?.taskMemory?.completedAt ? null : persisted?.taskMemory ?? null;
32698
33109
  const actions = Array.isArray(persisted?.actions) ? persisted.actions.slice(-120).map(
32699
33110
  (action) => INTERRUPTED_ACTION_STATUSES.has(action.status) ? {
32700
33111
  ...action,
@@ -32716,7 +33127,8 @@ function sanitizePersistence(persisted) {
32716
33127
  transcript: [],
32717
33128
  mcpStatus: "stopped",
32718
33129
  flowState: null,
32719
- taskTracker: null
33130
+ taskTracker: null,
33131
+ taskMemory: persistedTaskMemory
32720
33132
  };
32721
33133
  }
32722
33134
  class AgentRuntime {
@@ -32803,7 +33215,8 @@ class AgentRuntime {
32803
33215
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
32804
33216
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
32805
33217
  note: note?.trim() || void 0,
32806
- snapshot: snapshot2
33218
+ snapshot: snapshot2,
33219
+ taskMemory: this.state.taskMemory ? clone(this.state.taskMemory) : null
32807
33220
  };
32808
33221
  this.state.checkpoints = [...this.state.checkpoints, checkpoint].slice(
32809
33222
  -20
@@ -32816,6 +33229,7 @@ class AgentRuntime {
32816
33229
  const checkpoint = this.state.checkpoints.find((item) => item.id === checkpointId) || null;
32817
33230
  if (!checkpoint) return null;
32818
33231
  this.tabManager.restoreSession(checkpoint.snapshot);
33232
+ this.state.taskMemory = checkpoint.taskMemory ? clone(checkpoint.taskMemory) : null;
32819
33233
  this.captureSession(`Restored ${checkpoint.name}`);
32820
33234
  return clone(checkpoint);
32821
33235
  }
@@ -32932,6 +33346,54 @@ class AgentRuntime {
32932
33346
  getTaskTrackerContext() {
32933
33347
  return formatTaskTracker(this.state.taskTracker);
32934
33348
  }
33349
+ // --- Task Memory ---
33350
+ startTaskMemory(goal, options) {
33351
+ this.state.taskMemory = createTaskMemory(goal, options);
33352
+ this.emit();
33353
+ return clone(this.state.taskMemory);
33354
+ }
33355
+ updateTaskMemory(patch) {
33356
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33357
+ this.state.taskMemory = updateTaskMemory(this.state.taskMemory, patch);
33358
+ this.emit();
33359
+ return clone(this.state.taskMemory);
33360
+ }
33361
+ addTaskNote(text) {
33362
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33363
+ this.state.taskMemory = addTaskNote(this.state.taskMemory, text);
33364
+ this.emit();
33365
+ return clone(this.state.taskMemory);
33366
+ }
33367
+ setTaskBlocker(blocker) {
33368
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33369
+ this.state.taskMemory = setTaskBlocker(
33370
+ this.state.taskMemory,
33371
+ blocker
33372
+ );
33373
+ this.emit();
33374
+ return clone(this.state.taskMemory);
33375
+ }
33376
+ resolveTaskMemory(summary) {
33377
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33378
+ const resolved = resolveTaskMemory(this.state.taskMemory, summary);
33379
+ this.state.taskMemory = null;
33380
+ this.emit();
33381
+ return clone(resolved);
33382
+ }
33383
+ abandonTaskMemory(reason) {
33384
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33385
+ const abandoned = abandonTaskMemory(this.state.taskMemory, reason);
33386
+ this.state.taskMemory = null;
33387
+ this.emit();
33388
+ return clone(abandoned);
33389
+ }
33390
+ clearTaskMemory() {
33391
+ this.state.taskMemory = null;
33392
+ this.emit();
33393
+ }
33394
+ getTaskMemoryContext() {
33395
+ return formatTaskMemory(this.state.taskMemory);
33396
+ }
32935
33397
  // --- Speedee Flow State ---
32936
33398
  startFlow(goal, steps, startUrl) {
32937
33399
  const flow = {
@@ -33157,7 +33619,8 @@ ${progress}
33157
33619
  lastError: this.state.supervisor.lastError
33158
33620
  },
33159
33621
  actions: this.state.actions.slice(-120),
33160
- checkpoints: this.state.checkpoints.slice(-20)
33622
+ checkpoints: this.state.checkpoints.slice(-20),
33623
+ taskMemory: this.state.taskMemory
33161
33624
  };
33162
33625
  return fs$1.promises.mkdir(path.dirname(getRuntimeStatePath()), { recursive: true }).then(
33163
33626
  () => fs$1.promises.writeFile(