@agentmemory/agentmemory 0.9.17 → 0.9.19

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.
Files changed (36) hide show
  1. package/.env.example +6 -0
  2. package/AGENTS.md +2 -2
  3. package/README.md +19 -5
  4. package/dist/.env.example +6 -0
  5. package/dist/cli.mjs +19 -9
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/hooks/post-commit.d.mts +1 -0
  8. package/dist/hooks/post-commit.mjs +102 -0
  9. package/dist/hooks/post-commit.mjs.map +1 -0
  10. package/dist/index.mjs +537 -91
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/{src-TiNuQ3Ub.mjs → src-2wwYDPGA.mjs} +503 -91
  13. package/dist/src-2wwYDPGA.mjs.map +1 -0
  14. package/dist/{standalone-BIXq6S80.mjs → standalone-DMLk7YxP.mjs} +17 -8
  15. package/dist/standalone-DMLk7YxP.mjs.map +1 -0
  16. package/dist/standalone.mjs +49 -7
  17. package/dist/standalone.mjs.map +1 -1
  18. package/dist/{tools-registry-BFKFKmYh.mjs → tools-registry-Dz8ssuMf.mjs} +34 -1
  19. package/dist/tools-registry-Dz8ssuMf.mjs.map +1 -0
  20. package/dist/viewer/favicon.svg +1 -0
  21. package/dist/viewer/index.html +190 -60
  22. package/package.json +2 -2
  23. package/plugin/.claude-plugin/plugin.json +1 -1
  24. package/plugin/.codex-plugin/plugin.json +1 -1
  25. package/plugin/.mcp.json +5 -1
  26. package/plugin/hooks/hooks.codex.json +4 -0
  27. package/plugin/scripts/post-commit.d.mts +1 -0
  28. package/plugin/scripts/post-commit.mjs +102 -0
  29. package/plugin/scripts/post-commit.mjs.map +1 -0
  30. package/plugin/skills/commit-context/SKILL.md +19 -0
  31. package/plugin/skills/commit-history/SKILL.md +20 -0
  32. package/plugin/skills/handoff/SKILL.md +21 -0
  33. package/plugin/skills/recap/SKILL.md +25 -0
  34. package/dist/src-TiNuQ3Ub.mjs.map +0 -1
  35. package/dist/standalone-BIXq6S80.mjs.map +0 -1
  36. package/dist/tools-registry-BFKFKmYh.mjs.map +0 -1
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",
@@ -405,11 +418,63 @@ var NoopProvider = class {
405
418
  }
406
419
  };
407
420
 
421
+ //#endregion
422
+ //#region src/providers/_openai-shared.ts
423
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
424
+ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
425
+ function detectAzure(baseUrl) {
426
+ try {
427
+ return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
428
+ } catch {
429
+ return false;
430
+ }
431
+ }
432
+ function azureStyleOf(baseUrl) {
433
+ try {
434
+ const u = new URL(baseUrl);
435
+ if (/\/openai\/deployments\//.test(u.pathname)) return "legacy";
436
+ return "v1";
437
+ } catch {
438
+ return "v1";
439
+ }
440
+ }
441
+ function legacyAzureUrl(baseUrl, path, apiVersion) {
442
+ const url = new URL(baseUrl);
443
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
444
+ url.searchParams.set("api-version", apiVersion);
445
+ return url.toString();
446
+ }
447
+ function v1AzureUrl(baseUrl, path) {
448
+ const url = new URL(baseUrl);
449
+ const route = path.startsWith("/") ? path.slice(1) : path;
450
+ url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
451
+ return url.toString();
452
+ }
453
+ function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
454
+ if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
455
+ return `${baseUrl}/v1/chat/completions`;
456
+ }
457
+ function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
458
+ if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
459
+ return `${baseUrl}/v1/embeddings`;
460
+ }
461
+ function buildAuthHeaders(apiKey, isAzure) {
462
+ if (isAzure) return {
463
+ "Content-Type": "application/json",
464
+ "api-key": apiKey
465
+ };
466
+ return {
467
+ "Content-Type": "application/json",
468
+ Authorization: `Bearer ${apiKey}`
469
+ };
470
+ }
471
+ function normalizeBaseUrl(raw) {
472
+ return (raw || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
473
+ }
474
+
408
475
  //#endregion
409
476
  //#region src/providers/openai.ts
410
- const DEFAULT_BASE_URL$1 = "https://api.openai.com";
411
477
  const DEFAULT_TIMEOUT_MS = 6e4;
412
- const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
413
478
  /**
414
479
  * OpenAI-compatible LLM provider.
415
480
  *
@@ -429,7 +494,12 @@ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
429
494
  * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
430
495
  * OPENAI_MODEL — model name (default: gpt-4o-mini)
431
496
  * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
432
- * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (default: 60000)
497
+ * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (OpenAI-scoped alias,
498
+ * takes precedence over AGENTMEMORY_LLM_TIMEOUT_MS
499
+ * for back-compat with the v0.9.17 shipping name).
500
+ * AGENTMEMORY_LLM_TIMEOUT_MS — outbound fetch timeout in ms shared across all
501
+ * raw-fetch LLM + embedding providers. Used when
502
+ * OPENAI_TIMEOUT_MS is not set. Default: 60000.
433
503
  * MAX_TOKENS — max output tokens (default: from config or 4096)
434
504
  * OPENAI_REASONING_EFFORT — "low" | "medium" | "high" | "none"
435
505
  * Passthrough for reasoning models (e.g. Ollama Cloud
@@ -451,9 +521,9 @@ var OpenAIProvider = class {
451
521
  this.apiKey = apiKey;
452
522
  this.model = model;
453
523
  this.maxTokens = maxTokens;
454
- this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL$1).replace(/\/+$/, "");
524
+ this.baseUrl = normalizeBaseUrl(baseURL || getEnvVar("OPENAI_BASE_URL"));
455
525
  this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
456
- this.timeoutMs = parseTimeout(getEnvVar("OPENAI_TIMEOUT_MS"));
526
+ this.timeoutMs = resolveTimeout();
457
527
  this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
458
528
  this.isAzure = detectAzure(this.baseUrl);
459
529
  }
@@ -463,25 +533,8 @@ var OpenAIProvider = class {
463
533
  async summarize(systemPrompt, userPrompt) {
464
534
  return this.call(systemPrompt, userPrompt);
465
535
  }
466
- buildUrl() {
467
- if (this.isAzure) {
468
- const sep = this.baseUrl.includes("?") ? "&" : "?";
469
- return `${this.baseUrl}/chat/completions${sep}api-version=${encodeURIComponent(this.azureApiVersion)}`;
470
- }
471
- return `${this.baseUrl}/v1/chat/completions`;
472
- }
473
- buildHeaders() {
474
- if (this.isAzure) return {
475
- "Content-Type": "application/json",
476
- "api-key": this.apiKey
477
- };
478
- return {
479
- "Content-Type": "application/json",
480
- Authorization: `Bearer ${this.apiKey}`
481
- };
482
- }
483
536
  async call(systemPrompt, userPrompt) {
484
- const url = this.buildUrl();
537
+ const url = buildChatUrl(this.baseUrl, this.isAzure, this.azureApiVersion);
485
538
  const body = {
486
539
  model: this.model,
487
540
  max_tokens: this.maxTokens,
@@ -494,21 +547,16 @@ var OpenAIProvider = class {
494
547
  }]
495
548
  };
496
549
  if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
497
- const ac = new AbortController();
498
- const t = setTimeout(() => ac.abort(), this.timeoutMs);
499
550
  let response;
500
551
  try {
501
- response = await fetch(url, {
552
+ response = await fetchWithTimeout(url, {
502
553
  method: "POST",
503
- headers: this.buildHeaders(),
504
- body: JSON.stringify(body),
505
- signal: ac.signal
506
- });
554
+ headers: buildAuthHeaders(this.apiKey, this.isAzure),
555
+ body: JSON.stringify(body)
556
+ }, this.timeoutMs);
507
557
  } 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.`);
558
+ 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
559
  throw err;
510
- } finally {
511
- clearTimeout(t);
512
560
  }
513
561
  if (!response.ok) {
514
562
  const text = await response.text();
@@ -523,17 +571,19 @@ var OpenAIProvider = class {
523
571
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
524
572
  }
525
573
  };
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;
530
- }
531
- function detectAzure(baseUrl) {
532
- try {
533
- return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
534
- } catch {
535
- return false;
536
- }
574
+ function resolveTimeout() {
575
+ const openai = parsePositiveInt(getEnvVar("OPENAI_TIMEOUT_MS"));
576
+ if (openai !== void 0) return openai;
577
+ const globalMs = parsePositiveInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS"));
578
+ if (globalMs !== void 0) return globalMs;
579
+ return DEFAULT_TIMEOUT_MS;
580
+ }
581
+ function parsePositiveInt(raw) {
582
+ if (!raw) return void 0;
583
+ const trimmed = raw.trim();
584
+ if (!/^\d+$/.test(trimmed)) return void 0;
585
+ const n = Number(trimmed);
586
+ return Number.isFinite(n) && n > 0 ? n : void 0;
537
587
  }
538
588
 
539
589
  //#endregion
@@ -558,7 +608,7 @@ var OpenRouterProvider = class {
558
608
  return this.call(systemPrompt, userPrompt);
559
609
  }
560
610
  async call(systemPrompt, userPrompt) {
561
- const response = await fetch(this.baseUrl, {
611
+ const response = await fetchWithTimeout(this.baseUrl, {
562
612
  method: "POST",
563
613
  headers: {
564
614
  "Content-Type": "application/json",
@@ -727,7 +777,7 @@ var GeminiEmbeddingProvider = class {
727
777
  const results = [];
728
778
  for (let i = 0; i < texts.length; i += BATCH_LIMIT) {
729
779
  const chunk = texts.slice(i, i + BATCH_LIMIT);
730
- const response = await fetch(`${API_BASE}?key=${this.apiKey}`, {
780
+ const response = await fetchWithTimeout(`${API_BASE}?key=${this.apiKey}`, {
731
781
  method: "POST",
732
782
  headers: { "Content-Type": "application/json" },
733
783
  body: JSON.stringify({ requests: chunk.map((t) => ({
@@ -764,7 +814,6 @@ function l2Normalize(vec) {
764
814
 
765
815
  //#endregion
766
816
  //#region src/providers/embedding/openai.ts
767
- const DEFAULT_BASE_URL = "https://api.openai.com";
768
817
  const DEFAULT_MODEL$1 = "text-embedding-3-small";
769
818
  /**
770
819
  * Known OpenAI embedding model dimensions. Extend as new models ship.
@@ -788,11 +837,20 @@ function resolveDimensions(model, override) {
788
837
  /**
789
838
  * OpenAI-compatible embedding provider.
790
839
  *
840
+ * Shares transport (URL builder, auth header, Azure detection) with
841
+ * the OpenAI LLM provider via `_openai-shared` (#371). Same env knobs
842
+ * pick up automatically: when `OPENAI_BASE_URL` points at an Azure
843
+ * resource (`.openai.azure.com` hostname) the embedding request uses
844
+ * Azure's `/embeddings` path with the `api-version` query param and
845
+ * `api-key` header instead of `Authorization: Bearer`.
846
+ *
791
847
  * Required env vars:
792
848
  * OPENAI_API_KEY — API key
793
849
  *
794
850
  * Optional:
795
- * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com)
851
+ * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
852
+ * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
853
+ * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
796
854
  * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
797
855
  * OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
798
856
  * custom / self-hosted models not in the
@@ -804,25 +862,25 @@ var OpenAIEmbeddingProvider = class {
804
862
  apiKey;
805
863
  baseUrl;
806
864
  model;
865
+ isAzure;
866
+ azureApiVersion;
807
867
  constructor(apiKey) {
808
868
  this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
809
869
  if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
810
- this.baseUrl = getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL;
870
+ this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
811
871
  this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
812
872
  this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
873
+ this.isAzure = detectAzure(this.baseUrl);
874
+ this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
813
875
  }
814
876
  async embed(text) {
815
877
  const [result] = await this.embedBatch([text]);
816
878
  return result;
817
879
  }
818
880
  async embedBatch(texts) {
819
- const url = `${this.baseUrl}/v1/embeddings`;
820
- const response = await fetch(url, {
881
+ const response = await fetchWithTimeout(buildEmbeddingUrl(this.baseUrl, this.isAzure, this.azureApiVersion), {
821
882
  method: "POST",
822
- headers: {
823
- Authorization: `Bearer ${this.apiKey}`,
824
- "Content-Type": "application/json"
825
- },
883
+ headers: buildAuthHeaders(this.apiKey, this.isAzure),
826
884
  body: JSON.stringify({
827
885
  model: this.model,
828
886
  input: texts
@@ -852,7 +910,7 @@ var VoyageEmbeddingProvider = class {
852
910
  return result;
853
911
  }
854
912
  async embedBatch(texts) {
855
- const response = await fetch(API_URL$2, {
913
+ const response = await fetchWithTimeout(API_URL$2, {
856
914
  method: "POST",
857
915
  headers: {
858
916
  Authorization: `Bearer ${this.apiKey}`,
@@ -888,7 +946,7 @@ var CohereEmbeddingProvider = class {
888
946
  return result;
889
947
  }
890
948
  async embedBatch(texts) {
891
- const response = await fetch(API_URL$1, {
949
+ const response = await fetchWithTimeout(API_URL$1, {
892
950
  method: "POST",
893
951
  headers: {
894
952
  Authorization: `Bearer ${this.apiKey}`,
@@ -926,7 +984,7 @@ var OpenRouterEmbeddingProvider = class {
926
984
  return result;
927
985
  }
928
986
  async embedBatch(texts) {
929
- const response = await fetch(API_URL, {
987
+ const response = await fetchWithTimeout(API_URL, {
930
988
  method: "POST",
931
989
  headers: {
932
990
  Authorization: `Bearer ${this.apiKey}`,
@@ -1229,7 +1287,8 @@ const KV = {
1229
1287
  imageEmbeddings: "mem:image-embeddings",
1230
1288
  slots: "mem:slots",
1231
1289
  globalSlots: "mem:slots:global",
1232
- state: "mem:state"
1290
+ state: "mem:state",
1291
+ commits: "mem:commits"
1233
1292
  };
1234
1293
  const STREAM = {
1235
1294
  name: "mem-live",
@@ -1423,7 +1482,7 @@ var GraphRetrieval = class {
1423
1482
  const results = [];
1424
1483
  const visitedObs = /* @__PURE__ */ new Set();
1425
1484
  for (const startNode of matchingNodes) {
1426
- const paths = this.bfsTraversal(startNode, allNodes, allEdges, maxDepth);
1485
+ const paths = this.dijkstraTraversal(startNode, allNodes, allEdges, maxDepth);
1427
1486
  for (const path of paths) {
1428
1487
  const lastNode = path[path.length - 1].node;
1429
1488
  for (const obsId of lastNode.sourceObservationIds) {
@@ -1463,7 +1522,7 @@ var GraphRetrieval = class {
1463
1522
  const results = [];
1464
1523
  const visitedObs = new Set(obsIds);
1465
1524
  for (const node of linkedNodes) {
1466
- const paths = this.bfsTraversal(node, allNodes, allEdges, maxDepth);
1525
+ const paths = this.dijkstraTraversal(node, allNodes, allEdges, maxDepth);
1467
1526
  for (const path of paths) {
1468
1527
  const lastNode = path[path.length - 1].node;
1469
1528
  for (const obsId of lastNode.sourceObservationIds) {
@@ -1535,37 +1594,104 @@ var GraphRetrieval = class {
1535
1594
  }
1536
1595
  return latest;
1537
1596
  }
1538
- bfsTraversal(startNode, allNodes, allEdges, maxDepth) {
1539
- const paths = [];
1540
- const visited = /* @__PURE__ */ new Set();
1541
- const queue = [{
1597
+ dijkstraTraversal(startNode, allNodes, allEdges, maxDepth) {
1598
+ const nodeIndex = /* @__PURE__ */ new Map();
1599
+ for (const n of allNodes) nodeIndex.set(n.id, n);
1600
+ const adjacency = /* @__PURE__ */ new Map();
1601
+ for (const edge of allEdges) {
1602
+ const a = edge.sourceNodeId;
1603
+ const b = edge.targetNodeId;
1604
+ if (!adjacency.has(a)) adjacency.set(a, []);
1605
+ if (!adjacency.has(b)) adjacency.set(b, []);
1606
+ adjacency.get(a).push({
1607
+ neighborId: b,
1608
+ edge
1609
+ });
1610
+ adjacency.get(b).push({
1611
+ neighborId: a,
1612
+ edge
1613
+ });
1614
+ }
1615
+ const dist = /* @__PURE__ */ new Map();
1616
+ const pathTo = /* @__PURE__ */ new Map();
1617
+ dist.set(startNode.id, 0);
1618
+ pathTo.set(startNode.id, [{ node: startNode }]);
1619
+ const heap = new MinHeap((a, b) => a.cost - b.cost);
1620
+ heap.push({
1542
1621
  nodeId: startNode.id,
1543
1622
  depth: 0,
1544
- path: [{ node: startNode }]
1545
- }];
1546
- visited.add(startNode.id);
1547
- while (queue.length > 0) {
1548
- const { nodeId, depth, path } = queue.shift();
1549
- paths.push(path);
1623
+ cost: 0
1624
+ });
1625
+ while (heap.size() > 0) {
1626
+ const { nodeId, depth, cost } = heap.pop();
1627
+ if (cost > (dist.get(nodeId) ?? Infinity)) continue;
1550
1628
  if (depth >= maxDepth) continue;
1551
- const neighborEdges = allEdges.filter((e) => e.sourceNodeId === nodeId || e.targetNodeId === nodeId);
1552
- for (const edge of neighborEdges) {
1553
- const nextId = edge.sourceNodeId === nodeId ? edge.targetNodeId : edge.sourceNodeId;
1554
- if (visited.has(nextId)) continue;
1555
- visited.add(nextId);
1556
- const nextNode = allNodes.find((n) => n.id === nextId);
1629
+ const neighbors = adjacency.get(nodeId) ?? [];
1630
+ for (const { neighborId, edge } of neighbors) {
1631
+ const nextNode = nodeIndex.get(neighborId);
1557
1632
  if (!nextNode) continue;
1558
- queue.push({
1559
- nodeId: nextId,
1560
- depth: depth + 1,
1561
- path: [...path, {
1633
+ const newCost = cost + 1 / Math.max(edge.weight, .01);
1634
+ if (newCost < (dist.get(neighborId) ?? Infinity)) {
1635
+ dist.set(neighborId, newCost);
1636
+ pathTo.set(neighborId, [...pathTo.get(nodeId), {
1562
1637
  node: nextNode,
1563
1638
  edge
1564
- }]
1565
- });
1639
+ }]);
1640
+ heap.push({
1641
+ nodeId: neighborId,
1642
+ depth: depth + 1,
1643
+ cost: newCost
1644
+ });
1645
+ }
1566
1646
  }
1567
1647
  }
1568
- return paths;
1648
+ pathTo.delete(startNode.id);
1649
+ return Array.from(pathTo.values());
1650
+ }
1651
+ };
1652
+ var MinHeap = class {
1653
+ heap = [];
1654
+ constructor(compare) {
1655
+ this.compare = compare;
1656
+ }
1657
+ size() {
1658
+ return this.heap.length;
1659
+ }
1660
+ push(value) {
1661
+ this.heap.push(value);
1662
+ this.bubbleUp(this.heap.length - 1);
1663
+ }
1664
+ pop() {
1665
+ if (this.heap.length === 0) return void 0;
1666
+ const top = this.heap[0];
1667
+ const last = this.heap.pop();
1668
+ if (this.heap.length > 0) {
1669
+ this.heap[0] = last;
1670
+ this.sinkDown(0);
1671
+ }
1672
+ return top;
1673
+ }
1674
+ bubbleUp(i) {
1675
+ while (i > 0) {
1676
+ const parent = i - 1 >> 1;
1677
+ if (this.compare(this.heap[i], this.heap[parent]) < 0) {
1678
+ [this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
1679
+ i = parent;
1680
+ } else break;
1681
+ }
1682
+ }
1683
+ sinkDown(i) {
1684
+ const n = this.heap.length;
1685
+ while (true) {
1686
+ const left = 2 * i + 1;
1687
+ const right = 2 * i + 2;
1688
+ let smallest = i;
1689
+ if (left < n && this.compare(this.heap[left], this.heap[smallest]) < 0) smallest = left;
1690
+ if (right < n && this.compare(this.heap[right], this.heap[smallest]) < 0) smallest = right;
1691
+ if (smallest === i) break;
1692
+ [this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
1693
+ i = smallest;
1694
+ }
1569
1695
  }
1570
1696
  };
1571
1697
 
@@ -4543,7 +4669,11 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4543
4669
  sdk.registerFunction("mem::context", async (data) => {
4544
4670
  const budget = data.budget || tokenBudget;
4545
4671
  const blocks = [];
4546
- const [pinnedSlots, profile] = await Promise.all([isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]), kv.get(KV.profiles, data.project).catch(() => null)]);
4672
+ const [pinnedSlots, profile, lessons] = await Promise.all([
4673
+ isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]),
4674
+ kv.get(KV.profiles, data.project).catch(() => null),
4675
+ kv.list(KV.lessons).catch(() => [])
4676
+ ]);
4547
4677
  const slotContent = renderPinnedContext(pinnedSlots);
4548
4678
  if (slotContent) blocks.push({
4549
4679
  type: "memory",
@@ -4567,6 +4697,24 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4567
4697
  });
4568
4698
  }
4569
4699
  }
4700
+ const relevantLessons = lessons.filter((l) => !l.deleted && (!l.project || l.project === data.project)).sort((a, b) => {
4701
+ const scoreA = (a.project === data.project ? 1.5 : 1) * a.confidence;
4702
+ return (b.project === data.project ? 1.5 : 1) * b.confidence - scoreA;
4703
+ }).slice(0, 10);
4704
+ if (relevantLessons.length > 0) {
4705
+ const lessonsContent = `## Lessons Learned\n${relevantLessons.map((l) => `- (${l.confidence.toFixed(2)}) ${l.content}${l.context ? ` — ${l.context}` : ""}`).join("\n")}`;
4706
+ const mostRecent = relevantLessons.reduce((acc, l) => {
4707
+ const t = new Date(l.lastReinforcedAt || l.updatedAt).getTime();
4708
+ return t > acc ? t : acc;
4709
+ }, 0);
4710
+ blocks.push({
4711
+ type: "memory",
4712
+ content: lessonsContent,
4713
+ tokens: estimateTokens$1(lessonsContent),
4714
+ recency: mostRecent,
4715
+ sourceIds: relevantLessons.map((l) => l.id)
4716
+ });
4717
+ }
4570
4718
  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
4719
  const summariesPerSession = await Promise.all(sessions.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
4572
4720
  const sessionsNeedingObs = [];
@@ -5389,6 +5537,46 @@ const DEFAULTS$1 = {
5389
5537
  lowImportanceThreshold: 3,
5390
5538
  maxObservationsPerProject: 1e4
5391
5539
  };
5540
+ function isValidRecoveryResult(result) {
5541
+ if (!result || typeof result !== "object") return false;
5542
+ if (!("success" in result)) return true;
5543
+ return result.success !== false;
5544
+ }
5545
+ function isCompressedObservation(observation) {
5546
+ return "title" in observation && typeof observation.title === "string" && observation.title.length > 0;
5547
+ }
5548
+ async function recoverStaleSession(sdk, sessionId) {
5549
+ try {
5550
+ const result = await sdk.trigger({
5551
+ function_id: "event::session::stopped",
5552
+ payload: { sessionId }
5553
+ });
5554
+ if (!isValidRecoveryResult(result)) {
5555
+ logger.warn("Stale session recovery failed", {
5556
+ sessionId,
5557
+ result
5558
+ });
5559
+ return false;
5560
+ }
5561
+ return true;
5562
+ } catch (err) {
5563
+ logger.warn("Stale session recovery failed", {
5564
+ sessionId,
5565
+ error: err instanceof Error ? err.message : String(err)
5566
+ });
5567
+ return false;
5568
+ }
5569
+ }
5570
+ async function runRecoveredSessionConsolidation(sdk) {
5571
+ try {
5572
+ await sdk.trigger({
5573
+ function_id: "mem::consolidate-pipeline",
5574
+ payload: { tier: "all" }
5575
+ });
5576
+ } catch (err) {
5577
+ logger.warn("Recovered session consolidation failed", { error: err instanceof Error ? err.message : String(err) });
5578
+ }
5579
+ }
5392
5580
  function registerEvictFunction(sdk, kv) {
5393
5581
  sdk.registerFunction("mem::evict", async (data) => {
5394
5582
  const dryRun = data?.dryRun ?? false;
@@ -5407,6 +5595,7 @@ function registerEvictFunction(sdk, kv) {
5407
5595
  nonLatestMemories: 0,
5408
5596
  dryRun
5409
5597
  };
5598
+ let recoveredStaleSessions = 0;
5410
5599
  const sessions = await kv.list(KV.sessions).catch(() => []);
5411
5600
  const summaries = await kv.list(KV.summaries).catch(() => []);
5412
5601
  const summaryIds = new Set(summaries.map((s) => s.sessionId));
@@ -5414,6 +5603,23 @@ function registerEvictFunction(sdk, kv) {
5414
5603
  if (!session.startedAt) continue;
5415
5604
  if (now - new Date(session.startedAt).getTime() > cfg.staleSessionDays * MS_PER_DAY$1 && !summaryIds.has(session.id)) if (dryRun) stats.staleSessions++;
5416
5605
  else {
5606
+ const observations = await kv.list(KV.observations(session.id)).catch((err) => {
5607
+ logger.warn("Stale session observation scan failed", {
5608
+ sessionId: session.id,
5609
+ error: err instanceof Error ? err.message : String(err)
5610
+ });
5611
+ return null;
5612
+ });
5613
+ if (!observations) continue;
5614
+ let recovered = false;
5615
+ if (observations.some(isCompressedObservation)) {
5616
+ recovered = await recoverStaleSession(sdk, session.id);
5617
+ if (!recovered) continue;
5618
+ recoveredStaleSessions++;
5619
+ } else if (observations.length > 0) {
5620
+ logger.warn("Stale session has no compressed observations", { sessionId: session.id });
5621
+ continue;
5622
+ }
5417
5623
  try {
5418
5624
  await kv.delete(KV.sessions, session.id);
5419
5625
  stats.staleSessions++;
@@ -5427,11 +5633,12 @@ function registerEvictFunction(sdk, kv) {
5427
5633
  }
5428
5634
  await recordAudit(kv, "delete", "mem::evict", [session.id], {
5429
5635
  resource: "session",
5430
- reason: "stale_session_without_summary",
5636
+ reason: recovered ? "stale_session_recovered_then_evicted" : "stale_session_without_summary",
5431
5637
  dryRun
5432
5638
  });
5433
5639
  }
5434
5640
  }
5641
+ if (!dryRun && recoveredStaleSessions > 0) await runRecoveredSessionConsolidation(sdk);
5435
5642
  const projectObs = /* @__PURE__ */ new Map();
5436
5643
  for (const session of sessions) {
5437
5644
  const compressed = (await kv.list(KV.observations(session.id)).catch(() => [])).filter((o) => o.title);
@@ -6110,7 +6317,7 @@ function registerAutoForgetFunction(sdk, kv) {
6110
6317
 
6111
6318
  //#endregion
6112
6319
  //#region src/version.ts
6113
- const VERSION = "0.9.17";
6320
+ const VERSION = "0.9.19";
6114
6321
 
6115
6322
  //#endregion
6116
6323
  //#region src/functions/export-import.ts
@@ -6245,7 +6452,9 @@ function registerExportImportFunction(sdk, kv) {
6245
6452
  "0.9.14",
6246
6453
  "0.9.15",
6247
6454
  "0.9.16",
6248
- "0.9.17"
6455
+ "0.9.17",
6456
+ "0.9.18",
6457
+ "0.9.19"
6249
6458
  ]).has(importData.version)) return {
6250
6459
  success: false,
6251
6460
  error: `Unsupported export version: ${importData.version}`
@@ -14073,6 +14282,112 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14073
14282
  middleware_function_ids: ["middleware::api-auth"]
14074
14283
  }
14075
14284
  });
14285
+ sdk.registerFunction("api::session::commit", async (req) => {
14286
+ const body = req.body ?? {};
14287
+ const sha = asNonEmptyString$1(body.sha);
14288
+ if (!sha) return {
14289
+ status_code: 400,
14290
+ body: { error: "sha is required and must be a non-empty string" }
14291
+ };
14292
+ const sessionId = asNonEmptyString$1(body.sessionId) ?? void 0;
14293
+ const branch = asNonEmptyString$1(body.branch) ?? void 0;
14294
+ const repo = asNonEmptyString$1(body.repo) ?? void 0;
14295
+ const message = asNonEmptyString$1(body.message) ?? void 0;
14296
+ const author = asNonEmptyString$1(body.author) ?? void 0;
14297
+ const authoredAt = asNonEmptyString$1(body.authoredAt) ?? void 0;
14298
+ const files = Array.isArray(body.files) ? body.files.filter((f) => typeof f === "string" && f.length > 0) : void 0;
14299
+ const link = await withKeyedLock(`commit:${sha}`, async () => {
14300
+ const existing = await kv.get(KV.commits, sha);
14301
+ const sessionSet = new Set(existing?.sessionIds ?? []);
14302
+ if (sessionId) sessionSet.add(sessionId);
14303
+ const merged = {
14304
+ sha,
14305
+ shortSha: existing?.shortSha ?? sha.slice(0, 7),
14306
+ branch: branch ?? existing?.branch,
14307
+ repo: repo ?? existing?.repo,
14308
+ message: message ?? existing?.message,
14309
+ author: author ?? existing?.author,
14310
+ authoredAt: authoredAt ?? existing?.authoredAt,
14311
+ files: files ?? existing?.files,
14312
+ sessionIds: Array.from(sessionSet),
14313
+ linkedAt: existing?.linkedAt ?? (/* @__PURE__ */ new Date()).toISOString()
14314
+ };
14315
+ await kv.set(KV.commits, sha, merged);
14316
+ return merged;
14317
+ });
14318
+ if (sessionId) await withKeyedLock(`session:${sessionId}`, async () => {
14319
+ const session = await kv.get(KV.sessions, sessionId);
14320
+ if (!session) return;
14321
+ const shaSet = new Set(session.commitShas ?? []);
14322
+ shaSet.add(sha);
14323
+ session.commitShas = Array.from(shaSet);
14324
+ await kv.set(KV.sessions, sessionId, session);
14325
+ });
14326
+ return {
14327
+ status_code: 200,
14328
+ body: { commit: link }
14329
+ };
14330
+ });
14331
+ sdk.registerTrigger({
14332
+ type: "http",
14333
+ function_id: "api::session::commit",
14334
+ config: {
14335
+ api_path: "/agentmemory/session/commit",
14336
+ http_method: "POST",
14337
+ middleware_function_ids: ["middleware::api-auth"]
14338
+ }
14339
+ });
14340
+ sdk.registerFunction("api::session::by-commit", async (req) => {
14341
+ const authErr = checkAuth(req, secret);
14342
+ if (authErr) return authErr;
14343
+ const sha = asNonEmptyString$1(req.query_params?.["sha"]);
14344
+ if (!sha) return {
14345
+ status_code: 400,
14346
+ body: { error: "sha is required and must be a non-empty string" }
14347
+ };
14348
+ const link = await kv.get(KV.commits, sha);
14349
+ if (!link) return {
14350
+ status_code: 404,
14351
+ body: { error: "no sessions linked to this commit" }
14352
+ };
14353
+ return {
14354
+ status_code: 200,
14355
+ body: {
14356
+ commit: link,
14357
+ sessions: (await Promise.all((link.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null)
14358
+ }
14359
+ };
14360
+ });
14361
+ sdk.registerTrigger({
14362
+ type: "http",
14363
+ function_id: "api::session::by-commit",
14364
+ config: {
14365
+ api_path: "/agentmemory/session/by-commit",
14366
+ http_method: "GET",
14367
+ middleware_function_ids: ["middleware::api-auth"]
14368
+ }
14369
+ });
14370
+ sdk.registerFunction("api::commits", async (req) => {
14371
+ const authErr = checkAuth(req, secret);
14372
+ if (authErr) return authErr;
14373
+ const branch = asNonEmptyString$1(req.query_params?.["branch"]);
14374
+ const repo = asNonEmptyString$1(req.query_params?.["repo"]);
14375
+ const rawLimit = parseOptionalInt(req.query_params?.["limit"]);
14376
+ const limit = Math.max(1, Math.min(500, rawLimit ?? 100));
14377
+ return {
14378
+ status_code: 200,
14379
+ body: { commits: (await kv.list(KV.commits)).filter((c) => !branch || c.branch === branch).filter((c) => !repo || c.repo === repo).sort((a, b) => (a.linkedAt ?? "") < (b.linkedAt ?? "") ? 1 : -1).slice(0, limit) }
14380
+ };
14381
+ });
14382
+ sdk.registerTrigger({
14383
+ type: "http",
14384
+ function_id: "api::commits",
14385
+ config: {
14386
+ api_path: "/agentmemory/commits",
14387
+ http_method: "GET",
14388
+ middleware_function_ids: ["middleware::api-auth"]
14389
+ }
14390
+ });
14076
14391
  sdk.registerFunction("api::sessions", async (req) => {
14077
14392
  const authErr = checkAuth(req, secret);
14078
14393
  if (authErr) return authErr;
@@ -17073,6 +17388,39 @@ const CORE_TOOLS = [
17073
17388
  },
17074
17389
  required: ["memoryId"]
17075
17390
  }
17391
+ },
17392
+ {
17393
+ name: "memory_commit_lookup",
17394
+ description: "Look up the agent session(s) that produced a specific git commit, given its SHA. Returns the commit metadata and linked sessions.",
17395
+ inputSchema: {
17396
+ type: "object",
17397
+ properties: { sha: {
17398
+ type: "string",
17399
+ description: "Full git commit SHA"
17400
+ } },
17401
+ required: ["sha"]
17402
+ }
17403
+ },
17404
+ {
17405
+ name: "memory_commits",
17406
+ description: "List recent commits linked to agent sessions, optionally filtered by branch or repo.",
17407
+ inputSchema: {
17408
+ type: "object",
17409
+ properties: {
17410
+ branch: {
17411
+ type: "string",
17412
+ description: "Filter by branch name"
17413
+ },
17414
+ repo: {
17415
+ type: "string",
17416
+ description: "Filter by remote URL"
17417
+ },
17418
+ limit: {
17419
+ type: "number",
17420
+ description: "Max results (default 100, max 500)"
17421
+ }
17422
+ }
17423
+ }
17076
17424
  }
17077
17425
  ];
17078
17426
  const V040_TOOLS = [
@@ -19127,6 +19475,49 @@ function registerMcpEndpoints(sdk, kv, secret) {
19127
19475
  }] }
19128
19476
  };
19129
19477
  }
19478
+ case "memory_commit_lookup": {
19479
+ const sha = asNonEmptyString(args.sha);
19480
+ if (!sha) return {
19481
+ status_code: 400,
19482
+ body: { error: "sha required" }
19483
+ };
19484
+ const link = await kv.get(KV.commits, sha);
19485
+ if (!link) return {
19486
+ status_code: 200,
19487
+ body: { content: [{
19488
+ type: "text",
19489
+ text: JSON.stringify({
19490
+ commit: null,
19491
+ sessions: []
19492
+ }, null, 2)
19493
+ }] }
19494
+ };
19495
+ const linkRecord = link;
19496
+ const sessions = (await Promise.all((linkRecord.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null);
19497
+ return {
19498
+ status_code: 200,
19499
+ body: { content: [{
19500
+ type: "text",
19501
+ text: JSON.stringify({
19502
+ commit: link,
19503
+ sessions
19504
+ }, null, 2)
19505
+ }] }
19506
+ };
19507
+ }
19508
+ case "memory_commits": {
19509
+ const branch = typeof args.branch === "string" ? args.branch : void 0;
19510
+ const repo = typeof args.repo === "string" ? args.repo : void 0;
19511
+ const limit = Math.max(1, Math.min(500, asNumber(args.limit, 100) ?? 100));
19512
+ const filtered = (await kv.list(KV.commits)).filter((c) => !branch || c.branch === branch).filter((c) => !repo || c.repo === repo).sort((a, b) => (a.linkedAt ?? "") < (b.linkedAt ?? "") ? 1 : -1).slice(0, limit);
19513
+ return {
19514
+ status_code: 200,
19515
+ body: { content: [{
19516
+ type: "text",
19517
+ text: JSON.stringify({ commits: filtered }, null, 2)
19518
+ }] }
19519
+ };
19520
+ }
19130
19521
  default: return {
19131
19522
  status_code: 400,
19132
19523
  body: { error: `Unknown tool: ${name}` }
@@ -19512,7 +19903,38 @@ function registerMcpEndpoints(sdk, kv, secret) {
19512
19903
 
19513
19904
  //#endregion
19514
19905
  //#region src/viewer/server.ts
19906
+ function loadViewerFavicon() {
19907
+ const base = dirname(fileURLToPath(import.meta.url));
19908
+ const candidates = [
19909
+ join(base, "..", "src", "viewer", "favicon.svg"),
19910
+ join(base, "..", "viewer", "favicon.svg"),
19911
+ join(base, "viewer", "favicon.svg")
19912
+ ];
19913
+ for (const path of candidates) try {
19914
+ return readFileSync(path);
19915
+ } catch {}
19916
+ return null;
19917
+ }
19515
19918
  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());
19919
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
19920
+ function buildAllowedHosts(origins, listenPort) {
19921
+ const hosts = /* @__PURE__ */ new Set();
19922
+ for (const o of origins) try {
19923
+ const parsed = new URL(o);
19924
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
19925
+ } catch {}
19926
+ hosts.add(`localhost:${listenPort}`);
19927
+ hosts.add(`127.0.0.1:${listenPort}`);
19928
+ hosts.add(`[::1]:${listenPort}`);
19929
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
19930
+ return hosts;
19931
+ }
19932
+ function isHostAllowed(headerHost, allowed) {
19933
+ if (typeof headerHost !== "string") return false;
19934
+ const lower = headerHost.toLowerCase().trim();
19935
+ if (!lower) return false;
19936
+ return allowed.has(lower);
19937
+ }
19516
19938
  function corsHeaders(req) {
19517
19939
  const origin = req.headers.origin || "";
19518
19940
  const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
@@ -19556,7 +19978,17 @@ const MAX_VIEWER_PORT_RETRIES = 10;
19556
19978
  function startViewerServer(port, _kv, _sdk, secret, restPort) {
19557
19979
  const resolvedRestPort = restPort ?? port - 2;
19558
19980
  const requestedPort = port;
19981
+ let allowedHosts = null;
19559
19982
  const server = createServer(async (req, res) => {
19983
+ if (!allowedHosts) {
19984
+ const addr = server.address();
19985
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
19986
+ }
19987
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
19988
+ res.writeHead(403, { "Content-Type": "text/plain" });
19989
+ res.end("forbidden host");
19990
+ return;
19991
+ }
19560
19992
  const raw = req.url || "/";
19561
19993
  const qIdx = raw.indexOf("?");
19562
19994
  const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
@@ -19585,6 +20017,20 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
19585
20017
  res.end("viewer not found");
19586
20018
  return;
19587
20019
  }
20020
+ if (method === "GET" && pathname === "/favicon.svg") {
20021
+ const favicon = loadViewerFavicon();
20022
+ if (favicon) {
20023
+ res.writeHead(200, {
20024
+ "Content-Type": "image/svg+xml",
20025
+ "Cache-Control": "public, max-age=3600"
20026
+ });
20027
+ res.end(favicon);
20028
+ return;
20029
+ }
20030
+ res.writeHead(404, { "Content-Type": "text/plain" });
20031
+ res.end("favicon not found");
20032
+ return;
20033
+ }
19588
20034
  try {
19589
20035
  await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
19590
20036
  } catch (err) {
@@ -19984,7 +20430,7 @@ async function main() {
19984
20430
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
19985
20431
  }
19986
20432
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
19987
- bootLog(`REST API: 121 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
20433
+ bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
19988
20434
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
19989
20435
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
19990
20436
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);