agentic-browser 1.2.0 → 1.3.0

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/README.md CHANGED
@@ -92,10 +92,12 @@ Default profile locations per platform:
92
92
 
93
93
  These options can also be set via environment variables (CLI flags take precedence):
94
94
 
95
- | Variable | Example | Description |
96
- | ------------------------------ | ------------------------------ | ------------------------------- |
97
- | `AGENTIC_BROWSER_CDP_URL` | `http://127.0.0.1:9222` | Connect to a running Chrome |
98
- | `AGENTIC_BROWSER_USER_PROFILE` | `default` or an absolute path | Launch with a real profile |
95
+ | Variable | Example | Description |
96
+ | ------------------------------ | ------------------------------ | ------------------------------------ |
97
+ | `AGENTIC_BROWSER_CDP_URL` | `http://127.0.0.1:9222` | Connect to a running Chrome |
98
+ | `AGENTIC_BROWSER_USER_PROFILE` | `default` or an absolute path | Launch with a real profile |
99
+ | `AGENTIC_BROWSER_HEADLESS` | `true` | Run Chrome in headless mode |
100
+ | `AGENTIC_BROWSER_USER_AGENT` | `MyBot/1.0` | Override the browser user-agent |
99
101
 
100
102
  ## Agent Commands (Recommended for LLMs)
101
103
 
@@ -105,6 +107,8 @@ The `agent` subcommand manages session state, auto-restarts on disconnect, gener
105
107
  agentic-browser agent start
106
108
  agentic-browser agent start --cdp-url http://127.0.0.1:9222
107
109
  agentic-browser agent start --user-profile default
110
+ agentic-browser agent start --headless
111
+ agentic-browser agent start --user-agent "MyBot/1.0"
108
112
  agentic-browser agent status
109
113
  agentic-browser agent run navigate '{"url":"https://example.com"}'
110
114
  agentic-browser agent run interact '{"action":"click","selector":"#login"}'
@@ -184,6 +188,8 @@ For direct control without session state management:
184
188
  agentic-browser session:start
185
189
  agentic-browser session:start --cdp-url http://127.0.0.1:9222
186
190
  agentic-browser session:start --user-profile default
191
+ agentic-browser session:start --headless
192
+ agentic-browser session:start --user-agent "MyBot/1.0"
187
193
  ```
188
194
 
189
195
  ### 2. Read Session Status
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as createCliRuntime } from "../runtime-D6awVhGy.mjs";
2
+ import { r as createCliRuntime } from "../runtime-Dvmv5Xi_.mjs";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import crypto from "node:crypto";
@@ -243,9 +243,11 @@ async function main() {
243
243
  const runtime = createCliRuntime();
244
244
  const program = new Command();
245
245
  program.name("agentic-browser").description("Agentic browser CLI");
246
- program.command("session:start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").action(async (options) => {
246
+ program.command("session:start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").option("--headless", "run Chrome in headless mode (no visible window)").option("--user-agent <string>", "override the browser user-agent string").action(async (options) => {
247
247
  if (options.cdpUrl) runtime.context.config.cdpUrl = options.cdpUrl;
248
248
  if (options.userProfile) runtime.context.config.userProfileDir = options.userProfile === "true" || options.userProfile === "default" ? "default" : options.userProfile;
249
+ if (options.headless) runtime.context.config.headless = true;
250
+ if (options.userAgent) runtime.context.config.userAgent = options.userAgent;
249
251
  const result = await runSessionStart(runtime, { browser: "chrome" });
250
252
  console.log(JSON.stringify(result));
251
253
  });
@@ -310,9 +312,11 @@ async function main() {
310
312
  console.log(JSON.stringify(result));
311
313
  });
312
314
  const agent = program.command("agent").description("Stateful agent wrapper with session persistence and auto-retry");
313
- agent.command("start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").action(async (options) => {
315
+ agent.command("start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").option("--headless", "run Chrome in headless mode (no visible window)").option("--user-agent <string>", "override the browser user-agent string").action(async (options) => {
314
316
  if (options.cdpUrl) runtime.context.config.cdpUrl = options.cdpUrl;
315
317
  if (options.userProfile) runtime.context.config.userProfileDir = options.userProfile === "true" || options.userProfile === "default" ? "default" : options.userProfile;
318
+ if (options.headless) runtime.context.config.headless = true;
319
+ if (options.userAgent) runtime.context.config.userAgent = options.userAgent;
316
320
  const result = await agentStart(runtime);
317
321
  console.log(JSON.stringify(result));
318
322
  });
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-D6awVhGy.mjs";
2
+ import { i as createMockAgenticBrowserCore, n as createAgenticBrowserCore, t as AgenticBrowserCore } from "./runtime-Dvmv5Xi_.mjs";
3
3
 
4
4
  export { AgenticBrowserCore, createAgenticBrowserCore, createMockAgenticBrowserCore };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as createAgenticBrowserCore } from "../runtime-D6awVhGy.mjs";
2
+ import { n as createAgenticBrowserCore } from "../runtime-Dvmv5Xi_.mjs";
3
3
  import { z } from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -101,12 +101,34 @@ async function getJson(url) {
101
101
  if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
102
102
  return await response.json();
103
103
  }
104
+ /** Check if the debug port is accepting TCP connections (faster than an HTTP fetch). */
105
+ function probePort(port) {
106
+ return new Promise((resolve) => {
107
+ const socket = net.createConnection({
108
+ host: "127.0.0.1",
109
+ port
110
+ });
111
+ socket.once("connect", () => {
112
+ socket.destroy();
113
+ resolve(true);
114
+ });
115
+ socket.once("error", () => {
116
+ socket.destroy();
117
+ resolve(false);
118
+ });
119
+ });
120
+ }
104
121
  async function waitForDebugger(port) {
105
- for (let i = 0; i < 60; i += 1) try {
106
- await getJson(`http://127.0.0.1:${port}/json/version`);
107
- return;
108
- } catch {
109
- await new Promise((resolve) => setTimeout(resolve, 250));
122
+ const maxMs = 15e3;
123
+ const start = Date.now();
124
+ let delay = 50;
125
+ while (Date.now() - start < maxMs) {
126
+ if (await probePort(port)) try {
127
+ await getJson(`http://127.0.0.1:${port}/json/version`);
128
+ return;
129
+ } catch {}
130
+ await new Promise((resolve) => setTimeout(resolve, delay));
131
+ delay = Math.min(delay * 2, 250);
110
132
  }
111
133
  throw new Error("Chrome debug endpoint did not become ready in time");
112
134
  }
@@ -128,6 +150,15 @@ async function createTarget(cdpUrl, url = "about:blank") {
128
150
  } catch {}
129
151
  return await ensurePageWebSocketUrl(cdpUrl);
130
152
  }
153
+ async function applyUserAgent(targetWsUrl, userAgent) {
154
+ const conn = await CdpConnection.connect(targetWsUrl);
155
+ try {
156
+ await conn.send("Network.enable");
157
+ await conn.send("Network.setUserAgentOverride", { userAgent });
158
+ } finally {
159
+ conn.close();
160
+ }
161
+ }
131
162
  async function evaluateExpression(targetWsUrl, expression) {
132
163
  const conn = await CdpConnection.connect(targetWsUrl);
133
164
  try {
@@ -199,13 +230,14 @@ var ChromeCdpBrowserController = class {
199
230
  closeConnection(targetWsUrl) {
200
231
  this.dropConnection(targetWsUrl);
201
232
  }
202
- async connect(cdpUrl) {
233
+ async connect(cdpUrl, options) {
203
234
  const parsed = new URL(cdpUrl);
204
235
  const port = Number.parseInt(parsed.port, 10);
205
236
  if (!port) throw new Error(`Invalid CDP URL: could not extract port from ${cdpUrl}`);
206
237
  await waitForDebugger(port);
207
238
  const targetWsUrl = await createTarget(cdpUrl);
208
239
  await evaluateExpression(targetWsUrl, "window.location.href");
240
+ if (options?.userAgent) await applyUserAgent(targetWsUrl, options.userAgent);
209
241
  return {
210
242
  pid: 0,
211
243
  cdpUrl,
@@ -224,7 +256,8 @@ var ChromeCdpBrowserController = class {
224
256
  cached.enabled.runtime = true;
225
257
  }
226
258
  }
227
- async launch(sessionId, explicitPath, userProfileDir) {
259
+ async launch(sessionId, options) {
260
+ const { executablePath: explicitPath, userProfileDir, headless, userAgent } = options ?? {};
228
261
  const executablePath = discoverChrome(explicitPath);
229
262
  const extension = loadControlExtension();
230
263
  let profileDir;
@@ -234,7 +267,10 @@ var ChromeCdpBrowserController = class {
234
267
  fs.mkdirSync(profileDir, { recursive: true });
235
268
  const lockFile = path.join(profileDir, "SingletonLock");
236
269
  if (fs.existsSync(lockFile)) throw new Error(`Chrome profile is already in use (lock file exists: ${lockFile}). Quit the running Chrome instance first, or use --cdp-url to connect to it instead.`);
237
- const launchAttempts = [
270
+ const launchAttempts = headless ? [{
271
+ withExtension: false,
272
+ headless: true
273
+ }] : [
238
274
  {
239
275
  withExtension: true,
240
276
  headless: false
@@ -271,6 +307,7 @@ var ChromeCdpBrowserController = class {
271
307
  const targetWsUrl = await createTarget(cdpUrl, "about:blank");
272
308
  await evaluateExpression(targetWsUrl, "window.location.href");
273
309
  if (!child.pid) throw new Error("Failed to launch Chrome process");
310
+ if (userAgent) await applyUserAgent(targetWsUrl, userAgent);
274
311
  return {
275
312
  pid: child.pid,
276
313
  cdpUrl,
@@ -285,36 +322,31 @@ var ChromeCdpBrowserController = class {
285
322
  }
286
323
  throw new Error(lastError?.message ?? "Unable to launch Chrome");
287
324
  }
288
- async navigate(targetWsUrl, url) {
325
+ /** Execute fn with a pooled connection; on failure drop connection and retry once. */
326
+ async withRetry(targetWsUrl, fn) {
289
327
  let conn = await this.getConnection(targetWsUrl);
290
328
  try {
291
- await this.ensureEnabled(targetWsUrl);
292
- const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
293
- await conn.send("Page.navigate", { url });
294
- try {
295
- await loadPromise;
296
- } catch {}
297
- return (await conn.send("Runtime.evaluate", {
298
- expression: "window.location.href",
299
- returnByValue: true
300
- })).result.value ?? url;
329
+ return await fn(conn);
301
330
  } catch {
302
331
  this.dropConnection(targetWsUrl);
303
332
  conn = await this.getConnection(targetWsUrl);
333
+ return await fn(conn);
334
+ }
335
+ }
336
+ async navigate(targetWsUrl, url) {
337
+ return await this.withRetry(targetWsUrl, async (conn) => {
304
338
  await this.ensureEnabled(targetWsUrl);
339
+ const navigatedPromise = conn.waitForEvent("Page.frameNavigated", 6e3).catch(() => void 0);
305
340
  const loadPromise = Promise.race([conn.waitForEvent("Page.loadEventFired", 6e3), conn.waitForEvent("Page.frameStoppedLoading", 6e3)]);
306
341
  await conn.send("Page.navigate", { url });
342
+ const navigatedEvent = await navigatedPromise;
307
343
  try {
308
344
  await loadPromise;
309
345
  } catch {}
310
- return (await conn.send("Runtime.evaluate", {
311
- expression: "window.location.href",
312
- returnByValue: true
313
- })).result.value ?? url;
314
- }
346
+ return navigatedEvent?.frame?.url ?? url;
347
+ });
315
348
  }
316
349
  async interact(targetWsUrl, payload) {
317
- let conn = await this.getConnection(targetWsUrl);
318
350
  const expression = `(async () => {
319
351
  const payload = ${JSON.stringify(payload)};
320
352
  if (payload.action === 'click') {
@@ -373,31 +405,25 @@ var ChromeCdpBrowserController = class {
373
405
  }
374
406
  throw new Error('Unsupported interact action');
375
407
  })()`;
376
- const execute = async (c) => {
408
+ return await this.withRetry(targetWsUrl, async (conn) => {
377
409
  await this.ensureEnabled(targetWsUrl);
378
- const value = (await c.send("Runtime.evaluate", {
410
+ const value = (await conn.send("Runtime.evaluate", {
379
411
  expression,
380
412
  returnByValue: true,
381
413
  awaitPromise: true
382
414
  })).result.value ?? "";
383
415
  if (payload.action === "click" && value === "clicked") try {
384
- await c.waitForEvent("Page.frameStoppedLoading", 500);
416
+ await conn.waitForEvent("Page.frameNavigated", 50);
417
+ try {
418
+ await Promise.race([conn.waitForEvent("Page.loadEventFired", 3e3), conn.waitForEvent("Page.frameStoppedLoading", 3e3)]);
419
+ } catch {}
385
420
  } catch {}
386
421
  return value;
387
- };
388
- try {
389
- return await execute(conn);
390
- } catch {
391
- this.dropConnection(targetWsUrl);
392
- conn = await this.getConnection(targetWsUrl);
393
- return await execute(conn);
394
- }
422
+ });
395
423
  }
396
424
  async getContent(targetWsUrl, options) {
397
- const o = JSON.stringify(options);
398
- let conn = await this.getConnection(targetWsUrl);
399
425
  const expression = `(() => {
400
- const options = ${o};
426
+ const options = ${JSON.stringify(options)};
401
427
  if (options.mode === 'title') return document.title ?? '';
402
428
  if (options.mode === 'html') {
403
429
  if (options.selector) {
@@ -412,7 +438,7 @@ var ChromeCdpBrowserController = class {
412
438
  }
413
439
  return document.body?.innerText ?? '';
414
440
  })()`;
415
- try {
441
+ return await this.withRetry(targetWsUrl, async (conn) => {
416
442
  await this.ensureEnabled(targetWsUrl);
417
443
  const content = (await conn.send("Runtime.evaluate", {
418
444
  expression,
@@ -422,25 +448,11 @@ var ChromeCdpBrowserController = class {
422
448
  mode: options.mode,
423
449
  content
424
450
  };
425
- } catch {
426
- this.dropConnection(targetWsUrl);
427
- conn = await this.getConnection(targetWsUrl);
428
- await this.ensureEnabled(targetWsUrl);
429
- const content = (await conn.send("Runtime.evaluate", {
430
- expression,
431
- returnByValue: true
432
- })).result.value ?? "";
433
- return {
434
- mode: options.mode,
435
- content
436
- };
437
- }
451
+ });
438
452
  }
439
453
  async getInteractiveElements(targetWsUrl, options) {
440
- const o = JSON.stringify(options);
441
- let conn = await this.getConnection(targetWsUrl);
442
454
  const expression = `(() => {
443
- const options = ${o};
455
+ const options = ${JSON.stringify(options)};
444
456
  const visibleOnly = options.visibleOnly !== false;
445
457
  const limit = options.limit ?? 50;
446
458
  const scopeSelector = options.selector;
@@ -649,21 +661,13 @@ var ChromeCdpBrowserController = class {
649
661
  if (v && typeof v === "object" && Array.isArray(v.elements)) return v;
650
662
  return emptyResult;
651
663
  };
652
- try {
653
- await this.ensureEnabled(targetWsUrl);
654
- return extract(await conn.send("Runtime.evaluate", {
655
- expression,
656
- returnByValue: true
657
- }));
658
- } catch {
659
- this.dropConnection(targetWsUrl);
660
- conn = await this.getConnection(targetWsUrl);
664
+ return await this.withRetry(targetWsUrl, async (conn) => {
661
665
  await this.ensureEnabled(targetWsUrl);
662
666
  return extract(await conn.send("Runtime.evaluate", {
663
667
  expression,
664
668
  returnByValue: true
665
669
  }));
666
- }
670
+ });
667
671
  }
668
672
  terminate(pid) {
669
673
  if (pid === 0) return;
@@ -674,7 +678,7 @@ var ChromeCdpBrowserController = class {
674
678
  };
675
679
  var MockBrowserController = class {
676
680
  pages = /* @__PURE__ */ new Map();
677
- async launch(sessionId) {
681
+ async launch(sessionId, _options) {
678
682
  const cdpUrl = `mock://${sessionId}`;
679
683
  const targetWsUrl = cdpUrl;
680
684
  this.pages.set(cdpUrl, {
@@ -689,7 +693,7 @@ var MockBrowserController = class {
689
693
  targetWsUrl
690
694
  };
691
695
  }
692
- async connect(cdpUrl) {
696
+ async connect(cdpUrl, _options) {
693
697
  this.pages.set(cdpUrl, {
694
698
  url: "about:blank",
695
699
  title: "about:blank",
@@ -825,7 +829,12 @@ var SessionManager = class {
825
829
  if (active && active.session.status !== "terminated") throw new Error("A managed session is already active");
826
830
  const sessionId = crypto.randomUUID();
827
831
  const token = this.ctx.tokenService.issue(sessionId);
828
- const launched = this.ctx.config.cdpUrl ? await this.browser.connect(this.ctx.config.cdpUrl) : await this.browser.launch(sessionId, this.ctx.config.browserExecutablePath, this.ctx.config.userProfileDir);
832
+ const launched = this.ctx.config.cdpUrl ? await this.browser.connect(this.ctx.config.cdpUrl, { userAgent: this.ctx.config.userAgent }) : await this.browser.launch(sessionId, {
833
+ executablePath: this.ctx.config.browserExecutablePath,
834
+ userProfileDir: this.ctx.config.userProfileDir,
835
+ headless: this.ctx.config.headless,
836
+ userAgent: this.ctx.config.userAgent
837
+ });
829
838
  const session = {
830
839
  sessionId,
831
840
  status: "ready",
@@ -960,7 +969,12 @@ var SessionManager = class {
960
969
  if (this.browser.closeConnection) try {
961
970
  this.browser.closeConnection(record.targetWsUrl);
962
971
  } catch {}
963
- const relaunched = await this.browser.launch(sessionId, this.ctx.config.browserExecutablePath);
972
+ const relaunched = await this.browser.launch(sessionId, {
973
+ executablePath: this.ctx.config.browserExecutablePath,
974
+ userProfileDir: this.ctx.config.userProfileDir,
975
+ headless: this.ctx.config.headless,
976
+ userAgent: this.ctx.config.userAgent
977
+ });
964
978
  const restarted = {
965
979
  ...record.session,
966
980
  status: "ready",
@@ -1187,7 +1201,9 @@ function loadConfig(env = process.env) {
1187
1201
  logDir: env.AGENTIC_BROWSER_LOG_DIR ?? path.resolve(process.cwd(), ".agentic-browser"),
1188
1202
  browserExecutablePath: env.AGENTIC_BROWSER_CHROME_PATH,
1189
1203
  cdpUrl: env.AGENTIC_BROWSER_CDP_URL,
1190
- userProfileDir
1204
+ userProfileDir,
1205
+ headless: env.AGENTIC_BROWSER_HEADLESS === "true" || env.AGENTIC_BROWSER_HEADLESS === "1",
1206
+ userAgent: env.AGENTIC_BROWSER_USER_AGENT || void 0
1191
1207
  };
1192
1208
  }
1193
1209
 
@@ -1345,29 +1361,64 @@ function selectorSignal(insight) {
1345
1361
  const evidenceStrength = Math.min(selectorEvidence / 5, 1);
1346
1362
  return .7 * recipeCoverage + .3 * evidenceStrength;
1347
1363
  }
1364
+ function scoreInsight(insight, normalizedIntent, normalizedDomain) {
1365
+ const insightIntent = normalize(insight.taskIntent);
1366
+ const intentMatch = insightIntent === normalizedIntent ? 1 : 0;
1367
+ const intentPartial = intentMatch === 1 || insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent) ? .65 : 0;
1368
+ const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
1369
+ const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
1370
+ const selectorQuality = selectorSignal(insight);
1371
+ const score = .5 * Math.max(intentMatch, intentPartial) + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
1372
+ return {
1373
+ insightId: insight.insightId,
1374
+ taskIntent: insight.taskIntent,
1375
+ siteDomain: insight.siteDomain,
1376
+ confidence: insight.confidence,
1377
+ freshness: insight.freshness,
1378
+ lastVerifiedAt: insight.lastVerifiedAt,
1379
+ selectorHints: buildSelectorHints(insight),
1380
+ score
1381
+ };
1382
+ }
1348
1383
  var MemoryIndex = class {
1384
+ /** Domain → insights index, rebuilt lazily when insight list changes. */
1385
+ domainIndex = /* @__PURE__ */ new Map();
1386
+ indexedInsights = null;
1387
+ indexedLength = 0;
1388
+ /** Rebuild the domain index when the underlying array or its size changes. */
1389
+ ensureIndex(insights) {
1390
+ if (this.indexedInsights === insights && this.indexedLength === insights.length) return;
1391
+ this.domainIndex.clear();
1392
+ for (const insight of insights) {
1393
+ const domain = normalize(insight.siteDomain);
1394
+ let bucket = this.domainIndex.get(domain);
1395
+ if (!bucket) {
1396
+ bucket = [];
1397
+ this.domainIndex.set(domain, bucket);
1398
+ }
1399
+ bucket.push(insight);
1400
+ }
1401
+ this.indexedInsights = insights;
1402
+ this.indexedLength = insights.length;
1403
+ }
1349
1404
  search(insights, input) {
1350
1405
  const normalizedIntent = normalize(input.taskIntent);
1351
1406
  const normalizedDomain = input.siteDomain ? normalize(input.siteDomain) : void 0;
1352
1407
  const limit = input.limit ?? 10;
1353
- return insights.map((insight) => {
1354
- const intentMatch = normalize(insight.taskIntent) === normalizedIntent ? 1 : 0;
1355
- const intentPartial = intentMatch === 1 || normalize(insight.taskIntent).includes(normalizedIntent) || normalizedIntent.includes(normalize(insight.taskIntent)) ? .65 : 0;
1356
- const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
1357
- const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
1358
- const selectorQuality = selectorSignal(insight);
1359
- const score = .5 * Math.max(intentMatch, intentPartial) + .2 * domainMatch + .15 * reliability + .15 * selectorQuality;
1360
- return {
1361
- insightId: insight.insightId,
1362
- taskIntent: insight.taskIntent,
1363
- siteDomain: insight.siteDomain,
1364
- confidence: insight.confidence,
1365
- freshness: insight.freshness,
1366
- lastVerifiedAt: insight.lastVerifiedAt,
1367
- selectorHints: buildSelectorHints(insight),
1368
- score
1369
- };
1370
- }).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
1408
+ this.ensureIndex(insights);
1409
+ const candidates = normalizedDomain ? this.domainIndex.get(normalizedDomain) ?? [] : insights;
1410
+ if (limit === 1 && normalizedDomain) {
1411
+ let best;
1412
+ for (const insight of candidates) {
1413
+ const result = scoreInsight(insight, normalizedIntent, normalizedDomain);
1414
+ if (result.score > 0 && (!best || result.score > best.score)) {
1415
+ best = result;
1416
+ if (best.score >= .95) break;
1417
+ }
1418
+ }
1419
+ return best ? [best] : [];
1420
+ }
1421
+ return candidates.map((insight) => scoreInsight(insight, normalizedIntent, normalizedDomain)).filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
1371
1422
  }
1372
1423
  };
1373
1424
 
@@ -1464,8 +1515,13 @@ const MemoryStateSchema = z.object({ insights: z.array(TaskInsightSchema) });
1464
1515
  //#endregion
1465
1516
  //#region src/memory/task-insight-store.ts
1466
1517
  const EMPTY_STATE = { insights: [] };
1518
+ const FLUSH_DELAY_MS = 500;
1467
1519
  var TaskInsightStore = class {
1468
1520
  filePath;
1521
+ /** In-memory cache – authoritative after first load. */
1522
+ cached = null;
1523
+ dirty = false;
1524
+ flushTimer = null;
1469
1525
  constructor(baseDir) {
1470
1526
  const memoryDir = path.join(baseDir, "memory");
1471
1527
  fs.mkdirSync(memoryDir, { recursive: true });
@@ -1474,27 +1530,66 @@ var TaskInsightStore = class {
1474
1530
  try {
1475
1531
  if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
1476
1532
  } catch {}
1477
- if (!fs.existsSync(this.filePath)) this.write(EMPTY_STATE);
1533
+ if (!fs.existsSync(this.filePath)) this.writeDisk(EMPTY_STATE);
1534
+ const onExit = () => this.flushSync();
1535
+ process.on("exit", onExit);
1536
+ process.on("SIGINT", () => {
1537
+ this.flushSync();
1538
+ process.exit(0);
1539
+ });
1540
+ process.on("SIGTERM", () => {
1541
+ this.flushSync();
1542
+ process.exit(0);
1543
+ });
1478
1544
  }
1479
1545
  list() {
1480
- return this.read().insights;
1546
+ return this.getCache();
1481
1547
  }
1482
1548
  get(insightId) {
1483
- return this.read().insights.find((insight) => insight.insightId === insightId);
1549
+ return this.getCache().find((insight) => insight.insightId === insightId);
1484
1550
  }
1485
1551
  upsert(insight) {
1486
1552
  TaskInsightSchema.parse(insight);
1487
- const state = this.read();
1488
- const index = state.insights.findIndex((entry) => entry.insightId === insight.insightId);
1489
- if (index >= 0) state.insights[index] = insight;
1490
- else state.insights.push(insight);
1491
- this.write(state);
1553
+ const insights = this.getCache();
1554
+ const index = insights.findIndex((entry) => entry.insightId === insight.insightId);
1555
+ if (index >= 0) insights[index] = insight;
1556
+ else insights.push(insight);
1557
+ this.markDirty();
1492
1558
  }
1493
1559
  replaceMany(insights) {
1494
1560
  for (const insight of insights) TaskInsightSchema.parse(insight);
1495
- this.write({ insights });
1561
+ this.cached = insights;
1562
+ this.markDirty();
1563
+ }
1564
+ /** Force an immediate synchronous flush (used at shutdown). */
1565
+ flushSync() {
1566
+ if (this.flushTimer) {
1567
+ clearTimeout(this.flushTimer);
1568
+ this.flushTimer = null;
1569
+ }
1570
+ if (this.dirty && this.cached) {
1571
+ this.writeDisk({ insights: this.cached });
1572
+ this.dirty = false;
1573
+ }
1496
1574
  }
1497
- read() {
1575
+ /** Return the in-memory cache, loading from disk on first access. */
1576
+ getCache() {
1577
+ if (this.cached) return this.cached;
1578
+ this.cached = this.readDisk().insights;
1579
+ return this.cached;
1580
+ }
1581
+ markDirty() {
1582
+ this.dirty = true;
1583
+ if (!this.flushTimer) {
1584
+ this.flushTimer = setTimeout(() => {
1585
+ this.flushTimer = null;
1586
+ this.flushSync();
1587
+ }, FLUSH_DELAY_MS);
1588
+ if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) this.flushTimer.unref();
1589
+ }
1590
+ }
1591
+ /** Read and validate from disk (only on first load or corruption recovery). */
1592
+ readDisk() {
1498
1593
  let raw;
1499
1594
  try {
1500
1595
  raw = JSON.parse(fs.readFileSync(this.filePath, "utf8"));
@@ -1511,7 +1606,7 @@ var TaskInsightStore = class {
1511
1606
  if (salvaged.length > 0) {
1512
1607
  const state = { insights: salvaged };
1513
1608
  this.backupAndReset();
1514
- this.write(state);
1609
+ this.writeDisk(state);
1515
1610
  return state;
1516
1611
  }
1517
1612
  }
@@ -1525,7 +1620,7 @@ var TaskInsightStore = class {
1525
1620
  fs.copyFileSync(this.filePath, corruptPath);
1526
1621
  } catch {}
1527
1622
  }
1528
- write(state) {
1623
+ writeDisk(state) {
1529
1624
  const tempPath = `${this.filePath}.tmp`;
1530
1625
  fs.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf8");
1531
1626
  fs.renameSync(tempPath, this.filePath);
@@ -1534,15 +1629,31 @@ var TaskInsightStore = class {
1534
1629
 
1535
1630
  //#endregion
1536
1631
  //#region src/memory/memory-service.ts
1632
+ const SEARCH_CACHE_TTL_MS = 2e3;
1537
1633
  var MemoryService = class {
1538
1634
  store;
1539
1635
  index;
1636
+ /** Simple TTL cache for search results keyed by intent+domain+limit. */
1637
+ searchCache = /* @__PURE__ */ new Map();
1540
1638
  constructor(baseDir) {
1541
1639
  this.store = new TaskInsightStore(baseDir);
1542
1640
  this.index = new MemoryIndex();
1543
1641
  }
1544
1642
  search(input) {
1545
- return this.index.search(this.store.list(), input);
1643
+ const cacheKey = `${input.taskIntent}\0${input.siteDomain ?? ""}\0${input.limit ?? 10}`;
1644
+ const now = Date.now();
1645
+ const cached = this.searchCache.get(cacheKey);
1646
+ if (cached && now - cached.ts < SEARCH_CACHE_TTL_MS) return cached.results;
1647
+ const results = this.index.search(this.store.list(), input);
1648
+ this.searchCache.set(cacheKey, {
1649
+ results,
1650
+ ts: now
1651
+ });
1652
+ return results;
1653
+ }
1654
+ /** Invalidate search cache when data changes. */
1655
+ invalidateSearchCache() {
1656
+ this.searchCache.clear();
1546
1657
  }
1547
1658
  inspect(insightId) {
1548
1659
  const insight = this.store.get(insightId);
@@ -1573,6 +1684,7 @@ var MemoryService = class {
1573
1684
  updatedAt: now
1574
1685
  };
1575
1686
  this.store.upsert(verified);
1687
+ this.invalidateSearchCache();
1576
1688
  return verified;
1577
1689
  }
1578
1690
  recordSuccess(input) {
@@ -1600,6 +1712,7 @@ var MemoryService = class {
1600
1712
  evidence: [evidence]
1601
1713
  };
1602
1714
  this.store.upsert(created);
1715
+ this.invalidateSearchCache();
1603
1716
  return created;
1604
1717
  }
1605
1718
  const refreshed = applySuccess({
@@ -1620,9 +1733,11 @@ var MemoryService = class {
1620
1733
  updatedAt: now
1621
1734
  };
1622
1735
  this.store.upsert(versioned);
1736
+ this.invalidateSearchCache();
1623
1737
  return versioned;
1624
1738
  }
1625
1739
  this.store.upsert(refreshed);
1740
+ this.invalidateSearchCache();
1626
1741
  return refreshed;
1627
1742
  }
1628
1743
  recordFailure(input, errorMessage) {
@@ -1637,6 +1752,7 @@ var MemoryService = class {
1637
1752
  evidence: [...matched.evidence.slice(-49), evidence]
1638
1753
  }, signal);
1639
1754
  this.store.upsert(failed);
1755
+ this.invalidateSearchCache();
1640
1756
  return failed;
1641
1757
  }
1642
1758
  findBestExactMatch(insights, taskIntent, siteDomain) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-browser",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",