@agentmemory/agentmemory 0.9.17 → 0.9.18

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/dist/index.mjs CHANGED
@@ -325,6 +325,20 @@ var AnthropicProvider = class {
325
325
  }
326
326
  };
327
327
 
328
+ //#endregion
329
+ //#region src/providers/_fetch.ts
330
+ function fetchWithTimeout(url, init, timeoutMs) {
331
+ const parsed = timeoutMs ?? Number.parseInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS") ?? "60000", 10);
332
+ const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
333
+ const ctl = new AbortController();
334
+ const signal = init.signal ? AbortSignal.any([init.signal, ctl.signal]) : ctl.signal;
335
+ const t = setTimeout(() => ctl.abort(), ms);
336
+ return fetch(url, {
337
+ ...init,
338
+ signal
339
+ }).finally(() => clearTimeout(t));
340
+ }
341
+
328
342
  //#endregion
329
343
  //#region src/providers/minimax.ts
330
344
  /**
@@ -360,8 +374,7 @@ var MinimaxProvider = class {
360
374
  return this.call(systemPrompt, userPrompt);
361
375
  }
362
376
  async call(systemPrompt, userPrompt) {
363
- const url = `${this.baseUrl}/v1/messages`;
364
- const response = await fetch(url, {
377
+ const response = await fetchWithTimeout(`${this.baseUrl}/v1/messages`, {
365
378
  method: "POST",
366
379
  headers: {
367
380
  "Content-Type": "application/json",
@@ -429,7 +442,12 @@ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
429
442
  * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
430
443
  * OPENAI_MODEL — model name (default: gpt-4o-mini)
431
444
  * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
432
- * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (default: 60000)
445
+ * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (OpenAI-scoped alias,
446
+ * takes precedence over AGENTMEMORY_LLM_TIMEOUT_MS
447
+ * for back-compat with the v0.9.17 shipping name).
448
+ * AGENTMEMORY_LLM_TIMEOUT_MS — outbound fetch timeout in ms shared across all
449
+ * raw-fetch LLM + embedding providers. Used when
450
+ * OPENAI_TIMEOUT_MS is not set. Default: 60000.
433
451
  * MAX_TOKENS — max output tokens (default: from config or 4096)
434
452
  * OPENAI_REASONING_EFFORT — "low" | "medium" | "high" | "none"
435
453
  * Passthrough for reasoning models (e.g. Ollama Cloud
@@ -453,7 +471,7 @@ var OpenAIProvider = class {
453
471
  this.maxTokens = maxTokens;
454
472
  this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL$1).replace(/\/+$/, "");
455
473
  this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
456
- this.timeoutMs = parseTimeout(getEnvVar("OPENAI_TIMEOUT_MS"));
474
+ this.timeoutMs = resolveTimeout();
457
475
  this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
458
476
  this.isAzure = detectAzure(this.baseUrl);
459
477
  }
@@ -494,21 +512,16 @@ var OpenAIProvider = class {
494
512
  }]
495
513
  };
496
514
  if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
497
- const ac = new AbortController();
498
- const t = setTimeout(() => ac.abort(), this.timeoutMs);
499
515
  let response;
500
516
  try {
501
- response = await fetch(url, {
517
+ response = await fetchWithTimeout(url, {
502
518
  method: "POST",
503
519
  headers: this.buildHeaders(),
504
- body: JSON.stringify(body),
505
- signal: ac.signal
506
- });
520
+ body: JSON.stringify(body)
521
+ }, this.timeoutMs);
507
522
  } catch (err) {
508
- if (ac.signal.aborted || err instanceof Error && err.name === "AbortError") throw new Error(`OpenAI API request timed out after ${this.timeoutMs}ms — set OPENAI_TIMEOUT_MS to raise the bound or check the provider status.`);
523
+ if (err instanceof Error && err.name === "AbortError") throw new Error(`OpenAI API request timed out after ${this.timeoutMs}ms — set OPENAI_TIMEOUT_MS (or AGENTMEMORY_LLM_TIMEOUT_MS) to raise the bound or check the provider status.`);
509
524
  throw err;
510
- } finally {
511
- clearTimeout(t);
512
525
  }
513
526
  if (!response.ok) {
514
527
  const text = await response.text();
@@ -523,10 +536,19 @@ var OpenAIProvider = class {
523
536
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
524
537
  }
525
538
  };
526
- function parseTimeout(raw) {
527
- if (!raw) return DEFAULT_TIMEOUT_MS;
528
- const n = parseInt(raw, 10);
529
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_TIMEOUT_MS;
539
+ function resolveTimeout() {
540
+ const openai = parsePositiveInt(getEnvVar("OPENAI_TIMEOUT_MS"));
541
+ if (openai !== void 0) return openai;
542
+ const globalMs = parsePositiveInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS"));
543
+ if (globalMs !== void 0) return globalMs;
544
+ return DEFAULT_TIMEOUT_MS;
545
+ }
546
+ function parsePositiveInt(raw) {
547
+ if (!raw) return void 0;
548
+ const trimmed = raw.trim();
549
+ if (!/^\d+$/.test(trimmed)) return void 0;
550
+ const n = Number(trimmed);
551
+ return Number.isFinite(n) && n > 0 ? n : void 0;
530
552
  }
531
553
  function detectAzure(baseUrl) {
532
554
  try {
@@ -558,7 +580,7 @@ var OpenRouterProvider = class {
558
580
  return this.call(systemPrompt, userPrompt);
559
581
  }
560
582
  async call(systemPrompt, userPrompt) {
561
- const response = await fetch(this.baseUrl, {
583
+ const response = await fetchWithTimeout(this.baseUrl, {
562
584
  method: "POST",
563
585
  headers: {
564
586
  "Content-Type": "application/json",
@@ -727,7 +749,7 @@ var GeminiEmbeddingProvider = class {
727
749
  const results = [];
728
750
  for (let i = 0; i < texts.length; i += BATCH_LIMIT) {
729
751
  const chunk = texts.slice(i, i + BATCH_LIMIT);
730
- const response = await fetch(`${API_BASE}?key=${this.apiKey}`, {
752
+ const response = await fetchWithTimeout(`${API_BASE}?key=${this.apiKey}`, {
731
753
  method: "POST",
732
754
  headers: { "Content-Type": "application/json" },
733
755
  body: JSON.stringify({ requests: chunk.map((t) => ({
@@ -816,8 +838,7 @@ var OpenAIEmbeddingProvider = class {
816
838
  return result;
817
839
  }
818
840
  async embedBatch(texts) {
819
- const url = `${this.baseUrl}/v1/embeddings`;
820
- const response = await fetch(url, {
841
+ const response = await fetchWithTimeout(`${this.baseUrl}/v1/embeddings`, {
821
842
  method: "POST",
822
843
  headers: {
823
844
  Authorization: `Bearer ${this.apiKey}`,
@@ -852,7 +873,7 @@ var VoyageEmbeddingProvider = class {
852
873
  return result;
853
874
  }
854
875
  async embedBatch(texts) {
855
- const response = await fetch(API_URL$2, {
876
+ const response = await fetchWithTimeout(API_URL$2, {
856
877
  method: "POST",
857
878
  headers: {
858
879
  Authorization: `Bearer ${this.apiKey}`,
@@ -888,7 +909,7 @@ var CohereEmbeddingProvider = class {
888
909
  return result;
889
910
  }
890
911
  async embedBatch(texts) {
891
- const response = await fetch(API_URL$1, {
912
+ const response = await fetchWithTimeout(API_URL$1, {
892
913
  method: "POST",
893
914
  headers: {
894
915
  Authorization: `Bearer ${this.apiKey}`,
@@ -926,7 +947,7 @@ var OpenRouterEmbeddingProvider = class {
926
947
  return result;
927
948
  }
928
949
  async embedBatch(texts) {
929
- const response = await fetch(API_URL, {
950
+ const response = await fetchWithTimeout(API_URL, {
930
951
  method: "POST",
931
952
  headers: {
932
953
  Authorization: `Bearer ${this.apiKey}`,
@@ -4543,7 +4564,11 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4543
4564
  sdk.registerFunction("mem::context", async (data) => {
4544
4565
  const budget = data.budget || tokenBudget;
4545
4566
  const blocks = [];
4546
- const [pinnedSlots, profile] = await Promise.all([isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]), kv.get(KV.profiles, data.project).catch(() => null)]);
4567
+ const [pinnedSlots, profile, lessons] = await Promise.all([
4568
+ isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]),
4569
+ kv.get(KV.profiles, data.project).catch(() => null),
4570
+ kv.list(KV.lessons).catch(() => [])
4571
+ ]);
4547
4572
  const slotContent = renderPinnedContext(pinnedSlots);
4548
4573
  if (slotContent) blocks.push({
4549
4574
  type: "memory",
@@ -4567,6 +4592,24 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4567
4592
  });
4568
4593
  }
4569
4594
  }
4595
+ const relevantLessons = lessons.filter((l) => !l.deleted && (!l.project || l.project === data.project)).sort((a, b) => {
4596
+ const scoreA = (a.project === data.project ? 1.5 : 1) * a.confidence;
4597
+ return (b.project === data.project ? 1.5 : 1) * b.confidence - scoreA;
4598
+ }).slice(0, 10);
4599
+ if (relevantLessons.length > 0) {
4600
+ const lessonsContent = `## Lessons Learned\n${relevantLessons.map((l) => `- (${l.confidence.toFixed(2)}) ${l.content}${l.context ? ` — ${l.context}` : ""}`).join("\n")}`;
4601
+ const mostRecent = relevantLessons.reduce((acc, l) => {
4602
+ const t = new Date(l.lastReinforcedAt || l.updatedAt).getTime();
4603
+ return t > acc ? t : acc;
4604
+ }, 0);
4605
+ blocks.push({
4606
+ type: "memory",
4607
+ content: lessonsContent,
4608
+ tokens: estimateTokens$1(lessonsContent),
4609
+ recency: mostRecent,
4610
+ sourceIds: relevantLessons.map((l) => l.id)
4611
+ });
4612
+ }
4570
4613
  const sessions = (await kv.list(KV.sessions)).filter((s) => s.project === data.project && s.id !== data.sessionId).sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()).slice(0, 10);
4571
4614
  const summariesPerSession = await Promise.all(sessions.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
4572
4615
  const sessionsNeedingObs = [];
@@ -5389,6 +5432,46 @@ const DEFAULTS$1 = {
5389
5432
  lowImportanceThreshold: 3,
5390
5433
  maxObservationsPerProject: 1e4
5391
5434
  };
5435
+ function isValidRecoveryResult(result) {
5436
+ if (!result || typeof result !== "object") return false;
5437
+ if (!("success" in result)) return true;
5438
+ return result.success !== false;
5439
+ }
5440
+ function isCompressedObservation(observation) {
5441
+ return "title" in observation && typeof observation.title === "string" && observation.title.length > 0;
5442
+ }
5443
+ async function recoverStaleSession(sdk, sessionId) {
5444
+ try {
5445
+ const result = await sdk.trigger({
5446
+ function_id: "event::session::stopped",
5447
+ payload: { sessionId }
5448
+ });
5449
+ if (!isValidRecoveryResult(result)) {
5450
+ logger.warn("Stale session recovery failed", {
5451
+ sessionId,
5452
+ result
5453
+ });
5454
+ return false;
5455
+ }
5456
+ return true;
5457
+ } catch (err) {
5458
+ logger.warn("Stale session recovery failed", {
5459
+ sessionId,
5460
+ error: err instanceof Error ? err.message : String(err)
5461
+ });
5462
+ return false;
5463
+ }
5464
+ }
5465
+ async function runRecoveredSessionConsolidation(sdk) {
5466
+ try {
5467
+ await sdk.trigger({
5468
+ function_id: "mem::consolidate-pipeline",
5469
+ payload: { tier: "all" }
5470
+ });
5471
+ } catch (err) {
5472
+ logger.warn("Recovered session consolidation failed", { error: err instanceof Error ? err.message : String(err) });
5473
+ }
5474
+ }
5392
5475
  function registerEvictFunction(sdk, kv) {
5393
5476
  sdk.registerFunction("mem::evict", async (data) => {
5394
5477
  const dryRun = data?.dryRun ?? false;
@@ -5407,6 +5490,7 @@ function registerEvictFunction(sdk, kv) {
5407
5490
  nonLatestMemories: 0,
5408
5491
  dryRun
5409
5492
  };
5493
+ let recoveredStaleSessions = 0;
5410
5494
  const sessions = await kv.list(KV.sessions).catch(() => []);
5411
5495
  const summaries = await kv.list(KV.summaries).catch(() => []);
5412
5496
  const summaryIds = new Set(summaries.map((s) => s.sessionId));
@@ -5414,6 +5498,23 @@ function registerEvictFunction(sdk, kv) {
5414
5498
  if (!session.startedAt) continue;
5415
5499
  if (now - new Date(session.startedAt).getTime() > cfg.staleSessionDays * MS_PER_DAY$1 && !summaryIds.has(session.id)) if (dryRun) stats.staleSessions++;
5416
5500
  else {
5501
+ const observations = await kv.list(KV.observations(session.id)).catch((err) => {
5502
+ logger.warn("Stale session observation scan failed", {
5503
+ sessionId: session.id,
5504
+ error: err instanceof Error ? err.message : String(err)
5505
+ });
5506
+ return null;
5507
+ });
5508
+ if (!observations) continue;
5509
+ let recovered = false;
5510
+ if (observations.some(isCompressedObservation)) {
5511
+ recovered = await recoverStaleSession(sdk, session.id);
5512
+ if (!recovered) continue;
5513
+ recoveredStaleSessions++;
5514
+ } else if (observations.length > 0) {
5515
+ logger.warn("Stale session has no compressed observations", { sessionId: session.id });
5516
+ continue;
5517
+ }
5417
5518
  try {
5418
5519
  await kv.delete(KV.sessions, session.id);
5419
5520
  stats.staleSessions++;
@@ -5427,11 +5528,12 @@ function registerEvictFunction(sdk, kv) {
5427
5528
  }
5428
5529
  await recordAudit(kv, "delete", "mem::evict", [session.id], {
5429
5530
  resource: "session",
5430
- reason: "stale_session_without_summary",
5531
+ reason: recovered ? "stale_session_recovered_then_evicted" : "stale_session_without_summary",
5431
5532
  dryRun
5432
5533
  });
5433
5534
  }
5434
5535
  }
5536
+ if (!dryRun && recoveredStaleSessions > 0) await runRecoveredSessionConsolidation(sdk);
5435
5537
  const projectObs = /* @__PURE__ */ new Map();
5436
5538
  for (const session of sessions) {
5437
5539
  const compressed = (await kv.list(KV.observations(session.id)).catch(() => [])).filter((o) => o.title);
@@ -6110,7 +6212,7 @@ function registerAutoForgetFunction(sdk, kv) {
6110
6212
 
6111
6213
  //#endregion
6112
6214
  //#region src/version.ts
6113
- const VERSION = "0.9.17";
6215
+ const VERSION = "0.9.18";
6114
6216
 
6115
6217
  //#endregion
6116
6218
  //#region src/functions/export-import.ts
@@ -6245,7 +6347,8 @@ function registerExportImportFunction(sdk, kv) {
6245
6347
  "0.9.14",
6246
6348
  "0.9.15",
6247
6349
  "0.9.16",
6248
- "0.9.17"
6350
+ "0.9.17",
6351
+ "0.9.18"
6249
6352
  ]).has(importData.version)) return {
6250
6353
  success: false,
6251
6354
  error: `Unsupported export version: ${importData.version}`
@@ -19512,7 +19615,38 @@ function registerMcpEndpoints(sdk, kv, secret) {
19512
19615
 
19513
19616
  //#endregion
19514
19617
  //#region src/viewer/server.ts
19618
+ function loadViewerFavicon() {
19619
+ const base = dirname(fileURLToPath(import.meta.url));
19620
+ const candidates = [
19621
+ join(base, "..", "src", "viewer", "favicon.svg"),
19622
+ join(base, "..", "viewer", "favicon.svg"),
19623
+ join(base, "viewer", "favicon.svg")
19624
+ ];
19625
+ for (const path of candidates) try {
19626
+ return readFileSync(path);
19627
+ } catch {}
19628
+ return null;
19629
+ }
19515
19630
  const ALLOWED_ORIGINS = (process.env.VIEWER_ALLOWED_ORIGINS || "http://localhost:3111,http://localhost:3113,http://127.0.0.1:3111,http://127.0.0.1:3113").split(",").map((o) => o.trim());
19631
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
19632
+ function buildAllowedHosts(origins, listenPort) {
19633
+ const hosts = /* @__PURE__ */ new Set();
19634
+ for (const o of origins) try {
19635
+ const parsed = new URL(o);
19636
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
19637
+ } catch {}
19638
+ hosts.add(`localhost:${listenPort}`);
19639
+ hosts.add(`127.0.0.1:${listenPort}`);
19640
+ hosts.add(`[::1]:${listenPort}`);
19641
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
19642
+ return hosts;
19643
+ }
19644
+ function isHostAllowed(headerHost, allowed) {
19645
+ if (typeof headerHost !== "string") return false;
19646
+ const lower = headerHost.toLowerCase().trim();
19647
+ if (!lower) return false;
19648
+ return allowed.has(lower);
19649
+ }
19516
19650
  function corsHeaders(req) {
19517
19651
  const origin = req.headers.origin || "";
19518
19652
  const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
@@ -19556,7 +19690,17 @@ const MAX_VIEWER_PORT_RETRIES = 10;
19556
19690
  function startViewerServer(port, _kv, _sdk, secret, restPort) {
19557
19691
  const resolvedRestPort = restPort ?? port - 2;
19558
19692
  const requestedPort = port;
19693
+ let allowedHosts = null;
19559
19694
  const server = createServer(async (req, res) => {
19695
+ if (!allowedHosts) {
19696
+ const addr = server.address();
19697
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
19698
+ }
19699
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
19700
+ res.writeHead(403, { "Content-Type": "text/plain" });
19701
+ res.end("forbidden host");
19702
+ return;
19703
+ }
19560
19704
  const raw = req.url || "/";
19561
19705
  const qIdx = raw.indexOf("?");
19562
19706
  const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
@@ -19585,6 +19729,20 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
19585
19729
  res.end("viewer not found");
19586
19730
  return;
19587
19731
  }
19732
+ if (method === "GET" && pathname === "/favicon.svg") {
19733
+ const favicon = loadViewerFavicon();
19734
+ if (favicon) {
19735
+ res.writeHead(200, {
19736
+ "Content-Type": "image/svg+xml",
19737
+ "Cache-Control": "public, max-age=3600"
19738
+ });
19739
+ res.end(favicon);
19740
+ return;
19741
+ }
19742
+ res.writeHead(404, { "Content-Type": "text/plain" });
19743
+ res.end("favicon not found");
19744
+ return;
19745
+ }
19588
19746
  try {
19589
19747
  await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
19590
19748
  } catch (err) {