@quanta-intellect/vessel-browser 0.1.144 → 0.1.146

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
@@ -682,15 +682,6 @@ function loadTrustedAppURL(wc, url) {
682
682
  }
683
683
  return wc.loadURL(parsed.toString());
684
684
  }
685
- const urlSafety = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
686
- __proto__: null,
687
- assertPermittedNavigationURL,
688
- assertSafeURL,
689
- isSafeNavigationURL,
690
- loadInternalDataURL,
691
- loadPermittedNavigationURL,
692
- loadTrustedAppURL
693
- }, Symbol.toStringTag, { value: "Module" }));
694
685
  const MAX_CUSTOM_HISTORY = 50;
695
686
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
696
687
  const logger$B = createLogger("Tab");
@@ -1467,7 +1458,12 @@ function loadJsonFile({
1467
1458
  const decoded = decodeStoredData(raw, secure);
1468
1459
  return parse(JSON.parse(decoded));
1469
1460
  } catch (err) {
1470
- logger$A.warn(`Failed to load ${filePath2}, using fallback:`, err);
1461
+ const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
1462
+ if (isMissingFile) {
1463
+ logger$A.info(`Persistence file not found; using fallback defaults: ${filePath2}`);
1464
+ } else {
1465
+ logger$A.warn(`Failed to load ${filePath2}, using fallback:`, err);
1466
+ }
1471
1467
  return fallback;
1472
1468
  }
1473
1469
  }
@@ -3867,7 +3863,14 @@ const AIChannels = {
3867
3863
  AGENT_CHECKPOINT_UPDATE_NOTE: "agent:checkpoint-update-note",
3868
3864
  AGENT_UNDO_LAST_ACTION: "agent:undo-last-action",
3869
3865
  AGENT_SESSION_CAPTURE: "agent:session-capture",
3870
- AGENT_SESSION_RESTORE: "agent:session-restore"
3866
+ AGENT_SESSION_RESTORE: "agent:session-restore",
3867
+ AGENT_TASK_START: "agent:task-start",
3868
+ AGENT_TASK_UPDATE: "agent:task-update",
3869
+ AGENT_TASK_NOTE: "agent:task-note",
3870
+ AGENT_TASK_BLOCKER: "agent:task-blocker",
3871
+ AGENT_TASK_RESOLVE: "agent:task-resolve",
3872
+ AGENT_TASK_ABANDON: "agent:task-abandon",
3873
+ AGENT_TASK_CLEAR: "agent:task-clear"
3871
3874
  };
3872
3875
  const AutofillChannels = {
3873
3876
  AUTOFILL_LIST: "autofill:list",
@@ -7794,6 +7797,96 @@ function isClickReadLoop(names) {
7794
7797
  }
7795
7798
  return clickReadPairs >= 2;
7796
7799
  }
7800
+ const CLICK_READ_LOOP_SUPPRESS_THRESHOLD = 3;
7801
+ function classifyClickFailure(output) {
7802
+ if (/Error\[hidden\]/i.test(output)) return "hidden";
7803
+ if (/Error\[stale-index\]/i.test(output)) return "stale";
7804
+ if (/^\s*Error:/i.test(output)) return "other";
7805
+ return null;
7806
+ }
7807
+ function buildClickReadLoopIntervention(strikes, lastClickFailureKind) {
7808
+ if (strikes <= 0) return null;
7809
+ if (strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD) {
7810
+ const lines = [
7811
+ `Error: Suppressed repeated click — you have alternated click and read_page ${strikes} times without making progress and the clicks are not landing.`,
7812
+ `Stop calling click. Instead do one of: scroll (scroll or scroll_to_element) to load more of the page then read_page to refresh, inspect_element on a specific indexed result, or answer from the results already visible in the conversation.`
7813
+ ];
7814
+ if (lastClickFailureKind === "hidden") {
7815
+ lines.push(
7816
+ `The last click target was hidden / not laid out — scrolling toward it first usually reveals it.`
7817
+ );
7818
+ } else if (lastClickFailureKind === "stale") {
7819
+ lines.push(
7820
+ `The last click target was stale — refresh page state with read_page and choose a currently listed target before clicking again.`
7821
+ );
7822
+ }
7823
+ return { kind: "suppress", message: lines.join("\n") };
7824
+ }
7825
+ if (strikes >= 2) {
7826
+ const lines = [
7827
+ `[System] You are alternating between click and read_page without advancing the task, and the last click did not complete.`,
7828
+ `The click result already includes a page snapshot, so do not read_page after every click.`
7829
+ ];
7830
+ if (lastClickFailureKind === "hidden") {
7831
+ lines.push(
7832
+ `The click failed on a hidden element — call scroll (scroll or scroll_to_element) to reveal it, then read_page to refresh visible elements, before clicking again.`
7833
+ );
7834
+ } else if (lastClickFailureKind === "stale") {
7835
+ lines.push(
7836
+ `The click failed on a stale element index — call read_page to refresh current indexes before clicking again.`
7837
+ );
7838
+ } else {
7839
+ lines.push(
7840
+ `If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
7841
+ );
7842
+ }
7843
+ return { kind: "nudge", message: lines.join("\n") };
7844
+ }
7845
+ return {
7846
+ kind: "nudge",
7847
+ message: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates, so do not read_page after every click. If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
7848
+ };
7849
+ }
7850
+ class ClickReadLoopGuard {
7851
+ recentToolNames = [];
7852
+ strikes = 0;
7853
+ lastClickFailureKind = null;
7854
+ beforeTool(toolName) {
7855
+ if (toolName === "click" && this.strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD && isClickReadLoop(this.recentToolNames)) {
7856
+ return buildClickReadLoopIntervention(
7857
+ this.strikes,
7858
+ this.lastClickFailureKind
7859
+ );
7860
+ }
7861
+ return null;
7862
+ }
7863
+ afterToolResult(toolName, output, succeeded) {
7864
+ if (toolName === "click") {
7865
+ this.lastClickFailureKind = succeeded ? null : classifyClickFailure(output);
7866
+ }
7867
+ this.recentToolNames.push(toolName);
7868
+ if (this.recentToolNames.length > 8) this.recentToolNames.shift();
7869
+ if (toolName === "click" && succeeded) {
7870
+ this.strikes = 0;
7871
+ return null;
7872
+ }
7873
+ if (toolName !== "click" && toolName !== "read_page") {
7874
+ this.strikes = 0;
7875
+ return null;
7876
+ }
7877
+ if (isClickReadLoop(this.recentToolNames) && this.lastClickFailureKind) {
7878
+ this.strikes += 1;
7879
+ if (this.strikes >= CLICK_READ_LOOP_SUPPRESS_THRESHOLD) {
7880
+ return null;
7881
+ }
7882
+ return buildClickReadLoopIntervention(
7883
+ this.strikes,
7884
+ this.lastClickFailureKind
7885
+ );
7886
+ }
7887
+ return null;
7888
+ }
7889
+ }
7797
7890
  const TERMINAL_TOOL_RESULT = "__VESSEL_TERMINAL_TOOL_RESULT__";
7798
7891
  const logger$w = createLogger("PromptCache");
7799
7892
  function shortHash(value) {
@@ -7954,8 +8047,7 @@ class AnthropicProvider {
7954
8047
  try {
7955
8048
  const maxIterations = getEffectiveMaxIterations();
7956
8049
  let iterationsUsed = 0;
7957
- const recentToolNames = [];
7958
- let clickReadLoopNudged = false;
8050
+ const clickReadLoopGuard = new ClickReadLoopGuard();
7959
8051
  for (let i = 0; i < maxIterations; i++) {
7960
8052
  iterationsUsed = i + 1;
7961
8053
  const stream = this.client.messages.stream(
@@ -8062,6 +8154,7 @@ class AnthropicProvider {
8062
8154
  break;
8063
8155
  }
8064
8156
  const toolResults = [];
8157
+ const loopNudges = [];
8065
8158
  for (const tb of toolUseBlocks) {
8066
8159
  if (tb._malformedArgs !== void 0) {
8067
8160
  onChunk(`
@@ -8075,6 +8168,19 @@ class AnthropicProvider {
8075
8168
  });
8076
8169
  continue;
8077
8170
  }
8171
+ const clickLoopPreflight = clickReadLoopGuard.beforeTool(tb.name);
8172
+ if (clickLoopPreflight?.kind === "suppress") {
8173
+ onChunk(`
8174
+ <<tool:click:↻ loop suppressed>>
8175
+ `);
8176
+ toolResults.push({
8177
+ type: "tool_result",
8178
+ tool_use_id: tb.id,
8179
+ content: clickLoopPreflight.message,
8180
+ is_error: true
8181
+ });
8182
+ continue;
8183
+ }
8078
8184
  const argSummary = [tb.input.url, tb.input.query, tb.input.text, tb.input.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
8079
8185
  onChunk(`
8080
8186
  <<tool:${tb.name}${argSummary ? ":" + argSummary : ""}>>
@@ -8089,6 +8195,7 @@ class AnthropicProvider {
8089
8195
  if (result === TERMINAL_TOOL_RESULT) {
8090
8196
  return;
8091
8197
  }
8198
+ const toolSucceeded = !/^Error:/i.test(result.trim());
8092
8199
  let parsedRich = null;
8093
8200
  try {
8094
8201
  const parsed = JSON.parse(result);
@@ -8120,16 +8227,18 @@ class AnthropicProvider {
8120
8227
  content: result
8121
8228
  });
8122
8229
  }
8123
- recentToolNames.push(tb.name);
8124
- if (recentToolNames.length > 8) recentToolNames.shift();
8230
+ const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
8231
+ tb.name,
8232
+ result,
8233
+ toolSucceeded
8234
+ );
8235
+ if (clickLoopIntervention?.kind === "nudge") {
8236
+ loopNudges.push(clickLoopIntervention.message);
8237
+ }
8125
8238
  }
8126
8239
  messages.push({ role: "user", content: toolResults });
8127
- if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
8128
- clickReadLoopNudged = true;
8129
- messages.push({
8130
- role: "user",
8131
- content: `You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
8132
- });
8240
+ for (const nudge of loopNudges) {
8241
+ messages.push({ role: "user", content: nudge });
8133
8242
  }
8134
8243
  }
8135
8244
  if (iterationsUsed >= maxIterations) {
@@ -8683,6 +8792,106 @@ function recoverTextEncodedToolCalls(text, availableToolNames) {
8683
8792
  }
8684
8793
  return recovered;
8685
8794
  }
8795
+ function findInlineToolMarkerBodies(text) {
8796
+ const bodies = [];
8797
+ const lowerText = text.toLowerCase();
8798
+ let searchIndex = 0;
8799
+ while (searchIndex < text.length) {
8800
+ const start = lowerText.indexOf("<<tool", searchIndex);
8801
+ if (start === -1) break;
8802
+ let quote = null;
8803
+ let escaped = false;
8804
+ let end = -1;
8805
+ for (let index = start + 2; index < text.length - 1; index += 1) {
8806
+ const char = text[index];
8807
+ if (quote) {
8808
+ if (escaped) {
8809
+ escaped = false;
8810
+ } else if (char === "\\") {
8811
+ escaped = true;
8812
+ } else if (char === quote) {
8813
+ quote = null;
8814
+ }
8815
+ continue;
8816
+ }
8817
+ if (char === '"' || char === "'") {
8818
+ quote = char;
8819
+ continue;
8820
+ }
8821
+ if (char === ">" && text[index + 1] === ">") {
8822
+ end = index;
8823
+ break;
8824
+ }
8825
+ }
8826
+ if (end === -1) {
8827
+ searchIndex = start + 2;
8828
+ continue;
8829
+ }
8830
+ bodies.push(text.slice(start + 2, end));
8831
+ searchIndex = end + 2;
8832
+ }
8833
+ return bodies;
8834
+ }
8835
+ function recoverInlineToolMarkerToolCalls(text, availableToolNames) {
8836
+ const recovered = [];
8837
+ for (const markerBody of findInlineToolMarkerBodies(text)) {
8838
+ const match = markerBody.trim().match(/^tool[:=]([a-z_][a-z0-9_]*)(?:[:=]([\s\S]*))?$/i);
8839
+ if (!match) continue;
8840
+ const rawName = match[1] ?? "";
8841
+ const rawArgs = (match[2] ?? "").trim();
8842
+ const resolvedName = resolveToolCallName(
8843
+ rawName,
8844
+ {},
8845
+ availableToolNames
8846
+ );
8847
+ if (!availableToolNames.has(resolvedName)) continue;
8848
+ let parsedArgs = null;
8849
+ if (rawArgs.startsWith("{")) {
8850
+ const repaired = parseToolArgsWithRepair(resolvedName, rawArgs);
8851
+ if (repaired) {
8852
+ parsedArgs = repaired.args;
8853
+ }
8854
+ }
8855
+ if (!parsedArgs) {
8856
+ const kvArgs = {};
8857
+ const kvPattern = /([a-z_][a-z0-9_]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^,\s]*))/gi;
8858
+ let kvMatch;
8859
+ while ((kvMatch = kvPattern.exec(rawArgs)) !== null) {
8860
+ const key2 = kvMatch[1];
8861
+ const value = kvMatch[2] ?? kvMatch[3] ?? kvMatch[4] ?? "";
8862
+ kvArgs[key2] = value;
8863
+ }
8864
+ if (Object.keys(kvArgs).length > 0) {
8865
+ parsedArgs = kvArgs;
8866
+ }
8867
+ }
8868
+ if (!parsedArgs && rawArgs) {
8869
+ const repaired = parseToolArgsWithRepair(resolvedName, rawArgs);
8870
+ if (repaired) {
8871
+ parsedArgs = repaired.args;
8872
+ }
8873
+ }
8874
+ if (!parsedArgs) {
8875
+ parsedArgs = {};
8876
+ }
8877
+ recovered.push({
8878
+ id: `recovered_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8879
+ name: resolvedName,
8880
+ argsJson: JSON.stringify(parsedArgs)
8881
+ });
8882
+ }
8883
+ return recovered;
8884
+ }
8885
+ function recoverAssistantTextToolCalls(text, availableToolNames) {
8886
+ const textEncodedCalls = recoverTextEncodedToolCalls(text, availableToolNames);
8887
+ if (textEncodedCalls.length > 0) return textEncodedCalls;
8888
+ const inlineMarkerCalls = recoverInlineToolMarkerToolCalls(
8889
+ text,
8890
+ availableToolNames
8891
+ );
8892
+ if (inlineMarkerCalls.length > 0) return inlineMarkerCalls;
8893
+ return recoverNarratedActionToolCalls(text, availableToolNames);
8894
+ }
8686
8895
  function recoverNarratedActionToolCalls(text, availableToolNames) {
8687
8896
  const trimmed = text.trim();
8688
8897
  if (!trimmed) return [];
@@ -9320,10 +9529,9 @@ class OpenAICompatProvider {
9320
9529
  let highlightCompletionRecoveryCount = 0;
9321
9530
  let compactCorrectionCount = 0;
9322
9531
  const recentCompactToolSignatures = [];
9323
- const recentToolNames = [];
9324
9532
  const successfulToolNames = [];
9325
9533
  const searchLoopGuard = new SearchLoopGuard(isSearchContextResettingTool);
9326
- let clickReadLoopNudged = false;
9534
+ const clickReadLoopGuard = new ClickReadLoopGuard();
9327
9535
  for (let i = 0; i < maxIterations; i++) {
9328
9536
  iterationsUsed = i + 1;
9329
9537
  let textAccum = "";
@@ -9398,22 +9606,13 @@ class OpenAICompatProvider {
9398
9606
  tc.name = resolveToolCallName(tc.name, parsedArgs, availableToolNames);
9399
9607
  }
9400
9608
  if (toolCalls.length === 0) {
9401
- const recoveredToolCalls = recoverTextEncodedToolCalls(
9609
+ const recoveredToolCalls = recoverAssistantTextToolCalls(
9402
9610
  textAccum,
9403
9611
  availableToolNames
9404
9612
  );
9405
9613
  if (recoveredToolCalls.length > 0) {
9406
9614
  toolCalls = recoveredToolCalls;
9407
9615
  if (textAccum.trim()) onChunk("<<erase_prev>>");
9408
- } else {
9409
- const narratedToolCalls = recoverNarratedActionToolCalls(
9410
- textAccum,
9411
- availableToolNames
9412
- );
9413
- if (narratedToolCalls.length > 0) {
9414
- toolCalls = narratedToolCalls;
9415
- if (textAccum.trim()) onChunk("<<erase_prev>>");
9416
- }
9417
9616
  }
9418
9617
  }
9419
9618
  logAgentLoopDebug({
@@ -9622,6 +9821,19 @@ class OpenAICompatProvider {
9622
9821
  }
9623
9822
  continue;
9624
9823
  }
9824
+ const clickLoopPreflight = clickReadLoopGuard.beforeTool(tc.name);
9825
+ if (clickLoopPreflight?.kind === "suppress") {
9826
+ onChunk(`
9827
+ <<tool:click:↻ loop suppressed>>
9828
+ `);
9829
+ messages.push({
9830
+ role: "tool",
9831
+ tool_call_id: tc.id,
9832
+ content: clickLoopPreflight.message
9833
+ });
9834
+ compactCorrectionCount += 1;
9835
+ continue;
9836
+ }
9625
9837
  const argSummary = [args.url, args.query, args.text, args.direction].map((v) => typeof v === "string" ? v : "").find((v) => v.length > 0) ?? "";
9626
9838
  onChunk(`
9627
9839
  <<tool:${tc.name}${argSummary ? ":" + argSummary : ""}>>
@@ -9659,15 +9871,11 @@ class OpenAICompatProvider {
9659
9871
  searchToolQuery,
9660
9872
  toolSucceeded
9661
9873
  );
9662
- recentToolNames.push(tc.name);
9663
- if (recentToolNames.length > 8) recentToolNames.shift();
9664
- if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
9665
- clickReadLoopNudged = true;
9666
- messages.push({
9667
- role: "user",
9668
- content: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates — you do not need read_page after every click. If you need detail on a specific element, use inspect_element instead. If you have enough context, proceed with the next action directly.`
9669
- });
9670
- }
9874
+ const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
9875
+ tc.name,
9876
+ toolContent,
9877
+ toolSucceeded
9878
+ );
9671
9879
  compactCorrectionCount = 0;
9672
9880
  iterationToolResultPreviews.push(toolContent);
9673
9881
  messages.push({
@@ -9675,6 +9883,9 @@ class OpenAICompatProvider {
9675
9883
  tool_call_id: tc.id,
9676
9884
  content: toolContent
9677
9885
  });
9886
+ if (clickLoopIntervention?.kind === "nudge") {
9887
+ messages.push({ role: "user", content: clickLoopIntervention.message });
9888
+ }
9678
9889
  }
9679
9890
  const followUpReminder = followUpReminderForProfile(
9680
9891
  this.agentToolProfile,
@@ -10346,12 +10557,25 @@ function buildCodexFlightPriceEvidenceRecoveryInput(userMessage, assistantText,
10346
10557
  ]
10347
10558
  };
10348
10559
  }
10349
- function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1) {
10560
+ function buildCodexFailedClickRecoveryInput(attemptedTarget, latestToolResultPreview, failedClickCount = 1, errorOutput = "") {
10350
10561
  const stateReminder = buildLatestStateReminder(latestToolResultPreview);
10562
+ const clickFailureKind = classifyClickFailure(errorOutput);
10351
10563
  const lines = [
10352
- `[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`,
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.`
10564
+ `[System] The previous click did not complete${attemptedTarget ? ` for ${attemptedTarget}` : ""}.`
10354
10565
  ];
10566
+ if (clickFailureKind === "hidden") {
10567
+ lines.push(
10568
+ `The click target was hidden / not laid out (collapsed, lazy-loaded, or virtual-scroll). Call scroll or scroll_to_element toward it to reveal it, then read_page to refresh visible elements, before clicking again — or inspect_element on the intended index, or answer from the results already visible. Do not ask the user to inspect or click the result for you.`
10569
+ );
10570
+ } else if (clickFailureKind === "stale") {
10571
+ lines.push(
10572
+ `The click target was stale — the page changed since the last snapshot. Call read_page to refresh current indexes, then choose a currently listed target, inspect_element on the intended element, or answer from the results already visible. Do not ask the user to inspect or click the result for you.`
10573
+ );
10574
+ } else {
10575
+ lines.push(
10576
+ `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.`
10577
+ );
10578
+ }
10355
10579
  if (failedClickCount >= 2) {
10356
10580
  lines.push(
10357
10581
  `You have already had multiple failed clicks without making page progress. Do not keep clicking similar search result titles. Use the latest read_page/search result text to answer, or inspect a specific indexed result/control only if essential.`
@@ -10562,8 +10786,7 @@ class CodexProvider {
10562
10786
  let flightPriceEvidenceRecoveryCount = 0;
10563
10787
  let correctionCount = 0;
10564
10788
  const recentToolSignatures = [];
10565
- const recentToolNames = [];
10566
- let clickReadLoopNudged = false;
10789
+ const clickReadLoopGuard = new ClickReadLoopGuard();
10567
10790
  let latestToolResultPreview = null;
10568
10791
  let failedClickCountSinceProgress = 0;
10569
10792
  const searchLoopGuard = new SearchLoopGuard(isRealProgressTool);
@@ -10587,11 +10810,10 @@ class CodexProvider {
10587
10810
  (item) => item.type === "function_call"
10588
10811
  );
10589
10812
  if (functionCalls.length === 0) {
10590
- const recoveredTextCalls = recoverTextEncodedToolCalls(
10813
+ const recoveredCalls = recoverAssistantTextToolCalls(
10591
10814
  result.text,
10592
10815
  availableToolNames
10593
10816
  );
10594
- const recoveredCalls = recoveredTextCalls.length > 0 ? recoveredTextCalls : recoverNarratedActionToolCalls(result.text, availableToolNames);
10595
10817
  if (recoveredCalls.length > 0) {
10596
10818
  if (result.text.trim()) onChunk("<<erase_prev>>");
10597
10819
  functionCalls = recoveredCalls.map((toolCall) => ({
@@ -10720,6 +10942,22 @@ ${latestToolResultPreview || ""}`
10720
10942
  correctionCount += 1;
10721
10943
  continue;
10722
10944
  }
10945
+ const clickLoopPreflight = clickReadLoopGuard.beforeTool(
10946
+ prepared.prepared.name
10947
+ );
10948
+ if (clickLoopPreflight?.kind === "suppress") {
10949
+ onChunk(`
10950
+ <<tool:click:↻ loop suppressed>>
10951
+ `);
10952
+ const suppressed = createCodexToolOutput(
10953
+ prepared.prepared.callId,
10954
+ clickLoopPreflight.message
10955
+ );
10956
+ currentInput.push(suppressed);
10957
+ latestToolResultPreview = previewToolResult(suppressed.output);
10958
+ correctionCount += 1;
10959
+ continue;
10960
+ }
10723
10961
  const output = await executePreparedCodexFunctionCall(
10724
10962
  prepared.prepared,
10725
10963
  onChunk,
@@ -10747,7 +10985,8 @@ ${latestToolResultPreview || ""}`
10747
10985
  buildCodexFailedClickRecoveryInput(
10748
10986
  summarizeToolArg(prepared.prepared.args),
10749
10987
  latestToolResultPreview,
10750
- failedClickCountSinceProgress
10988
+ failedClickCountSinceProgress,
10989
+ outputText
10751
10990
  )
10752
10991
  );
10753
10992
  }
@@ -10755,17 +10994,16 @@ ${latestToolResultPreview || ""}`
10755
10994
  if (recentToolSignatures.length > 4) {
10756
10995
  recentToolSignatures.shift();
10757
10996
  }
10758
- recentToolNames.push(prepared.prepared.name);
10759
- if (recentToolNames.length > 8) recentToolNames.shift();
10760
- if (!clickReadLoopNudged && recentToolNames.length >= 6 && isClickReadLoop(recentToolNames)) {
10761
- clickReadLoopNudged = true;
10997
+ const clickLoopIntervention = clickReadLoopGuard.afterToolResult(
10998
+ prepared.prepared.name,
10999
+ outputText,
11000
+ toolSucceeded
11001
+ );
11002
+ if (clickLoopIntervention?.kind === "nudge") {
10762
11003
  currentInput.push({
10763
11004
  type: "message",
10764
11005
  role: "user",
10765
- content: [{
10766
- type: "input_text",
10767
- text: `[System] You are alternating between click and read_page without advancing the task. The click result already includes a page snapshot when it navigates, so do not read_page after every click. If you need detail on a specific element, use inspect_element. Otherwise continue the original task directly.`
10768
- }]
11006
+ content: [{ type: "input_text", text: clickLoopIntervention.message }]
10769
11007
  });
10770
11008
  }
10771
11009
  correctionCount = 0;
@@ -11989,8 +12227,7 @@ function pageBusyError(action) {
11989
12227
  return `Error: Page is still busy; ${action} timed out waiting for page scripts. Retry in a moment.`;
11990
12228
  }
11991
12229
  async function loadPermittedUrl(wc, url) {
11992
- const { assertPermittedNavigationURL: assertPermittedNavigationURL2 } = await Promise.resolve().then(() => urlSafety);
11993
- assertPermittedNavigationURL2(url);
12230
+ assertPermittedNavigationURL(url);
11994
12231
  await wc.loadURL(url);
11995
12232
  }
11996
12233
  async function executePageScript(wc, script, options) {
@@ -16330,6 +16567,60 @@ async function clickElement(wc, selector) {
16330
16567
  }));
16331
16568
  }
16332
16569
 
16570
+ // Sum offsetTop up the offsetParent chain until reaching "container",
16571
+ // giving the element's vertical position within that scroll container.
16572
+ function offsetTopWithin(el, container) {
16573
+ let top = 0;
16574
+ let node = el;
16575
+ while (node && node !== container) {
16576
+ top += node.offsetTop || 0;
16577
+ node = node.offsetParent;
16578
+ }
16579
+ return top;
16580
+ }
16581
+
16582
+ function nearestScrollableAncestor(el) {
16583
+ let node = el.parentElement;
16584
+ while (node) {
16585
+ if (node instanceof HTMLElement) {
16586
+ const style = window.getComputedStyle(node);
16587
+ if (
16588
+ (style.overflowY === "auto" || style.overflowY === "scroll") &&
16589
+ node.scrollHeight > node.clientHeight
16590
+ ) {
16591
+ return node;
16592
+ }
16593
+ }
16594
+ node = node.parentElement;
16595
+ }
16596
+ return null;
16597
+ }
16598
+
16599
+ // Wait for the element to gain a non-zero layout box, polling for up to
16600
+ // maxFrames animation frames. Lazy / virtual-scroll renderers
16601
+ // (content-visibility, intersection-triggered list items) often lay out
16602
+ // a frame or two after the scroller moves. Falls back to setTimeout when
16603
+ // the window is hidden (requestAnimationFrame does not fire then).
16604
+ function waitForBox(el, maxFrames) {
16605
+ return new Promise((resolve) => {
16606
+ let frames = 0;
16607
+ const rafAvailable =
16608
+ typeof requestAnimationFrame === "function" &&
16609
+ document.visibilityState === "visible";
16610
+ const schedule = rafAvailable
16611
+ ? (cb) => requestAnimationFrame(cb)
16612
+ : (cb) => setTimeout(cb, 16);
16613
+ const check = () => {
16614
+ const r = el.getBoundingClientRect();
16615
+ if (r.width > 0 && r.height > 0) return resolve(true);
16616
+ if (frames >= maxFrames) return resolve(false);
16617
+ frames += 1;
16618
+ schedule(check);
16619
+ };
16620
+ check();
16621
+ });
16622
+ }
16623
+
16333
16624
  const el = document.querySelector(${JSON.stringify(selector)});
16334
16625
  if (!el) return { error: "Error[stale-index]: Element not found — the page may have changed. Call read_page to refresh." };
16335
16626
 
@@ -16337,21 +16628,26 @@ async function clickElement(wc, selector) {
16337
16628
  el.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
16338
16629
  }
16339
16630
 
16340
- await new Promise((resolve) => {
16341
- let settled = false;
16342
- const finish = () => {
16343
- if (settled) return;
16344
- settled = true;
16345
- resolve(undefined);
16346
- };
16347
- if (
16348
- typeof requestAnimationFrame === "function" &&
16349
- document.visibilityState === "visible"
16350
- ) {
16351
- requestAnimationFrame(() => finish());
16631
+ // Give the renderer a brief grace to lay the element out after the
16632
+ // initial scroll. Already-visible elements resolve on the first check.
16633
+ let revealed = await waitForBox(el, 4);
16634
+
16635
+ // scrollIntoView is a no-op on zero-rect elements (collapsed, lazy, or
16636
+ // virtual-scroll content). Force the nearest scrollable ancestor to bring
16637
+ // the element's offset position into view, then wait longer for the
16638
+ // renderer to produce a layout box. This recovers many hidden targets
16639
+ // without the model having to scroll manually.
16640
+ if (!revealed) {
16641
+ const scroller = nearestScrollableAncestor(el);
16642
+ if (scroller) {
16643
+ const targetTop = offsetTopWithin(el, scroller) - scroller.clientHeight / 2;
16644
+ scroller.scrollTop = Math.max(0, targetTop);
16352
16645
  }
16353
- setTimeout(finish, 32);
16354
- });
16646
+ if (el instanceof HTMLElement) {
16647
+ el.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
16648
+ }
16649
+ revealed = await waitForBox(el, 24);
16650
+ }
16355
16651
 
16356
16652
  const rect = el.getBoundingClientRect();
16357
16653
  if (rect.width <= 0 || rect.height <= 0) {
@@ -17914,9 +18210,13 @@ Go back and select a different product.`;
17914
18210
  const clickText = `Clicked: ${elInfo.text}${tagLabel}`;
17915
18211
  const clickResult = await clickElement(wc, selector);
17916
18212
  if (clickResult.startsWith("Error:")) return clickResult;
17917
- await waitForPotentialNavigation$1(wc, beforeUrl);
18213
+ const initialNavigationWaitMs = /DOM activation/i.test(clickResult) && !elInfo.href ? 800 : void 0;
18214
+ await waitForPotentialNavigation$1(wc, beforeUrl, initialNavigationWaitMs);
17918
18215
  const afterUrl = wc.getURL();
17919
18216
  if (afterUrl !== beforeUrl) {
18217
+ if (/DOM activation/i.test(clickResult)) {
18218
+ return `${clickText} -> ${afterUrl} (recovered via DOM activation)`;
18219
+ }
17920
18220
  return `${clickText} -> ${afterUrl}`;
17921
18221
  }
17922
18222
  const overlayHint = await detectPostClickOverlay(wc);
@@ -17943,6 +18243,9 @@ ${overlayHint}`;
17943
18243
  }
17944
18244
  return `${clickText} (${clickResult})${await buildCartSuccessSuffix(wc, beforeUrl)}`;
17945
18245
  }
18246
+ if (/DOM activation/i.test(clickResult) && (!elInfo.href || elInfo.target === "_blank")) {
18247
+ return `${clickText} (${clickResult})`;
18248
+ }
17946
18249
  const activationResult = await activateElement(wc, selector);
17947
18250
  if (!activationResult.startsWith("Error:")) {
17948
18251
  await waitForPotentialNavigation$1(wc, beforeUrl);
@@ -23312,7 +23615,10 @@ Recent checkpoints:
23312
23615
  ${input.recentCheckpoints || "- none"}
23313
23616
 
23314
23617
  Task tracker:
23315
- ${input.taskTrackerContext || "- none"}`;
23618
+ ${input.taskTrackerContext || "- none"}
23619
+
23620
+ Task memory:
23621
+ ${input.taskMemoryContext || "- none"}`;
23316
23622
  }
23317
23623
  function buildAgentSystemPrompt(input) {
23318
23624
  const instructionBlocks = input.profile === "compact" ? [
@@ -23693,6 +23999,7 @@ async function handleAIQuery(query, provider, activeWebContents, onChunk, onEnd,
23693
23999
  const runtimeState = runtime2.getState();
23694
24000
  const recentCheckpoints = runtimeState.checkpoints.slice(-3).map((item) => `- ${item.name} (${item.id})`).join("\n");
23695
24001
  const taskTrackerContext = runtime2.getTaskTrackerContext();
24002
+ const taskMemoryContext = runtime2.getTaskMemoryContext();
23696
24003
  const activeTabTitle = pageContent.title || "(untitled)";
23697
24004
  const activeTabUrl = pageContent.url || activeWebContents.getURL();
23698
24005
  const allTabs = tabManager.getAllStates();
@@ -23711,7 +24018,8 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
23711
24018
  approvalMode: runtimeState.supervisor.approvalMode,
23712
24019
  pendingApprovals: runtimeState.supervisor.pendingApprovals.length,
23713
24020
  recentCheckpoints: recentCheckpoints || "- none",
23714
- taskTrackerContext: taskTrackerContext || "- none"
24021
+ taskTrackerContext: taskTrackerContext || "- none",
24022
+ taskMemoryContext: taskMemoryContext || "- none"
23715
24023
  });
23716
24024
  const actionCtx = {
23717
24025
  tabManager,
@@ -23740,6 +24048,9 @@ All open tabs: ${allTabs.map((t) => `${t.id === activeTabId ? "→ " : ""}${t.ti
23740
24048
  let isError = false;
23741
24049
  try {
23742
24050
  output = await executeAction(name, args, actionCtx);
24051
+ if (/^\s*Error:/i.test(output)) {
24052
+ isError = true;
24053
+ }
23743
24054
  if (provider.agentToolProfile === "compact") {
23744
24055
  runtime2.updateTaskTracker(name, output);
23745
24056
  const trackerCtx = runtime2.getTaskTrackerContext();
@@ -24311,6 +24622,17 @@ function setMcpHealth(update) {
24311
24622
  }
24312
24623
  const ApprovalModeSchema = zod.z.enum(["auto", "confirm-dangerous", "manual"]);
24313
24624
  const CheckpointIdSchema = zod.z.string().min(1);
24625
+ const TaskTextSchema = zod.z.string().trim().min(1).max(2e4);
24626
+ const OptionalTaskTextSchema = zod.z.string().trim().max(2e4).optional();
24627
+ const OptionalNullableTaskTextSchema = zod.z.string().trim().max(2e4).nullable().optional();
24628
+ const TaskFactsSchema = zod.z.record(
24629
+ zod.z.string().trim().min(1).max(200),
24630
+ zod.z.string().max(2e4)
24631
+ );
24632
+ const TaskMemoryPatchSchema = zod.z.object({
24633
+ nextStep: OptionalNullableTaskTextSchema,
24634
+ facts: TaskFactsSchema.optional()
24635
+ });
24314
24636
  function registerAgentRuntimeHandlers(runtime2, chromeView, sidebarView, sendToRendererViews) {
24315
24637
  let runtimeUpdateTimer = null;
24316
24638
  let pendingRuntimeState = null;
@@ -24403,6 +24725,45 @@ function registerAgentRuntimeHandlers(runtime2, chromeView, sidebarView, sendToR
24403
24725
  return runtime2.restoreSession(snapshot2);
24404
24726
  }
24405
24727
  );
24728
+ electron.ipcMain.handle(Channels.AGENT_TASK_START, (event, goal) => {
24729
+ assertTrustedIpcSender(event);
24730
+ return runtime2.startTaskMemory(parseIpc(TaskTextSchema, goal, "goal"));
24731
+ });
24732
+ electron.ipcMain.handle(
24733
+ Channels.AGENT_TASK_UPDATE,
24734
+ (event, patch) => {
24735
+ assertTrustedIpcSender(event);
24736
+ return runtime2.updateTaskMemory(
24737
+ parseIpc(TaskMemoryPatchSchema, patch ?? {}, "patch")
24738
+ );
24739
+ }
24740
+ );
24741
+ electron.ipcMain.handle(Channels.AGENT_TASK_NOTE, (event, text) => {
24742
+ assertTrustedIpcSender(event);
24743
+ return runtime2.addTaskNote(parseIpc(TaskTextSchema, text, "text"));
24744
+ });
24745
+ electron.ipcMain.handle(Channels.AGENT_TASK_BLOCKER, (event, blocker) => {
24746
+ assertTrustedIpcSender(event);
24747
+ const validated = blocker == null ? null : parseIpc(OptionalNullableTaskTextSchema, blocker, "blocker");
24748
+ return runtime2.setTaskBlocker(validated ?? null);
24749
+ });
24750
+ electron.ipcMain.handle(Channels.AGENT_TASK_RESOLVE, (event, summary) => {
24751
+ assertTrustedIpcSender(event);
24752
+ return runtime2.resolveTaskMemory(
24753
+ summary == null ? void 0 : parseIpc(OptionalTaskTextSchema, summary, "summary")
24754
+ );
24755
+ });
24756
+ electron.ipcMain.handle(Channels.AGENT_TASK_ABANDON, (event, reason) => {
24757
+ assertTrustedIpcSender(event);
24758
+ return runtime2.abandonTaskMemory(
24759
+ reason == null ? void 0 : parseIpc(OptionalTaskTextSchema, reason, "reason")
24760
+ );
24761
+ });
24762
+ electron.ipcMain.handle(Channels.AGENT_TASK_CLEAR, (event) => {
24763
+ assertTrustedIpcSender(event);
24764
+ runtime2.clearTaskMemory();
24765
+ return null;
24766
+ });
24406
24767
  }
24407
24768
  function asTextResponse$1(text) {
24408
24769
  return { content: [{ type: "text", text }] };
@@ -28129,6 +28490,137 @@ ${flow.steps.map((s, i) => ` ${i === 0 ? "→" : " "} ${s.label}`).join("\n")}`
28129
28490
  }
28130
28491
  );
28131
28492
  }
28493
+ function registerTaskMemoryTools(server, _tabManager, runtime2) {
28494
+ server.registerTool(
28495
+ "task_start",
28496
+ {
28497
+ title: "Start Task",
28498
+ 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.",
28499
+ inputSchema: {
28500
+ goal: zod.z.string().describe("What this task aims to accomplish"),
28501
+ nextStep: zod.z.string().optional().describe("The first step you plan to take"),
28502
+ facts: zod.z.record(zod.z.string()).optional().describe("Key-value facts relevant to this task (e.g. { username: alice })")
28503
+ }
28504
+ },
28505
+ async ({ goal, nextStep, facts }) => {
28506
+ const task = runtime2.startTaskMemory(goal, {
28507
+ nextStep: nextStep ?? null,
28508
+ facts: facts ?? {}
28509
+ });
28510
+ return asTextResponse(
28511
+ `Task started: ${task.goal}
28512
+ Status: ${task.status}${task.nextStep ? `
28513
+ Next step: ${task.nextStep}` : ""}`
28514
+ );
28515
+ }
28516
+ );
28517
+ server.registerTool(
28518
+ "task_update",
28519
+ {
28520
+ title: "Update Task",
28521
+ 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.",
28522
+ inputSchema: {
28523
+ nextStep: zod.z.string().optional().describe("The next step you plan to take"),
28524
+ facts: zod.z.record(zod.z.string()).optional().describe("Key-value facts to merge into the task (existing keys are overwritten)")
28525
+ }
28526
+ },
28527
+ async ({ nextStep, facts }) => {
28528
+ const updated = runtime2.updateTaskMemory({
28529
+ nextStep,
28530
+ facts
28531
+ });
28532
+ if (!updated) return asTextResponse("No active task to update. Start one with task_start first.");
28533
+ return asTextResponse(
28534
+ `Task updated: ${updated.goal}
28535
+ Status: ${updated.status}${updated.nextStep ? `
28536
+ Next step: ${updated.nextStep}` : ""}${Object.keys(updated.facts).length > 0 ? `
28537
+ Facts: ${Object.entries(updated.facts).map(([k, v]) => `${k}=${v}`).join(", ")}` : ""}`
28538
+ );
28539
+ }
28540
+ );
28541
+ server.registerTool(
28542
+ "task_note",
28543
+ {
28544
+ title: "Add Task Note",
28545
+ description: "Add a note to the current task. Use this to record observations, intermediate results, or context for the human supervisor.",
28546
+ inputSchema: {
28547
+ text: zod.z.string().describe("The note text to add")
28548
+ }
28549
+ },
28550
+ async ({ text }) => {
28551
+ const updated = runtime2.addTaskNote(text);
28552
+ if (!updated) return asTextResponse("No active task to add a note to. Start one with task_start first.");
28553
+ return asTextResponse(`Note added to task: ${updated.goal}`);
28554
+ }
28555
+ );
28556
+ server.registerTool(
28557
+ "task_blocker",
28558
+ {
28559
+ title: "Set or Clear Task Blocker",
28560
+ 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.",
28561
+ inputSchema: {
28562
+ blocker: zod.z.string().optional().describe("Description of what is blocking progress. Omit or empty string to clear a blocker.")
28563
+ }
28564
+ },
28565
+ async ({ blocker }) => {
28566
+ const updated = runtime2.setTaskBlocker(blocker?.trim() || null);
28567
+ if (!updated) return asTextResponse("No active task. Start one with task_start first.");
28568
+ if (updated.blocker) {
28569
+ return asTextResponse(`Task blocked: ${updated.blocker}
28570
+ Status: ${updated.status}`);
28571
+ }
28572
+ return asTextResponse(`Blocker cleared. Task: ${updated.goal}
28573
+ Status: ${updated.status}`);
28574
+ }
28575
+ );
28576
+ server.registerTool(
28577
+ "task_resolve",
28578
+ {
28579
+ title: "Resolve Task",
28580
+ description: "Mark the current task as completed. Optionally add a summary note. Use this when the task goal has been achieved.",
28581
+ inputSchema: {
28582
+ summary: zod.z.string().optional().describe("Brief summary of the completed task")
28583
+ }
28584
+ },
28585
+ async ({ summary }) => {
28586
+ const resolved = runtime2.resolveTaskMemory(summary);
28587
+ if (!resolved) return asTextResponse("No active task to resolve. Start one with task_start first.");
28588
+ return asTextResponse(
28589
+ `Task completed: ${resolved.goal}${resolved.notes.length > 0 ? `
28590
+ Notes: ${resolved.notes.length} note(s)` : ""}`
28591
+ );
28592
+ }
28593
+ );
28594
+ server.registerTool(
28595
+ "task_abandon",
28596
+ {
28597
+ title: "Abandon Task",
28598
+ description: "Mark the current task as abandoned. Use this when the task cannot be completed or is no longer relevant.",
28599
+ inputSchema: {
28600
+ reason: zod.z.string().optional().describe("Reason for abandoning the task")
28601
+ }
28602
+ },
28603
+ async ({ reason }) => {
28604
+ const abandoned = runtime2.abandonTaskMemory(reason);
28605
+ if (!abandoned) return asTextResponse("No active task to abandon. Start one with task_start first.");
28606
+ return asTextResponse(
28607
+ `Task abandoned: ${abandoned.goal}${reason ? ` (${reason})` : ""}`
28608
+ );
28609
+ }
28610
+ );
28611
+ server.registerTool(
28612
+ "task_status",
28613
+ {
28614
+ title: "Task Status",
28615
+ description: "Check the current task memory status including goal, progress, notes, and blocker."
28616
+ },
28617
+ async () => {
28618
+ const ctx = runtime2.getTaskMemoryContext();
28619
+ if (!ctx) return asTextResponse("No active task. Start one with task_start.");
28620
+ return asTextResponse(ctx);
28621
+ }
28622
+ );
28623
+ }
28132
28624
  function registerMacroTools(server, tabManager, runtime2) {
28133
28625
  server.registerTool(
28134
28626
  "fill_form",
@@ -29429,6 +29921,7 @@ function registerTools(server, tabManager, runtime2) {
29429
29921
  registerSessionTools(server, tabManager, runtime2);
29430
29922
  registerMemoryTools(server, tabManager, runtime2);
29431
29923
  registerFlowTools(server, tabManager, runtime2);
29924
+ registerTaskMemoryTools(server, tabManager, runtime2);
29432
29925
  registerMacroTools(server, tabManager, runtime2);
29433
29926
  registerVaultTools(server, tabManager);
29434
29927
  registerMetricsTools(server, tabManager, runtime2);
@@ -32672,6 +33165,121 @@ function formatTaskTracker(state2) {
32672
33165
  ${lines.join("\n")}
32673
33166
  ---`;
32674
33167
  }
33168
+ function createTaskMemory(goal, options = {}) {
33169
+ const now = (/* @__PURE__ */ new Date()).toISOString();
33170
+ return {
33171
+ id: crypto$1.randomUUID(),
33172
+ goal: goal.trim(),
33173
+ status: "active",
33174
+ blocker: null,
33175
+ notes: [],
33176
+ nextStep: options.nextStep?.trim() || null,
33177
+ facts: { ...options.facts ?? {} },
33178
+ startedAt: now,
33179
+ updatedAt: now,
33180
+ completedAt: null
33181
+ };
33182
+ }
33183
+ function updateTaskMemory(task, patch) {
33184
+ const updated = {
33185
+ ...task,
33186
+ nextStep: patch.nextStep !== void 0 ? patch.nextStep : task.nextStep,
33187
+ facts: {
33188
+ ...task.facts,
33189
+ ...patch.facts ?? {}
33190
+ },
33191
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
33192
+ };
33193
+ return updated;
33194
+ }
33195
+ function addTaskNote(task, text) {
33196
+ const note = {
33197
+ id: crypto$1.randomUUID(),
33198
+ text: text.trim(),
33199
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
33200
+ };
33201
+ const notes = [...task.notes, note].slice(-50);
33202
+ return {
33203
+ ...task,
33204
+ notes,
33205
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
33206
+ };
33207
+ }
33208
+ function setTaskBlocker(task, blocker) {
33209
+ const status = blocker ? "blocked" : task.status === "blocked" ? "active" : task.status;
33210
+ return {
33211
+ ...task,
33212
+ status,
33213
+ blocker,
33214
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
33215
+ };
33216
+ }
33217
+ function resolveTaskMemory(task, summary) {
33218
+ const now = (/* @__PURE__ */ new Date()).toISOString();
33219
+ let notes = task.notes;
33220
+ if (summary?.trim()) {
33221
+ const note = {
33222
+ id: crypto$1.randomUUID(),
33223
+ text: summary.trim(),
33224
+ createdAt: now
33225
+ };
33226
+ notes = [...task.notes, note].slice(-50);
33227
+ }
33228
+ return {
33229
+ ...task,
33230
+ status: "completed",
33231
+ blocker: null,
33232
+ notes,
33233
+ completedAt: now,
33234
+ updatedAt: now
33235
+ };
33236
+ }
33237
+ function abandonTaskMemory(task, reason) {
33238
+ const now = (/* @__PURE__ */ new Date()).toISOString();
33239
+ let notes = task.notes;
33240
+ if (reason?.trim()) {
33241
+ const note = {
33242
+ id: crypto$1.randomUUID(),
33243
+ text: `Abandoned: ${reason.trim()}`,
33244
+ createdAt: now
33245
+ };
33246
+ notes = [...task.notes, note].slice(-50);
33247
+ }
33248
+ return {
33249
+ ...task,
33250
+ status: "abandoned",
33251
+ blocker: null,
33252
+ notes,
33253
+ completedAt: now,
33254
+ updatedAt: now
33255
+ };
33256
+ }
33257
+ function formatTaskMemory(task) {
33258
+ if (!task) return "";
33259
+ const lines = [
33260
+ "--- Task Memory ---",
33261
+ `Goal: ${task.goal}`,
33262
+ `Status: ${task.status}${task.blocker ? ` (blocked: ${task.blocker})` : ""}`
33263
+ ];
33264
+ if (task.nextStep) {
33265
+ lines.push(`Next step: ${task.nextStep}`);
33266
+ }
33267
+ if (Object.keys(task.facts).length > 0) {
33268
+ lines.push("Facts:");
33269
+ for (const [key2, value] of Object.entries(task.facts)) {
33270
+ lines.push(` ${key2}: ${value}`);
33271
+ }
33272
+ }
33273
+ if (task.notes.length > 0) {
33274
+ lines.push("Notes:");
33275
+ for (const note of task.notes.slice(-10)) {
33276
+ const time = note.createdAt.slice(11, 16);
33277
+ lines.push(` [${time}] ${note.text}`);
33278
+ }
33279
+ }
33280
+ lines.push("---");
33281
+ return lines.join("\n");
33282
+ }
32675
33283
  const MAX_TRANSCRIPT_TEXT_LENGTH = 8e3;
32676
33284
  const PERSIST_DEBOUNCE_MS = 500;
32677
33285
  const INTERRUPTED_ACTION_STATUSES = /* @__PURE__ */ new Set([
@@ -32701,6 +33309,7 @@ function getRuntimeStatePath() {
32701
33309
  }
32702
33310
  function sanitizePersistence(persisted) {
32703
33311
  const recoveredAt = (/* @__PURE__ */ new Date()).toISOString();
33312
+ const persistedTaskMemory = persisted?.taskMemory?.completedAt ? null : persisted?.taskMemory ?? null;
32704
33313
  const actions = Array.isArray(persisted?.actions) ? persisted.actions.slice(-120).map(
32705
33314
  (action) => INTERRUPTED_ACTION_STATUSES.has(action.status) ? {
32706
33315
  ...action,
@@ -32722,7 +33331,8 @@ function sanitizePersistence(persisted) {
32722
33331
  transcript: [],
32723
33332
  mcpStatus: "stopped",
32724
33333
  flowState: null,
32725
- taskTracker: null
33334
+ taskTracker: null,
33335
+ taskMemory: persistedTaskMemory
32726
33336
  };
32727
33337
  }
32728
33338
  class AgentRuntime {
@@ -32809,7 +33419,8 @@ class AgentRuntime {
32809
33419
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
32810
33420
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
32811
33421
  note: note?.trim() || void 0,
32812
- snapshot: snapshot2
33422
+ snapshot: snapshot2,
33423
+ taskMemory: this.state.taskMemory ? clone(this.state.taskMemory) : null
32813
33424
  };
32814
33425
  this.state.checkpoints = [...this.state.checkpoints, checkpoint].slice(
32815
33426
  -20
@@ -32822,6 +33433,7 @@ class AgentRuntime {
32822
33433
  const checkpoint = this.state.checkpoints.find((item) => item.id === checkpointId) || null;
32823
33434
  if (!checkpoint) return null;
32824
33435
  this.tabManager.restoreSession(checkpoint.snapshot);
33436
+ this.state.taskMemory = checkpoint.taskMemory ? clone(checkpoint.taskMemory) : null;
32825
33437
  this.captureSession(`Restored ${checkpoint.name}`);
32826
33438
  return clone(checkpoint);
32827
33439
  }
@@ -32938,6 +33550,54 @@ class AgentRuntime {
32938
33550
  getTaskTrackerContext() {
32939
33551
  return formatTaskTracker(this.state.taskTracker);
32940
33552
  }
33553
+ // --- Task Memory ---
33554
+ startTaskMemory(goal, options) {
33555
+ this.state.taskMemory = createTaskMemory(goal, options);
33556
+ this.emit();
33557
+ return clone(this.state.taskMemory);
33558
+ }
33559
+ updateTaskMemory(patch) {
33560
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33561
+ this.state.taskMemory = updateTaskMemory(this.state.taskMemory, patch);
33562
+ this.emit();
33563
+ return clone(this.state.taskMemory);
33564
+ }
33565
+ addTaskNote(text) {
33566
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33567
+ this.state.taskMemory = addTaskNote(this.state.taskMemory, text);
33568
+ this.emit();
33569
+ return clone(this.state.taskMemory);
33570
+ }
33571
+ setTaskBlocker(blocker) {
33572
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33573
+ this.state.taskMemory = setTaskBlocker(
33574
+ this.state.taskMemory,
33575
+ blocker
33576
+ );
33577
+ this.emit();
33578
+ return clone(this.state.taskMemory);
33579
+ }
33580
+ resolveTaskMemory(summary) {
33581
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33582
+ const resolved = resolveTaskMemory(this.state.taskMemory, summary);
33583
+ this.state.taskMemory = null;
33584
+ this.emit();
33585
+ return clone(resolved);
33586
+ }
33587
+ abandonTaskMemory(reason) {
33588
+ if (!this.state.taskMemory || this.state.taskMemory.completedAt) return null;
33589
+ const abandoned = abandonTaskMemory(this.state.taskMemory, reason);
33590
+ this.state.taskMemory = null;
33591
+ this.emit();
33592
+ return clone(abandoned);
33593
+ }
33594
+ clearTaskMemory() {
33595
+ this.state.taskMemory = null;
33596
+ this.emit();
33597
+ }
33598
+ getTaskMemoryContext() {
33599
+ return formatTaskMemory(this.state.taskMemory);
33600
+ }
32941
33601
  // --- Speedee Flow State ---
32942
33602
  startFlow(goal, steps, startUrl) {
32943
33603
  const flow = {
@@ -33163,7 +33823,8 @@ ${progress}
33163
33823
  lastError: this.state.supervisor.lastError
33164
33824
  },
33165
33825
  actions: this.state.actions.slice(-120),
33166
- checkpoints: this.state.checkpoints.slice(-20)
33826
+ checkpoints: this.state.checkpoints.slice(-20),
33827
+ taskMemory: this.state.taskMemory
33167
33828
  };
33168
33829
  return fs$1.promises.mkdir(path.dirname(getRuntimeStatePath()), { recursive: true }).then(
33169
33830
  () => fs$1.promises.writeFile(