agentic-browser 2.5.1 → 2.6.1

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.
@@ -4,7 +4,7 @@ import fs from "node:fs";
4
4
  import net from "node:net";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
- import WebSocket, { WebSocketServer } from "ws";
7
+ import WebSocket from "ws";
8
8
  import crypto from "node:crypto";
9
9
  import { URL as URL$1 } from "node:url";
10
10
  import { z } from "zod";
@@ -24,6 +24,7 @@ function discoverChrome(explicitPath) {
24
24
  //#endregion
25
25
  //#region src/session/browser-controller.ts
26
26
  var CdpConnection = class CdpConnection {
27
+ ws;
27
28
  nextId = 0;
28
29
  pendingRequests = /* @__PURE__ */ new Map();
29
30
  pendingEvents = /* @__PURE__ */ new Map();
@@ -151,6 +152,26 @@ var CdpConnection = class CdpConnection {
151
152
  this.pendingEvents.set(method, waiters);
152
153
  });
153
154
  }
155
+ /**
156
+ * Resolve on the first event of `method` whose params satisfy `predicate`.
157
+ * Uses the persistent onEvent channel so non-matching events (e.g. subframe
158
+ * navigations) don't consume the wait.
159
+ */
160
+ waitForEventMatching(method, predicate, timeoutMs = 5e3) {
161
+ return new Promise((resolve, reject) => {
162
+ let unsubscribe = () => {};
163
+ const timeout = setTimeout(() => {
164
+ unsubscribe();
165
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for ${method}`));
166
+ }, timeoutMs);
167
+ unsubscribe = this.onEvent(method, (params) => {
168
+ if (!predicate(params)) return;
169
+ clearTimeout(timeout);
170
+ unsubscribe();
171
+ resolve(params);
172
+ });
173
+ });
174
+ }
154
175
  onEvent(method, handler) {
155
176
  const handlers = this.eventHandlers.get(method) ?? /* @__PURE__ */ new Set();
156
177
  const wrapped = (params) => {
@@ -231,7 +252,12 @@ async function createTarget(cdpUrl, url = "about:blank") {
231
252
  * Apply stealth patches that remove common automation fingerprints.
232
253
  * These run before any page loads so sites cannot detect the bot.
233
254
  */
234
- const STEALTH_SCRIPT = `
255
+ const STEALTH_SCRIPT = `(() => {
256
+ // Guard: registered once per CDP session, so re-connects would re-run the patches
257
+ // and the non-configurable defineProperty calls below would throw.
258
+ if (window.__agenticStealthApplied) return;
259
+ window.__agenticStealthApplied = true;
260
+
235
261
  // Remove navigator.webdriver flag
236
262
  Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
237
263
 
@@ -273,8 +299,13 @@ const STEALTH_SCRIPT = `
273
299
  return origQuery.call(this, params);
274
300
  };
275
301
  }
276
- `;
277
- /** Verify the page is ready, apply stealth, and optionally set a custom user-agent. */
302
+ })();`;
303
+ /**
304
+ * Verify the page is ready, apply stealth, and optionally set a custom user-agent.
305
+ * Returns the user-agent override that was applied (if any) so callers can re-apply
306
+ * it on later CDP sessions — script/UA overrides are scoped to the session that set
307
+ * them and die when this bootstrap connection closes.
308
+ */
278
309
  async function initTarget(targetWsUrl, userAgent) {
279
310
  const conn = await CdpConnection.connect(targetWsUrl);
280
311
  try {
@@ -291,18 +322,38 @@ async function initTarget(targetWsUrl, userAgent) {
291
322
  returnByValue: true,
292
323
  awaitPromise: true
293
324
  });
294
- if (userAgent) await conn.send("Network.setUserAgentOverride", { userAgent });
295
- else try {
325
+ if (userAgent) {
326
+ await conn.send("Network.setUserAgentOverride", { userAgent });
327
+ return userAgent;
328
+ }
329
+ try {
296
330
  const { userAgent: currentUA } = await conn.send("Browser.getVersion");
297
331
  if (currentUA?.includes("HeadlessChrome")) {
298
332
  const cleanUA = currentUA.replace(/HeadlessChrome/g, "Chrome");
299
333
  await conn.send("Network.setUserAgentOverride", { userAgent: cleanUA });
334
+ return cleanUA;
300
335
  }
301
336
  } catch {}
337
+ return;
302
338
  } finally {
303
339
  conn.close();
304
340
  }
305
341
  }
342
+ /**
343
+ * Run Runtime.evaluate and surface in-page exceptions as errors.
344
+ * CDP reports page-side throws via `exceptionDetails` on an otherwise successful
345
+ * response — callers that read `result.value` directly would treat a throw as an
346
+ * empty (successful) result.
347
+ */
348
+ async function evaluateOnPage(conn, params) {
349
+ const result = await conn.send("Runtime.evaluate", params);
350
+ const details = result.exceptionDetails;
351
+ if (details) {
352
+ const raw = details.exception?.description ?? (typeof details.exception?.value === "string" ? details.exception.value : void 0) ?? details.text ?? "Page evaluation failed";
353
+ throw new Error(raw.split("\n")[0].replace(/^Error:\s*/, ""));
354
+ }
355
+ return result;
356
+ }
306
357
  async function getFreePort() {
307
358
  return await new Promise((resolve, reject) => {
308
359
  const server = net.createServer();
@@ -535,7 +586,7 @@ ${LOCATOR_RUNTIME_HELPERS}
535
586
  ${body}
536
587
  })()`;
537
588
  }
538
- const SPECIAL_KEY_PAYLOADS = new Map([
589
+ const SPECIAL_KEY_PAYLOADS = /* @__PURE__ */ new Map([
539
590
  ["Enter", {
540
591
  key: "Enter",
541
592
  code: "Enter",
@@ -700,9 +751,28 @@ function buildKeyDispatchPayload(inputKey) {
700
751
  /** Connection pool configuration */
701
752
  const CONNECTION_IDLE_TIMEOUT_MS = 300 * 1e3;
702
753
  const CONNECTION_CLEANUP_INTERVAL_MS = 6e4;
754
+ /**
755
+ * True when the error means the socket was closed/not-open — i.e. the request
756
+ * was never delivered, so re-running it cannot double-apply a side effect.
757
+ * Deliberately excludes CDP timeouts: a timed-out request may have reached
758
+ * Chrome, so replaying it for a non-idempotent op could fire the action twice.
759
+ */
760
+ function isConnectionError(error) {
761
+ const message = error instanceof Error ? error.message : String(error);
762
+ return message.includes("CDP connection closed") || message.includes("WebSocket not open");
763
+ }
703
764
  var ChromeCdpBrowserController = class {
765
+ baseDir;
766
+ connectionFactory;
704
767
  connections = /* @__PURE__ */ new Map();
705
768
  cleanupInterval;
769
+ /**
770
+ * User-agent override resolved at target init, keyed by target ws URL.
771
+ * Stealth script + UA override are scoped to the CDP session that set them,
772
+ * so every pooled connection must re-apply them (initTarget's bootstrap
773
+ * connection closes right after launch).
774
+ */
775
+ userAgentOverrides = /* @__PURE__ */ new Map();
706
776
  constructor(baseDir, connectionFactory = CdpConnection.connect) {
707
777
  this.baseDir = baseDir;
708
778
  this.connectionFactory = connectionFactory;
@@ -724,6 +794,7 @@ var ChromeCdpBrowserController = class {
724
794
  return cached.conn;
725
795
  }
726
796
  const conn = await this.connectionFactory(targetWsUrl);
797
+ await this.applySessionOverrides(conn, targetWsUrl);
727
798
  this.connections.set(targetWsUrl, {
728
799
  conn,
729
800
  enabled: {
@@ -734,6 +805,26 @@ var ChromeCdpBrowserController = class {
734
805
  });
735
806
  return conn;
736
807
  }
808
+ /**
809
+ * Re-apply session-scoped stealth patches + UA override on a fresh connection.
810
+ * Overrides are per-CDP-session, so every pooled connection must re-apply them
811
+ * — including in a fresh CLI process that reconnects to an existing target
812
+ * (the in-memory override map is empty there, so fall back to deriving one).
813
+ */
814
+ async applySessionOverrides(conn, targetWsUrl) {
815
+ try {
816
+ await conn.send("Page.addScriptToEvaluateOnNewDocument", { source: STEALTH_SCRIPT });
817
+ let userAgent = this.userAgentOverrides.get(targetWsUrl);
818
+ if (!userAgent) try {
819
+ const { userAgent: currentUA } = await conn.send("Browser.getVersion");
820
+ if (currentUA?.includes("HeadlessChrome")) {
821
+ userAgent = currentUA.replace(/HeadlessChrome/g, "Chrome");
822
+ this.userAgentOverrides.set(targetWsUrl, userAgent);
823
+ }
824
+ } catch {}
825
+ if (userAgent) await conn.send("Emulation.setUserAgentOverride", { userAgent });
826
+ } catch {}
827
+ }
737
828
  dropConnection(targetWsUrl) {
738
829
  const cached = this.connections.get(targetWsUrl);
739
830
  if (!cached) return;
@@ -751,7 +842,8 @@ var ChromeCdpBrowserController = class {
751
842
  if (!port) throw new Error(`Invalid CDP URL: could not extract port from ${cdpUrl}`);
752
843
  await waitForDebugger(port);
753
844
  const targetWsUrl = await createTarget(cdpUrl);
754
- await initTarget(targetWsUrl, options?.userAgent);
845
+ const appliedUa = await initTarget(targetWsUrl, options?.userAgent);
846
+ if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
755
847
  return {
756
848
  pid: 0,
757
849
  cdpUrl,
@@ -819,7 +911,8 @@ var ChromeCdpBrowserController = class {
819
911
  const cdpUrl = `http://127.0.0.1:${port}`;
820
912
  const targetWsUrl = await createTarget(cdpUrl, "about:blank");
821
913
  if (!child.pid) throw new Error("Failed to launch Chrome process");
822
- await initTarget(targetWsUrl, userAgent);
914
+ const appliedUa = await initTarget(targetWsUrl, userAgent);
915
+ if (appliedUa) this.userAgentOverrides.set(targetWsUrl, appliedUa);
823
916
  return {
824
917
  pid: child.pid,
825
918
  cdpUrl,
@@ -834,12 +927,22 @@ var ChromeCdpBrowserController = class {
834
927
  }
835
928
  throw new Error(lastError?.message ?? "Unable to launch Chrome");
836
929
  }
837
- /** Execute fn with a pooled connection; on failure drop connection and retry once. */
838
- async withRetry(targetWsUrl, fn) {
930
+ /**
931
+ * Execute fn with a pooled connection; on failure drop the connection and retry once.
932
+ *
933
+ * `retryOn`:
934
+ * - "any" (default): retry any error — safe for reads/idempotent work.
935
+ * - "connection": retry only when the connection itself died. Use for
936
+ * non-idempotent input (click/type/press/toggle/select): a mid-operation
937
+ * connection drop after an event was already dispatched must not replay it,
938
+ * and in-page errors (selector missing, covered element) are not transient.
939
+ */
940
+ async withRetry(targetWsUrl, fn, retryOn = "any") {
839
941
  let conn = await this.getConnection(targetWsUrl);
840
942
  try {
841
943
  return await fn(conn);
842
- } catch {
944
+ } catch (error) {
945
+ if (retryOn === "connection" && !isConnectionError(error)) throw error;
843
946
  this.dropConnection(targetWsUrl);
844
947
  conn = await this.getConnection(targetWsUrl);
845
948
  return await fn(conn);
@@ -848,11 +951,12 @@ var ChromeCdpBrowserController = class {
848
951
  async navigate(targetWsUrl, url) {
849
952
  return await this.withRetry(targetWsUrl, async (conn) => {
850
953
  await this.ensureEnabled(targetWsUrl);
851
- const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 6e3).catch(() => void 0);
852
- const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
954
+ const mainFrameNavigated = conn.waitForEventMatching ? conn.waitForEventMatching("Page.frameNavigated", (params) => !params.frame?.parentId, 6e3).catch(() => void 0) : conn.waitForEvent("Page.frameNavigated", 6e3).catch(() => void 0);
853
955
  const navResult = await conn.send("Page.navigate", { url });
854
956
  if (navResult.errorText) throw new Error(`Navigation failed: ${navResult.errorText}`);
855
- const navigatedEvent = await navigatedPromise;
957
+ const mainFrameId = navResult.frameId;
958
+ const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEventMatching ? conn.waitForEventMatching("Page.frameStoppedLoading", (params) => !mainFrameId || params.frameId === mainFrameId, 6e3) : conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
959
+ const navigatedEvent = await mainFrameNavigated;
856
960
  try {
857
961
  await loadPromise;
858
962
  } catch {}
@@ -888,7 +992,8 @@ var ChromeCdpBrowserController = class {
888
992
  });
889
993
  }
890
994
  async preparePointerTarget(conn, payload) {
891
- const expression = buildLocatorRuntimeExpression("payload", payload, `
995
+ const point = (await evaluateOnPage(conn, {
996
+ expression: buildLocatorRuntimeExpression("payload", payload, `
892
997
  const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
893
998
  const el = resolved.element;
894
999
  ensureInView(el);
@@ -905,9 +1010,7 @@ var ChromeCdpBrowserController = class {
905
1010
  }
906
1011
 
907
1012
  return { x: point.x, y: point.y };
908
- `);
909
- const point = (await conn.send("Runtime.evaluate", {
910
- expression,
1013
+ `),
911
1014
  returnByValue: true,
912
1015
  awaitPromise: true
913
1016
  })).result.value;
@@ -918,7 +1021,8 @@ var ChromeCdpBrowserController = class {
918
1021
  };
919
1022
  }
920
1023
  async prepareTypeTarget(conn, payload) {
921
- const expression = buildLocatorRuntimeExpression("payload", payload, `
1024
+ const value = (await evaluateOnPage(conn, {
1025
+ expression: buildLocatorRuntimeExpression("payload", payload, `
922
1026
  const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
923
1027
  const el = resolved.element;
924
1028
  ensureInView(el);
@@ -967,12 +1071,12 @@ var ChromeCdpBrowserController = class {
967
1071
  }
968
1072
 
969
1073
  return 'ready';
970
- `);
971
- return (await conn.send("Runtime.evaluate", {
972
- expression,
1074
+ `),
973
1075
  returnByValue: true,
974
1076
  awaitPromise: true
975
- })).result.value ?? "ready";
1077
+ })).result.value;
1078
+ if (value !== "ready" && value !== "cleared") throw new Error("Unable to resolve type target");
1079
+ return value;
976
1080
  }
977
1081
  async dispatchMouseClick(conn, x, y) {
978
1082
  await conn.send("Input.dispatchMouseEvent", {
@@ -998,7 +1102,8 @@ var ChromeCdpBrowserController = class {
998
1102
  }
999
1103
  async focusTarget(conn, payload) {
1000
1104
  if (!payload.selector) return;
1001
- const expression = buildLocatorRuntimeExpression("payload", payload, `
1105
+ await evaluateOnPage(conn, {
1106
+ expression: buildLocatorRuntimeExpression("payload", payload, `
1002
1107
  const resolved = resolveWithFallbacks(payload.selector, payload.fallbackSelectors);
1003
1108
  const el = resolved.element;
1004
1109
  ensureInView(el);
@@ -1006,9 +1111,7 @@ var ChromeCdpBrowserController = class {
1006
1111
  el.focus();
1007
1112
  }
1008
1113
  return true;
1009
- `);
1010
- await conn.send("Runtime.evaluate", {
1011
- expression,
1114
+ `),
1012
1115
  returnByValue: true,
1013
1116
  awaitPromise: true
1014
1117
  });
@@ -1035,8 +1138,8 @@ var ChromeCdpBrowserController = class {
1035
1138
  async interact(targetWsUrl, payload) {
1036
1139
  if (payload.action === "goBack") return await this.withRetry(targetWsUrl, async (conn) => {
1037
1140
  await this.ensureEnabled(targetWsUrl);
1038
- const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 3e3).catch(() => void 0);
1039
- await conn.send("Runtime.evaluate", {
1141
+ const navigatedPromise = (conn.waitForEventMatching ? conn.waitForEventMatching("Page.frameNavigated", (params) => !params.frame?.parentId, 3e3) : conn.waitForEvent("Page.frameNavigated", 3e3)).catch(() => void 0);
1142
+ await evaluateOnPage(conn, {
1040
1143
  expression: "history.back()",
1041
1144
  returnByValue: true
1042
1145
  });
@@ -1049,8 +1152,8 @@ var ChromeCdpBrowserController = class {
1049
1152
  });
1050
1153
  if (payload.action === "goForward") return await this.withRetry(targetWsUrl, async (conn) => {
1051
1154
  await this.ensureEnabled(targetWsUrl);
1052
- const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 3e3).catch(() => void 0);
1053
- await conn.send("Runtime.evaluate", {
1155
+ const navigatedPromise = (conn.waitForEventMatching ? conn.waitForEventMatching("Page.frameNavigated", (params) => !params.frame?.parentId, 3e3) : conn.waitForEvent("Page.frameNavigated", 3e3)).catch(() => void 0);
1156
+ await evaluateOnPage(conn, {
1054
1157
  expression: "history.forward()",
1055
1158
  returnByValue: true
1056
1159
  });
@@ -1100,7 +1203,7 @@ var ChromeCdpBrowserController = class {
1100
1203
  }
1101
1204
  if (await this.prepareTypeTarget(conn, payload) === "ready") await conn.send("Input.insertText", { text: payload.text ?? "" });
1102
1205
  return "typed";
1103
- });
1206
+ }, "connection");
1104
1207
  const expression = buildLocatorRuntimeExpression("payload", payload, `
1105
1208
  if (payload.action === 'waitFor') {
1106
1209
  const timeout = payload.timeoutMs ?? 2000;
@@ -1148,12 +1251,12 @@ var ChromeCdpBrowserController = class {
1148
1251
  `);
1149
1252
  return await this.withRetry(targetWsUrl, async (conn) => {
1150
1253
  await this.ensureEnabled(targetWsUrl);
1151
- return (await conn.send("Runtime.evaluate", {
1254
+ return (await evaluateOnPage(conn, {
1152
1255
  expression,
1153
1256
  returnByValue: true,
1154
1257
  awaitPromise: true
1155
1258
  })).result.value ?? "";
1156
- });
1259
+ }, "connection");
1157
1260
  }
1158
1261
  async screenshot(targetWsUrl, options) {
1159
1262
  return await this.withRetry(targetWsUrl, async (conn) => {
@@ -1172,6 +1275,7 @@ var ChromeCdpBrowserController = class {
1172
1275
  height: metrics.contentSize.height,
1173
1276
  scale: 1
1174
1277
  };
1278
+ params.captureBeyondViewport = true;
1175
1279
  }
1176
1280
  return {
1177
1281
  data: (await conn.send("Page.captureScreenshot", params)).data,
@@ -1181,7 +1285,7 @@ var ChromeCdpBrowserController = class {
1181
1285
  }
1182
1286
  async getCurrentUrl(targetWsUrl) {
1183
1287
  return await this.withRetry(targetWsUrl, async (conn) => {
1184
- return (await conn.send("Runtime.evaluate", {
1288
+ return (await evaluateOnPage(conn, {
1185
1289
  expression: "window.location.href",
1186
1290
  returnByValue: true
1187
1291
  })).result.value ?? "";
@@ -1229,7 +1333,7 @@ var ChromeCdpBrowserController = class {
1229
1333
  `);
1230
1334
  return await this.withRetry(targetWsUrl, async (conn) => {
1231
1335
  await this.ensureEnabled(targetWsUrl);
1232
- const limited = normalizeRuntimeContentPayload((await conn.send("Runtime.evaluate", {
1336
+ const limited = normalizeRuntimeContentPayload((await evaluateOnPage(conn, {
1233
1337
  expression,
1234
1338
  returnByValue: true,
1235
1339
  awaitPromise: true
@@ -1466,7 +1570,7 @@ var ChromeCdpBrowserController = class {
1466
1570
  `);
1467
1571
  const pageSummary = await this.withRetry(targetWsUrl, async (conn) => {
1468
1572
  await this.ensureEnabled(targetWsUrl);
1469
- return (await conn.send("Runtime.evaluate", {
1573
+ return (await evaluateOnPage(conn, {
1470
1574
  expression,
1471
1575
  returnByValue: true,
1472
1576
  awaitPromise: true
@@ -1538,10 +1642,11 @@ var ChromeCdpBrowserController = class {
1538
1642
  let nodes;
1539
1643
  if (selector) {
1540
1644
  const objectGroup = "agentic-browser-a11y";
1541
- const selected = await conn.send("Runtime.evaluate", {
1645
+ const selected = await evaluateOnPage(conn, {
1542
1646
  expression: buildLocatorRuntimeExpression("selector", selector, `
1543
1647
  return resolveLocator(selector);
1544
1648
  `),
1649
+ awaitPromise: true,
1545
1650
  objectGroup
1546
1651
  });
1547
1652
  const objectId = selected.result.objectId;
@@ -1932,10 +2037,12 @@ var ChromeCdpBrowserController = class {
1932
2037
  };
1933
2038
  return await this.withRetry(targetWsUrl, async (conn) => {
1934
2039
  await this.ensureEnabled(targetWsUrl);
1935
- return extract(await conn.send("Runtime.evaluate", {
2040
+ const result = await evaluateOnPage(conn, {
1936
2041
  expression,
1937
- returnByValue: true
1938
- }));
2042
+ returnByValue: true,
2043
+ awaitPromise: true
2044
+ });
2045
+ return extract(result);
1939
2046
  });
1940
2047
  }
1941
2048
  /**
@@ -2160,6 +2267,28 @@ var ChromeCdpBrowserController = class {
2160
2267
  }
2161
2268
  return results;
2162
2269
  }
2270
+ // Only click buttons that live inside something that looks like a consent
2271
+ // banner: a dialog/modal, an element with consent-ish id/class, or a
2272
+ // fixed/sticky overlay — AND whose container text actually talks about
2273
+ // cookies/consent. Without this the loose text patterns above would click
2274
+ // arbitrary page buttons ("Accept invitation", "I agree" form submits).
2275
+ const containerTextPattern = /cookie|consent|privacy|gdpr|datenschutz|einwillig|zustimm|tracking|personal data|persönliche daten|données personnelles|datos personales/i;
2276
+ function findBannerContainer(el) {
2277
+ let node = el;
2278
+ let depth = 0;
2279
+ while (node && node.nodeType === 1 && depth < 20) {
2280
+ const role = (node.getAttribute('role') || '').toLowerCase();
2281
+ if (role === 'dialog' || role === 'alertdialog') return node;
2282
+ if (node.getAttribute('aria-modal') === 'true') return node;
2283
+ const idClass = (node.id + ' ' + (typeof node.className === 'string' ? node.className : '')).toLowerCase();
2284
+ if (/cookie|consent|gdpr|cmp|privacy|datenschutz/.test(idClass)) return node;
2285
+ const style = getComputedStyle(node);
2286
+ if (style.position === 'fixed' || style.position === 'sticky') return node;
2287
+ node = node.parentElement || (node.getRootNode && node.getRootNode().host) || null;
2288
+ depth++;
2289
+ }
2290
+ return null;
2291
+ }
2163
2292
  const candidates = collectButtons(document);
2164
2293
  for (const el of candidates) {
2165
2294
  if (!isVisible(el)) continue;
@@ -2167,6 +2296,10 @@ var ChromeCdpBrowserController = class {
2167
2296
  if (text.length > 60) continue;
2168
2297
  for (const pat of patterns) {
2169
2298
  if (pat.test(text)) {
2299
+ const container = findBannerContainer(el);
2300
+ if (!container) break;
2301
+ const containerText = (container.textContent || '').slice(0, 4000);
2302
+ if (!containerTextPattern.test(containerText)) break;
2170
2303
  el.click();
2171
2304
  return JSON.stringify({ dismissed: true, method: 'text', detail: text });
2172
2305
  }
@@ -2177,7 +2310,7 @@ var ChromeCdpBrowserController = class {
2177
2310
  })()`;
2178
2311
  return await this.withRetry(targetWsUrl, async (conn) => {
2179
2312
  await this.ensureEnabled(targetWsUrl);
2180
- const raw = (await conn.send("Runtime.evaluate", {
2313
+ const raw = (await evaluateOnPage(conn, {
2181
2314
  expression,
2182
2315
  returnByValue: true
2183
2316
  })).result.value ?? "{\"dismissed\":false}";
@@ -2261,7 +2394,7 @@ var ChromeCdpBrowserController = class {
2261
2394
  })()`;
2262
2395
  return await this.withRetry(targetWsUrl, async (conn) => {
2263
2396
  await this.ensureEnabled(targetWsUrl);
2264
- const raw = (await conn.send("Runtime.evaluate", {
2397
+ const raw = (await evaluateOnPage(conn, {
2265
2398
  expression,
2266
2399
  returnByValue: true
2267
2400
  })).result.value ?? "{\"dismissed\":false}";
@@ -2282,14 +2415,21 @@ var ChromeCdpBrowserController = class {
2282
2415
  await conn.send("Accessibility.enable");
2283
2416
  await conn.send("Page.enable");
2284
2417
  const { nodes } = await conn.send("Accessibility.getFullAXTree");
2285
- const allNodes = [...nodes];
2418
+ const nodeSets = [{
2419
+ nodes,
2420
+ trusted: false
2421
+ }];
2286
2422
  try {
2287
2423
  const { frameTree } = await conn.send("Page.getFrameTree");
2288
2424
  for (const child of frameTree.childFrames ?? []) {
2289
2425
  const url = child.frame.url ?? "";
2290
- if (/consent|cookie|privacy|sourcepoint|didomi|cmp|gdpr|trustarc|consentmanager|sp_message|onetrust|usercentrics|consent-cdn/i.test(url) || url === "about:blank") try {
2426
+ const consentFrame = /consent|cookie|privacy|sourcepoint|didomi|cmp|gdpr|trustarc|consentmanager|sp_message|onetrust|usercentrics|consent-cdn/i.test(url);
2427
+ if (consentFrame || url === "about:blank") try {
2291
2428
  const frameResult = await conn.send("Accessibility.getFullAXTree", { frameId: child.frame.id });
2292
- allNodes.push(...frameResult.nodes);
2429
+ nodeSets.push({
2430
+ nodes: frameResult.nodes,
2431
+ trusted: consentFrame
2432
+ });
2293
2433
  } catch {}
2294
2434
  }
2295
2435
  } catch {}
@@ -2326,21 +2466,38 @@ var ChromeCdpBrowserController = class {
2326
2466
  /accepter(\s+alle)?/i,
2327
2467
  /aksepter(\s+alle)?/i
2328
2468
  ];
2469
+ const consentAncestorName = /cookie|consent|privacy|gdpr|datenschutz|einwillig|zustimm/i;
2470
+ const hasConsentAncestor = (byId, node) => {
2471
+ let current = node.parentId ? byId.get(node.parentId) : void 0;
2472
+ let depth = 0;
2473
+ while (current && depth < 50) {
2474
+ const role = current.role?.value;
2475
+ if (role === "dialog" || role === "alertdialog") return true;
2476
+ const name = current.name?.value;
2477
+ if (name && consentAncestorName.test(name)) return true;
2478
+ current = current.parentId ? byId.get(current.parentId) : void 0;
2479
+ depth++;
2480
+ }
2481
+ return false;
2482
+ };
2329
2483
  const candidates = [];
2330
- for (const node of allNodes) {
2331
- if (node.ignored) continue;
2332
- const role = node.role?.value;
2333
- if (role !== "button" && role !== "link") continue;
2334
- const name = node.name?.value?.trim();
2335
- if (!name || name.length > 50) continue;
2336
- if (node.properties?.some((p) => p.name === "disabled" && p.value.value === true)) continue;
2337
- for (const pat of consentPatterns) if (pat.test(name) && node.backendDOMNodeId) {
2338
- candidates.push({
2339
- nodeId: node.nodeId,
2340
- backendDOMNodeId: node.backendDOMNodeId,
2341
- name
2342
- });
2343
- break;
2484
+ for (const { nodes: setNodes, trusted } of nodeSets) {
2485
+ const byId = new Map(setNodes.map((n) => [n.nodeId, n]));
2486
+ for (const node of setNodes) {
2487
+ if (node.ignored) continue;
2488
+ const role = node.role?.value;
2489
+ if (role !== "button" && role !== "link") continue;
2490
+ const name = node.name?.value?.trim();
2491
+ if (!name || name.length > 50) continue;
2492
+ if (node.properties?.some((p) => p.name === "disabled" && p.value.value === true)) continue;
2493
+ for (const pat of consentPatterns) if (pat.test(name) && node.backendDOMNodeId) {
2494
+ if (trusted || hasConsentAncestor(byId, node)) candidates.push({
2495
+ nodeId: node.nodeId,
2496
+ backendDOMNodeId: node.backendDOMNodeId,
2497
+ name
2498
+ });
2499
+ break;
2500
+ }
2344
2501
  }
2345
2502
  }
2346
2503
  if (candidates.length === 0) return { dismissed: false };
@@ -2594,7 +2751,6 @@ z.object({
2594
2751
  //#region src/session/session-store.ts
2595
2752
  var SessionStore = class {
2596
2753
  filePath;
2597
- cache = null;
2598
2754
  constructor(baseDir) {
2599
2755
  fs.mkdirSync(baseDir, { recursive: true });
2600
2756
  this.filePath = path.join(baseDir, "sessions.json");
@@ -2662,25 +2818,35 @@ var SessionStore = class {
2662
2818
  this.write(state);
2663
2819
  }
2664
2820
  read() {
2665
- if (this.cache) return this.cache;
2821
+ let raw;
2822
+ try {
2823
+ raw = fs.readFileSync(this.filePath, "utf8");
2824
+ } catch (error) {
2825
+ if (error.code === "ENOENT") return { sessions: {} };
2826
+ throw error;
2827
+ }
2666
2828
  try {
2667
- const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
2668
- this.cache = parsed;
2669
- return parsed;
2829
+ return JSON.parse(raw);
2670
2830
  } catch {
2831
+ try {
2832
+ fs.copyFileSync(this.filePath, `${this.filePath}.corrupt`);
2833
+ } catch {}
2671
2834
  const empty = { sessions: {} };
2672
2835
  this.write(empty);
2673
2836
  return empty;
2674
2837
  }
2675
2838
  }
2676
2839
  write(state) {
2677
- this.cache = state;
2678
- fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf8");
2840
+ const tmp = `${this.filePath}.${process.pid}.tmp`;
2841
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
2842
+ fs.renameSync(tmp, this.filePath);
2679
2843
  }
2680
2844
  };
2681
2845
  //#endregion
2682
2846
  //#region src/session/session-manager.ts
2683
2847
  var SessionManager = class {
2848
+ ctx;
2849
+ browser;
2684
2850
  store;
2685
2851
  constructor(ctx, browser) {
2686
2852
  this.ctx = ctx;
@@ -2773,7 +2939,7 @@ var SessionManager = class {
2773
2939
  } else if (input.type === "terminate") {
2774
2940
  await this.terminateSession(sessionId);
2775
2941
  resultMessage = "Session terminated";
2776
- }
2942
+ } else throw new Error(`Unsupported command type: ${String(input.type)}`);
2777
2943
  } catch (error) {
2778
2944
  resultStatus = "failed";
2779
2945
  resultMessage = error.message;
@@ -2876,6 +3042,7 @@ var SessionManager = class {
2876
3042
  }
2877
3043
  async restartSession(sessionId) {
2878
3044
  const record = this.mustGetRecord(sessionId);
3045
+ if (record.session.status === "terminated") throw new Error("Session is terminated. Start a new session.");
2879
3046
  if (this.browser.closeConnection) try {
2880
3047
  this.browser.closeConnection(record.targetWsUrl);
2881
3048
  } catch {}
@@ -2907,6 +3074,7 @@ var SessionManager = class {
2907
3074
  }
2908
3075
  rotateAuthToken(sessionId) {
2909
3076
  const record = this.mustGetRecord(sessionId);
3077
+ if (record.session.status === "terminated") throw new Error("Session is terminated. Start a new session.");
2910
3078
  this.ctx.tokenService.revoke(sessionId);
2911
3079
  const nextToken = this.ctx.tokenService.issue(sessionId);
2912
3080
  const updated = {
@@ -3018,8 +3186,10 @@ var SessionManager = class {
3018
3186
  const record = this.store.get(sessionId);
3019
3187
  if (!record) throw new Error("Session not found");
3020
3188
  if (!record.targetWsUrl) throw new Error("Session target is missing. Restart the session.");
3021
- const seeded = this.ctx.tokenService.get(sessionId);
3022
- if (!seeded || seeded !== record.session.authTokenRef) this.ctx.tokenService.seed(sessionId, record.session.authTokenRef);
3189
+ if (record.session.status !== "terminated") {
3190
+ const seeded = this.ctx.tokenService.get(sessionId);
3191
+ if (!seeded || seeded !== record.session.authTokenRef) this.ctx.tokenService.seed(sessionId, record.session.authTokenRef);
3192
+ }
3023
3193
  return record;
3024
3194
  }
3025
3195
  recordEvent(sessionId, category, severity, message) {
@@ -3079,6 +3249,7 @@ var SessionManager = class {
3079
3249
  //#endregion
3080
3250
  //#region src/transport/control-api.ts
3081
3251
  var ControlApi = class {
3252
+ sessions;
3082
3253
  constructor(sessions) {
3083
3254
  this.sessions = sessions;
3084
3255
  }
@@ -3136,21 +3307,12 @@ var ControlApi = class {
3136
3307
  };
3137
3308
  //#endregion
3138
3309
  //#region src/lib/config.ts
3139
- const DEFAULT_PORT = 43111;
3140
- const DEFAULT_TIMEOUT_MS = 2e3;
3141
3310
  function loadConfig(env = process.env) {
3142
- const wsPort = Number.parseInt(env.AGENTIC_BROWSER_WS_PORT ?? `${DEFAULT_PORT}`, 10);
3143
- const commandTimeoutMs = Number.parseInt(env.AGENTIC_BROWSER_COMMAND_TIMEOUT_MS ?? `${DEFAULT_TIMEOUT_MS}`, 10);
3144
- if (Number.isNaN(wsPort) || wsPort <= 0) throw new Error("AGENTIC_BROWSER_WS_PORT must be a positive integer");
3145
- if (Number.isNaN(commandTimeoutMs) || commandTimeoutMs <= 0) throw new Error("AGENTIC_BROWSER_COMMAND_TIMEOUT_MS must be a positive integer");
3146
3311
  const userProfile = env.AGENTIC_BROWSER_USER_PROFILE;
3147
3312
  let userProfileDir;
3148
3313
  if (userProfile === "true" || userProfile === "default") userProfileDir = "default";
3149
3314
  else if (userProfile && path.isAbsolute(userProfile)) userProfileDir = userProfile;
3150
3315
  return {
3151
- host: env.AGENTIC_BROWSER_HOST ?? "127.0.0.1",
3152
- wsPort,
3153
- commandTimeoutMs,
3154
3316
  dataDir: env.AGENTIC_BROWSER_DIR ?? path.join(os.homedir(), ".agentic-browser"),
3155
3317
  browserExecutablePath: env.AGENTIC_BROWSER_CHROME_EXECUTABLE_PATH ?? env.AGENTIC_BROWSER_CHROME_PATH,
3156
3318
  cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
@@ -3162,6 +3324,7 @@ function loadConfig(env = process.env) {
3162
3324
  //#endregion
3163
3325
  //#region src/observability/logger.ts
3164
3326
  var Logger = class {
3327
+ namespace;
3165
3328
  constructor(namespace) {
3166
3329
  this.namespace = namespace;
3167
3330
  }
@@ -3198,17 +3361,28 @@ var EventStore = class {
3198
3361
  filePath;
3199
3362
  bufferedLines = [];
3200
3363
  flushTimer;
3364
+ pendingFlushDelayMs = Number.POSITIVE_INFINITY;
3201
3365
  flushInFlight;
3202
3366
  constructor(baseDir) {
3203
3367
  fs.mkdirSync(baseDir, { recursive: true });
3204
3368
  this.filePath = path.join(baseDir, "session-events.log");
3205
- process.once("beforeExit", this.flushSync);
3206
- process.once("exit", this.flushSync);
3369
+ process.on("beforeExit", this.flushSync);
3370
+ process.on("exit", this.flushSync);
3371
+ }
3372
+ /**
3373
+ * Remove the process exit handlers and flush any buffered lines.
3374
+ * Call when discarding a store (repeated context creation in tests/embedders
3375
+ * would otherwise leak a listener pair per instance).
3376
+ */
3377
+ dispose() {
3378
+ process.off("beforeExit", this.flushSync);
3379
+ process.off("exit", this.flushSync);
3380
+ this.flushSync();
3207
3381
  }
3208
3382
  append(event) {
3209
3383
  let existing = this.events.get(event.sessionId) ?? [];
3210
3384
  existing.push(event);
3211
- if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-MAX_EVENTS_PER_SESSION);
3385
+ if (existing.length > MAX_EVENTS_PER_SESSION) existing = existing.slice(-500);
3212
3386
  this.events.set(event.sessionId, existing);
3213
3387
  this.bufferedLines.push(`${JSON.stringify(event)}\n`);
3214
3388
  if (this.bufferedLines.length >= 20) {
@@ -3226,7 +3400,12 @@ var EventStore = class {
3226
3400
  }
3227
3401
  flushSoon(delayMs) {
3228
3402
  if (this.flushInFlight) return this.flushInFlight;
3229
- if (this.flushTimer) return Promise.resolve();
3403
+ if (this.flushTimer) {
3404
+ if (delayMs >= this.pendingFlushDelayMs) return Promise.resolve();
3405
+ clearTimeout(this.flushTimer);
3406
+ this.flushTimer = void 0;
3407
+ }
3408
+ this.pendingFlushDelayMs = delayMs;
3230
3409
  this.flushTimer = setTimeout(() => {
3231
3410
  this.flushTimer = void 0;
3232
3411
  this.flushAsync();
@@ -3283,40 +3462,6 @@ var SessionTokenService = class {
3283
3462
  }
3284
3463
  };
3285
3464
  //#endregion
3286
- //#region src/transport/ws-server.ts
3287
- var AuthenticatedWsServer = class {
3288
- logger = new Logger("ws-server");
3289
- server;
3290
- constructor(options) {
3291
- this.options = options;
3292
- }
3293
- start(onMessage) {
3294
- this.server = new WebSocketServer({
3295
- host: this.options.host,
3296
- port: this.options.port
3297
- });
3298
- this.server.on("connection", (socket, request) => {
3299
- const url = new URL(request.url ?? "/", `http://${this.options.host}:${this.options.port}`);
3300
- const sessionId = url.searchParams.get("sessionId");
3301
- const token = url.searchParams.get("token");
3302
- if (!sessionId || !token || !this.options.tokenService.validate(sessionId, token)) {
3303
- this.logger.warning("Rejected websocket client", { sessionId });
3304
- socket.close(4401, "Unauthorized");
3305
- return;
3306
- }
3307
- socket.on("message", (payload) => onMessage(socket, payload.toString("utf8")));
3308
- });
3309
- this.logger.info("WebSocket server started", {
3310
- host: this.options.host,
3311
- port: this.options.port
3312
- });
3313
- }
3314
- stop() {
3315
- this.server?.close();
3316
- this.server = void 0;
3317
- }
3318
- };
3319
- //#endregion
3320
3465
  //#region src/memory/memory-index.ts
3321
3466
  function normalize(value) {
3322
3467
  return value.trim().toLowerCase();
@@ -3357,10 +3502,11 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup
3357
3502
  const insightIntent = normalize(insight.taskIntent);
3358
3503
  const intentMatch = insightIntent === normalizedIntent ? 1 : 0;
3359
3504
  const intentPartial = intentMatch === 1 || insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent) ? .65 : 0;
3505
+ const intentSignal = Math.max(intentMatch, intentPartial);
3360
3506
  const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
3361
3507
  const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
3362
3508
  const selectorQuality = selectorSignal(insight);
3363
- const score = .5 * Math.max(intentMatch, intentPartial) + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
3509
+ const score = .5 * intentSignal + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
3364
3510
  const result = {
3365
3511
  insightId: insight.insightId,
3366
3512
  taskIntent: insight.taskIntent,
@@ -3379,7 +3525,10 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup
3379
3525
  cookieBanner: profile.cookieBanner,
3380
3526
  visitCount: profile.visitCount
3381
3527
  };
3382
- return result;
3528
+ return {
3529
+ result,
3530
+ intentSignal
3531
+ };
3383
3532
  }
3384
3533
  var MemoryIndex = class {
3385
3534
  /** Domain → insights index, rebuilt lazily when insight list changes. */
@@ -3406,18 +3555,20 @@ var MemoryIndex = class {
3406
3555
  const limit = input.limit ?? 10;
3407
3556
  this.ensureIndex(insights);
3408
3557
  const candidates = normalizedDomain ? this.domainIndex.get(normalizedDomain) ?? [] : insights;
3558
+ const relevant = (scored) => scored.intentSignal > 0;
3409
3559
  if (limit === 1 && normalizedDomain) {
3410
3560
  let best;
3411
3561
  for (const insight of candidates) {
3412
- const result = scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup);
3413
- if (result.score > 0 && (!best || result.score > best.score)) {
3414
- best = result;
3415
- if (best.score >= .95) break;
3562
+ const scored = scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup);
3563
+ if (!relevant(scored)) continue;
3564
+ if (!best || scored.result.score > best.result.score) {
3565
+ best = scored;
3566
+ if (best.result.score >= 1) break;
3416
3567
  }
3417
3568
  }
3418
- return best ? [best] : [];
3569
+ return best ? [best.result] : [];
3419
3570
  }
3420
- return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter((result) => result.score > 0).toSorted((a, b) => b.score - a.score).slice(0, limit);
3571
+ return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup)).filter(relevant).toSorted((a, b) => b.result.score - a.result.score).slice(0, limit).map((scored) => scored.result);
3421
3572
  }
3422
3573
  };
3423
3574
  //#endregion
@@ -3584,11 +3735,15 @@ var SiteProfileStore = class {
3584
3735
  return this.getCache().find((p) => p.domain === normalized);
3585
3736
  }
3586
3737
  upsert(profile) {
3587
- SiteProfileSchema.parse(profile);
3738
+ const normalized = {
3739
+ ...profile,
3740
+ domain: profile.domain.toLowerCase()
3741
+ };
3742
+ SiteProfileSchema.parse(normalized);
3588
3743
  const profiles = this.getCache();
3589
- const index = profiles.findIndex((p) => p.domain === profile.domain);
3590
- if (index >= 0) profiles[index] = profile;
3591
- else profiles.push(profile);
3744
+ const index = profiles.findIndex((p) => p.domain === normalized.domain);
3745
+ if (index >= 0) profiles[index] = normalized;
3746
+ else profiles.push(normalized);
3592
3747
  this.markDirty();
3593
3748
  }
3594
3749
  flushSync() {
@@ -3603,7 +3758,8 @@ var SiteProfileStore = class {
3603
3758
  }
3604
3759
  getCache() {
3605
3760
  if (this.cached) return this.cached;
3606
- this.cached = this.readDisk().profiles;
3761
+ const state = this.readDisk();
3762
+ this.cached = state.profiles;
3607
3763
  return this.cached;
3608
3764
  }
3609
3765
  markDirty() {
@@ -3626,7 +3782,7 @@ var SiteProfileStore = class {
3626
3782
  } catch {
3627
3783
  const obj = raw;
3628
3784
  if (Array.isArray(obj?.profiles)) {
3629
- const salvaged = obj.profiles.filter((item) => SiteProfileSchema.safeParse(item).success);
3785
+ const salvaged = obj.profiles.map((item) => SiteProfileSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
3630
3786
  if (salvaged.length > 0) {
3631
3787
  const state = { profiles: salvaged };
3632
3788
  this.backupAndReset();
@@ -3706,6 +3862,15 @@ var TaskInsightStore = class {
3706
3862
  else insights.push(insight);
3707
3863
  this.markDirty();
3708
3864
  }
3865
+ /** Remove an insight by id. Returns true if a record was removed. */
3866
+ remove(insightId) {
3867
+ const insights = this.getCache();
3868
+ const index = insights.findIndex((entry) => entry.insightId === insightId);
3869
+ if (index < 0) return false;
3870
+ insights.splice(index, 1);
3871
+ this.markDirty();
3872
+ return true;
3873
+ }
3709
3874
  replaceMany(insights) {
3710
3875
  for (const insight of insights) TaskInsightSchema.parse(insight);
3711
3876
  this.cached = insights;
@@ -3725,7 +3890,8 @@ var TaskInsightStore = class {
3725
3890
  /** Return the in-memory cache, loading from disk on first access. */
3726
3891
  getCache() {
3727
3892
  if (this.cached) return this.cached;
3728
- this.cached = this.readDisk().insights;
3893
+ const state = this.readDisk();
3894
+ this.cached = state.insights;
3729
3895
  return this.cached;
3730
3896
  }
3731
3897
  markDirty() {
@@ -3749,7 +3915,7 @@ var TaskInsightStore = class {
3749
3915
  } catch {
3750
3916
  const obj = raw;
3751
3917
  if (Array.isArray(obj?.insights)) {
3752
- const salvaged = obj.insights.filter((item) => TaskInsightSchema.safeParse(item).success);
3918
+ const salvaged = obj.insights.map((item) => TaskInsightSchema.safeParse(item)).filter((result) => result.success).map((result) => result.data);
3753
3919
  if (salvaged.length > 0) {
3754
3920
  const state = { insights: salvaged };
3755
3921
  this.backupAndReset();
@@ -3796,8 +3962,6 @@ var TaskInsightStore = class {
3796
3962
  //#region src/memory/memory-service.ts
3797
3963
  /** Memory service configuration */
3798
3964
  const SEARCH_CACHE_TTL_MS = 2e3;
3799
- const MAX_EVIDENCE_RECORDS = 50;
3800
- const MAX_RECIPE_STEPS = 8;
3801
3965
  const MAX_SELECTOR_ALIASES = 10;
3802
3966
  /** Site profile limits */
3803
3967
  const MAX_NAVIGATION_PATHS = 20;
@@ -3875,6 +4039,8 @@ var MemoryService = class {
3875
4039
  const now = (/* @__PURE__ */ new Date()).toISOString();
3876
4040
  const verified = {
3877
4041
  ...insight,
4042
+ freshness: "fresh",
4043
+ staleStrikeCount: 0,
3878
4044
  lastVerifiedAt: now,
3879
4045
  updatedAt: now
3880
4046
  };
@@ -3916,7 +4082,7 @@ var MemoryService = class {
3916
4082
  const refreshed = applySuccess({
3917
4083
  ...matched,
3918
4084
  useCount: matched.useCount + 1,
3919
- evidence: [...matched.evidence.slice(-(MAX_EVIDENCE_RECORDS - 1)), evidence],
4085
+ evidence: [...matched.evidence.slice(-49), evidence],
3920
4086
  actionRecipe: this.mergeRecipe(matched.actionRecipe, input.step),
3921
4087
  expectedOutcome: input.expectedOutcome
3922
4088
  });
@@ -3932,6 +4098,7 @@ var MemoryService = class {
3932
4098
  };
3933
4099
  versioned = this.maybeGenerateAliases(versioned);
3934
4100
  this.store.upsert(versioned);
4101
+ this.store.remove(matched.insightId);
3935
4102
  this.invalidateSearchCache();
3936
4103
  return versioned;
3937
4104
  }
@@ -3950,7 +4117,7 @@ var MemoryService = class {
3950
4117
  const failed = applyFailure({
3951
4118
  ...matched,
3952
4119
  useCount: matched.useCount + 1,
3953
- evidence: [...matched.evidence.slice(-(MAX_EVIDENCE_RECORDS - 1)), evidence]
4120
+ evidence: [...matched.evidence.slice(-49), evidence]
3954
4121
  }, signal);
3955
4122
  this.store.upsert(failed);
3956
4123
  this.invalidateSearchCache();
@@ -4117,26 +4284,18 @@ var MemoryService = class {
4117
4284
  }
4118
4285
  mergeRecipe(recipe, step) {
4119
4286
  if (recipe.find((existing) => existing.summary === step.summary && existing.selector === step.selector)) return recipe;
4120
- return [...recipe, step].slice(-MAX_RECIPE_STEPS);
4287
+ return [...recipe, step].slice(-8);
4121
4288
  }
4122
4289
  };
4123
4290
  //#endregion
4124
4291
  //#region src/cli/app.ts
4125
4292
  function createAppContext(env = process.env) {
4126
4293
  const config = loadConfig(env);
4127
- const logger = new Logger("app");
4128
- const eventStore = new EventStore(config.dataDir);
4129
- const tokenService = new SessionTokenService();
4130
4294
  return {
4131
4295
  config,
4132
- logger,
4133
- eventStore,
4134
- tokenService,
4135
- wsServer: new AuthenticatedWsServer({
4136
- host: config.host,
4137
- port: config.wsPort,
4138
- tokenService
4139
- }),
4296
+ logger: new Logger("app"),
4297
+ eventStore: new EventStore(config.dataDir),
4298
+ tokenService: new SessionTokenService(),
4140
4299
  memoryService: new MemoryService(config.dataDir)
4141
4300
  };
4142
4301
  }