@agentmemory/agentmemory 0.9.18 → 0.9.20

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.
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+
5
+ //#region src/hooks/post-commit.ts
6
+ const exec = promisify(execFile);
7
+ function isSdkChildContext(payload) {
8
+ if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
9
+ if (!payload || typeof payload !== "object") return false;
10
+ return payload.entrypoint === "sdk-ts";
11
+ }
12
+ const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
13
+ const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
14
+ const TIMEOUT_MS = 1500;
15
+ function authHeaders() {
16
+ const h = { "Content-Type": "application/json" };
17
+ if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
18
+ return h;
19
+ }
20
+ async function git(args, cwd) {
21
+ try {
22
+ const { stdout } = await exec("git", args, {
23
+ cwd,
24
+ timeout: 1500
25
+ });
26
+ return stdout.trim();
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ async function main() {
32
+ let input = "";
33
+ for await (const chunk of process.stdin) input += chunk;
34
+ let data = {};
35
+ if (input.trim()) try {
36
+ data = JSON.parse(input);
37
+ } catch {}
38
+ if (isSdkChildContext(data)) return;
39
+ const cwd = data.cwd || process.env["AGENTMEMORY_CWD"] || process.cwd();
40
+ const sessionId = data.session_id || process.env["AGENTMEMORY_SESSION_ID"] || void 0;
41
+ const sha = process.env["AGENTMEMORY_COMMIT_SHA"] || await git(["rev-parse", "HEAD"], cwd);
42
+ if (!sha) return;
43
+ const branch = await git([
44
+ "rev-parse",
45
+ "--abbrev-ref",
46
+ "HEAD"
47
+ ], cwd);
48
+ const repo = await git([
49
+ "config",
50
+ "--get",
51
+ "remote.origin.url"
52
+ ], cwd);
53
+ const message = await git([
54
+ "log",
55
+ "-1",
56
+ "--pretty=%B",
57
+ sha
58
+ ], cwd);
59
+ const author = await git([
60
+ "log",
61
+ "-1",
62
+ "--pretty=%an <%ae>",
63
+ sha
64
+ ], cwd);
65
+ const authoredAt = await git([
66
+ "log",
67
+ "-1",
68
+ "--pretty=%aI",
69
+ sha
70
+ ], cwd);
71
+ const filesRaw = await git([
72
+ "diff-tree",
73
+ "--no-commit-id",
74
+ "--name-only",
75
+ "-r",
76
+ sha
77
+ ], cwd);
78
+ const files = filesRaw ? filesRaw.split("\n").filter(Boolean) : void 0;
79
+ const body = {
80
+ sessionId,
81
+ sha,
82
+ branch: branch || void 0,
83
+ repo: repo || void 0,
84
+ message: message || void 0,
85
+ author: author || void 0,
86
+ authoredAt: authoredAt || void 0,
87
+ files
88
+ };
89
+ try {
90
+ await fetch(`${REST_URL}/agentmemory/session/commit`, {
91
+ method: "POST",
92
+ headers: authHeaders(),
93
+ body: JSON.stringify(body),
94
+ signal: AbortSignal.timeout(TIMEOUT_MS)
95
+ });
96
+ } catch {}
97
+ }
98
+ main();
99
+
100
+ //#endregion
101
+ export { };
102
+ //# sourceMappingURL=post-commit.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-commit.mjs","names":[],"sources":["../../src/hooks/post-commit.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst exec = promisify(execFile);\n\nfunction isSdkChildContext(payload: unknown): boolean {\n if (process.env[\"AGENTMEMORY_SDK_CHILD\"] === \"1\") return true;\n if (!payload || typeof payload !== \"object\") return false;\n return (payload as { entrypoint?: unknown }).entrypoint === \"sdk-ts\";\n}\n\nconst REST_URL = process.env[\"AGENTMEMORY_URL\"] || \"http://localhost:3111\";\nconst SECRET = process.env[\"AGENTMEMORY_SECRET\"] || \"\";\nconst TIMEOUT_MS = 1500;\n\nfunction authHeaders(): Record<string, string> {\n const h: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (SECRET) h[\"Authorization\"] = `Bearer ${SECRET}`;\n return h;\n}\n\nasync function git(args: string[], cwd: string): Promise<string | null> {\n try {\n const { stdout } = await exec(\"git\", args, { cwd, timeout: 1500 });\n return stdout.trim();\n } catch {\n return null;\n }\n}\n\nasync function main() {\n let input = \"\";\n for await (const chunk of process.stdin) {\n input += chunk;\n }\n\n let data: Record<string, unknown> = {};\n if (input.trim()) {\n try {\n data = JSON.parse(input);\n } catch {\n // Direct invocation from .git/hooks/post-commit may pass no stdin.\n }\n }\n\n if (isSdkChildContext(data)) return;\n\n const cwd =\n (data.cwd as string) ||\n process.env[\"AGENTMEMORY_CWD\"] ||\n process.cwd();\n const sessionId =\n (data.session_id as string) ||\n process.env[\"AGENTMEMORY_SESSION_ID\"] ||\n undefined;\n\n const sha =\n process.env[\"AGENTMEMORY_COMMIT_SHA\"] ||\n (await git([\"rev-parse\", \"HEAD\"], cwd));\n if (!sha) return;\n\n const branch = await git([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], cwd);\n const repo = await git([\"config\", \"--get\", \"remote.origin.url\"], cwd);\n const message = await git([\"log\", \"-1\", \"--pretty=%B\", sha], cwd);\n const author = await git([\"log\", \"-1\", \"--pretty=%an <%ae>\", sha], cwd);\n const authoredAt = await git([\"log\", \"-1\", \"--pretty=%aI\", sha], cwd);\n const filesRaw = await git(\n [\"diff-tree\", \"--no-commit-id\", \"--name-only\", \"-r\", sha],\n cwd,\n );\n const files = filesRaw ? filesRaw.split(\"\\n\").filter(Boolean) : undefined;\n\n const body = {\n sessionId,\n sha,\n branch: branch || undefined,\n repo: repo || undefined,\n message: message || undefined,\n author: author || undefined,\n authoredAt: authoredAt || undefined,\n files,\n };\n\n try {\n await fetch(`${REST_URL}/agentmemory/session/commit`, {\n method: \"POST\",\n headers: authHeaders(),\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n } catch {\n // best-effort\n }\n}\n\nmain();\n"],"mappings":";;;;;AAKA,MAAM,OAAO,UAAU,SAAS;AAEhC,SAAS,kBAAkB,SAA2B;AACpD,KAAI,QAAQ,IAAI,6BAA6B,IAAK,QAAO;AACzD,KAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAQ,QAAqC,eAAe;;AAG9D,MAAM,WAAW,QAAQ,IAAI,sBAAsB;AACnD,MAAM,SAAS,QAAQ,IAAI,yBAAyB;AACpD,MAAM,aAAa;AAEnB,SAAS,cAAsC;CAC7C,MAAM,IAA4B,EAAE,gBAAgB,oBAAoB;AACxE,KAAI,OAAQ,GAAE,mBAAmB,UAAU;AAC3C,QAAO;;AAGT,eAAe,IAAI,MAAgB,KAAqC;AACtE,KAAI;EACF,MAAM,EAAE,WAAW,MAAM,KAAK,OAAO,MAAM;GAAE;GAAK,SAAS;GAAM,CAAC;AAClE,SAAO,OAAO,MAAM;SACd;AACN,SAAO;;;AAIX,eAAe,OAAO;CACpB,IAAI,QAAQ;AACZ,YAAW,MAAM,SAAS,QAAQ,MAChC,UAAS;CAGX,IAAI,OAAgC,EAAE;AACtC,KAAI,MAAM,MAAM,CACd,KAAI;AACF,SAAO,KAAK,MAAM,MAAM;SAClB;AAKV,KAAI,kBAAkB,KAAK,CAAE;CAE7B,MAAM,MACH,KAAK,OACN,QAAQ,IAAI,sBACZ,QAAQ,KAAK;CACf,MAAM,YACH,KAAK,cACN,QAAQ,IAAI,6BACZ;CAEF,MAAM,MACJ,QAAQ,IAAI,6BACX,MAAM,IAAI,CAAC,aAAa,OAAO,EAAE,IAAI;AACxC,KAAI,CAAC,IAAK;CAEV,MAAM,SAAS,MAAM,IAAI;EAAC;EAAa;EAAgB;EAAO,EAAE,IAAI;CACpE,MAAM,OAAO,MAAM,IAAI;EAAC;EAAU;EAAS;EAAoB,EAAE,IAAI;CACrE,MAAM,UAAU,MAAM,IAAI;EAAC;EAAO;EAAM;EAAe;EAAI,EAAE,IAAI;CACjE,MAAM,SAAS,MAAM,IAAI;EAAC;EAAO;EAAM;EAAsB;EAAI,EAAE,IAAI;CACvE,MAAM,aAAa,MAAM,IAAI;EAAC;EAAO;EAAM;EAAgB;EAAI,EAAE,IAAI;CACrE,MAAM,WAAW,MAAM,IACrB;EAAC;EAAa;EAAkB;EAAe;EAAM;EAAI,EACzD,IACD;CACD,MAAM,QAAQ,WAAW,SAAS,MAAM,KAAK,CAAC,OAAO,QAAQ,GAAG;CAEhE,MAAM,OAAO;EACX;EACA;EACA,QAAQ,UAAU;EAClB,MAAM,QAAQ;EACd,SAAS,WAAW;EACpB,QAAQ,UAAU;EAClB,YAAY,cAAc;EAC1B;EACD;AAED,KAAI;AACF,QAAM,MAAM,GAAG,SAAS,8BAA8B;GACpD,QAAQ;GACR,SAAS,aAAa;GACtB,MAAM,KAAK,UAAU,KAAK;GAC1B,QAAQ,YAAY,QAAQ,WAAW;GACxC,CAAC;SACI;;AAKV,MAAM"}
package/dist/index.mjs CHANGED
@@ -418,11 +418,63 @@ var NoopProvider = class {
418
418
  }
419
419
  };
420
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
+
421
475
  //#endregion
422
476
  //#region src/providers/openai.ts
423
- const DEFAULT_BASE_URL$1 = "https://api.openai.com";
424
477
  const DEFAULT_TIMEOUT_MS = 6e4;
425
- const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
426
478
  /**
427
479
  * OpenAI-compatible LLM provider.
428
480
  *
@@ -469,7 +521,7 @@ var OpenAIProvider = class {
469
521
  this.apiKey = apiKey;
470
522
  this.model = model;
471
523
  this.maxTokens = maxTokens;
472
- this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL$1).replace(/\/+$/, "");
524
+ this.baseUrl = normalizeBaseUrl(baseURL || getEnvVar("OPENAI_BASE_URL"));
473
525
  this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
474
526
  this.timeoutMs = resolveTimeout();
475
527
  this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
@@ -481,25 +533,8 @@ var OpenAIProvider = class {
481
533
  async summarize(systemPrompt, userPrompt) {
482
534
  return this.call(systemPrompt, userPrompt);
483
535
  }
484
- buildUrl() {
485
- if (this.isAzure) {
486
- const sep = this.baseUrl.includes("?") ? "&" : "?";
487
- return `${this.baseUrl}/chat/completions${sep}api-version=${encodeURIComponent(this.azureApiVersion)}`;
488
- }
489
- return `${this.baseUrl}/v1/chat/completions`;
490
- }
491
- buildHeaders() {
492
- if (this.isAzure) return {
493
- "Content-Type": "application/json",
494
- "api-key": this.apiKey
495
- };
496
- return {
497
- "Content-Type": "application/json",
498
- Authorization: `Bearer ${this.apiKey}`
499
- };
500
- }
501
536
  async call(systemPrompt, userPrompt) {
502
- const url = this.buildUrl();
537
+ const url = buildChatUrl(this.baseUrl, this.isAzure, this.azureApiVersion);
503
538
  const body = {
504
539
  model: this.model,
505
540
  max_tokens: this.maxTokens,
@@ -516,7 +551,7 @@ var OpenAIProvider = class {
516
551
  try {
517
552
  response = await fetchWithTimeout(url, {
518
553
  method: "POST",
519
- headers: this.buildHeaders(),
554
+ headers: buildAuthHeaders(this.apiKey, this.isAzure),
520
555
  body: JSON.stringify(body)
521
556
  }, this.timeoutMs);
522
557
  } catch (err) {
@@ -550,13 +585,6 @@ function parsePositiveInt(raw) {
550
585
  const n = Number(trimmed);
551
586
  return Number.isFinite(n) && n > 0 ? n : void 0;
552
587
  }
553
- function detectAzure(baseUrl) {
554
- try {
555
- return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
556
- } catch {
557
- return false;
558
- }
559
- }
560
588
 
561
589
  //#endregion
562
590
  //#region src/providers/openrouter.ts
@@ -786,7 +814,6 @@ function l2Normalize(vec) {
786
814
 
787
815
  //#endregion
788
816
  //#region src/providers/embedding/openai.ts
789
- const DEFAULT_BASE_URL = "https://api.openai.com";
790
817
  const DEFAULT_MODEL$1 = "text-embedding-3-small";
791
818
  /**
792
819
  * Known OpenAI embedding model dimensions. Extend as new models ship.
@@ -810,11 +837,20 @@ function resolveDimensions(model, override) {
810
837
  /**
811
838
  * OpenAI-compatible embedding provider.
812
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
+ *
813
847
  * Required env vars:
814
848
  * OPENAI_API_KEY — API key
815
849
  *
816
850
  * Optional:
817
- * 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)
818
854
  * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
819
855
  * OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
820
856
  * custom / self-hosted models not in the
@@ -826,24 +862,25 @@ var OpenAIEmbeddingProvider = class {
826
862
  apiKey;
827
863
  baseUrl;
828
864
  model;
865
+ isAzure;
866
+ azureApiVersion;
829
867
  constructor(apiKey) {
830
868
  this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
831
869
  if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
832
- this.baseUrl = getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL;
870
+ this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
833
871
  this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
834
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;
835
875
  }
836
876
  async embed(text) {
837
877
  const [result] = await this.embedBatch([text]);
838
878
  return result;
839
879
  }
840
880
  async embedBatch(texts) {
841
- const response = await fetchWithTimeout(`${this.baseUrl}/v1/embeddings`, {
881
+ const response = await fetchWithTimeout(buildEmbeddingUrl(this.baseUrl, this.isAzure, this.azureApiVersion), {
842
882
  method: "POST",
843
- headers: {
844
- Authorization: `Bearer ${this.apiKey}`,
845
- "Content-Type": "application/json"
846
- },
883
+ headers: buildAuthHeaders(this.apiKey, this.isAzure),
847
884
  body: JSON.stringify({
848
885
  model: this.model,
849
886
  input: texts
@@ -1250,7 +1287,8 @@ const KV = {
1250
1287
  imageEmbeddings: "mem:image-embeddings",
1251
1288
  slots: "mem:slots",
1252
1289
  globalSlots: "mem:slots:global",
1253
- state: "mem:state"
1290
+ state: "mem:state",
1291
+ commits: "mem:commits"
1254
1292
  };
1255
1293
  const STREAM = {
1256
1294
  name: "mem-live",
@@ -1444,7 +1482,7 @@ var GraphRetrieval = class {
1444
1482
  const results = [];
1445
1483
  const visitedObs = /* @__PURE__ */ new Set();
1446
1484
  for (const startNode of matchingNodes) {
1447
- const paths = this.bfsTraversal(startNode, allNodes, allEdges, maxDepth);
1485
+ const paths = this.dijkstraTraversal(startNode, allNodes, allEdges, maxDepth);
1448
1486
  for (const path of paths) {
1449
1487
  const lastNode = path[path.length - 1].node;
1450
1488
  for (const obsId of lastNode.sourceObservationIds) {
@@ -1484,7 +1522,7 @@ var GraphRetrieval = class {
1484
1522
  const results = [];
1485
1523
  const visitedObs = new Set(obsIds);
1486
1524
  for (const node of linkedNodes) {
1487
- const paths = this.bfsTraversal(node, allNodes, allEdges, maxDepth);
1525
+ const paths = this.dijkstraTraversal(node, allNodes, allEdges, maxDepth);
1488
1526
  for (const path of paths) {
1489
1527
  const lastNode = path[path.length - 1].node;
1490
1528
  for (const obsId of lastNode.sourceObservationIds) {
@@ -1556,37 +1594,104 @@ var GraphRetrieval = class {
1556
1594
  }
1557
1595
  return latest;
1558
1596
  }
1559
- bfsTraversal(startNode, allNodes, allEdges, maxDepth) {
1560
- const paths = [];
1561
- const visited = /* @__PURE__ */ new Set();
1562
- 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({
1563
1621
  nodeId: startNode.id,
1564
1622
  depth: 0,
1565
- path: [{ node: startNode }]
1566
- }];
1567
- visited.add(startNode.id);
1568
- while (queue.length > 0) {
1569
- const { nodeId, depth, path } = queue.shift();
1570
- 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;
1571
1628
  if (depth >= maxDepth) continue;
1572
- const neighborEdges = allEdges.filter((e) => e.sourceNodeId === nodeId || e.targetNodeId === nodeId);
1573
- for (const edge of neighborEdges) {
1574
- const nextId = edge.sourceNodeId === nodeId ? edge.targetNodeId : edge.sourceNodeId;
1575
- if (visited.has(nextId)) continue;
1576
- visited.add(nextId);
1577
- 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);
1578
1632
  if (!nextNode) continue;
1579
- queue.push({
1580
- nodeId: nextId,
1581
- depth: depth + 1,
1582
- 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), {
1583
1637
  node: nextNode,
1584
1638
  edge
1585
- }]
1586
- });
1639
+ }]);
1640
+ heap.push({
1641
+ nodeId: neighborId,
1642
+ depth: depth + 1,
1643
+ cost: newCost
1644
+ });
1645
+ }
1587
1646
  }
1588
1647
  }
1589
- 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
+ }
1590
1695
  }
1591
1696
  };
1592
1697
 
@@ -6212,7 +6317,7 @@ function registerAutoForgetFunction(sdk, kv) {
6212
6317
 
6213
6318
  //#endregion
6214
6319
  //#region src/version.ts
6215
- const VERSION = "0.9.18";
6320
+ const VERSION = "0.9.20";
6216
6321
 
6217
6322
  //#endregion
6218
6323
  //#region src/functions/export-import.ts
@@ -6348,7 +6453,9 @@ function registerExportImportFunction(sdk, kv) {
6348
6453
  "0.9.15",
6349
6454
  "0.9.16",
6350
6455
  "0.9.17",
6351
- "0.9.18"
6456
+ "0.9.18",
6457
+ "0.9.19",
6458
+ "0.9.20"
6352
6459
  ]).has(importData.version)) return {
6353
6460
  success: false,
6354
6461
  error: `Unsupported export version: ${importData.version}`
@@ -14176,6 +14283,112 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14176
14283
  middleware_function_ids: ["middleware::api-auth"]
14177
14284
  }
14178
14285
  });
14286
+ sdk.registerFunction("api::session::commit", async (req) => {
14287
+ const body = req.body ?? {};
14288
+ const sha = asNonEmptyString$1(body.sha);
14289
+ if (!sha) return {
14290
+ status_code: 400,
14291
+ body: { error: "sha is required and must be a non-empty string" }
14292
+ };
14293
+ const sessionId = asNonEmptyString$1(body.sessionId) ?? void 0;
14294
+ const branch = asNonEmptyString$1(body.branch) ?? void 0;
14295
+ const repo = asNonEmptyString$1(body.repo) ?? void 0;
14296
+ const message = asNonEmptyString$1(body.message) ?? void 0;
14297
+ const author = asNonEmptyString$1(body.author) ?? void 0;
14298
+ const authoredAt = asNonEmptyString$1(body.authoredAt) ?? void 0;
14299
+ const files = Array.isArray(body.files) ? body.files.filter((f) => typeof f === "string" && f.length > 0) : void 0;
14300
+ const link = await withKeyedLock(`commit:${sha}`, async () => {
14301
+ const existing = await kv.get(KV.commits, sha);
14302
+ const sessionSet = new Set(existing?.sessionIds ?? []);
14303
+ if (sessionId) sessionSet.add(sessionId);
14304
+ const merged = {
14305
+ sha,
14306
+ shortSha: existing?.shortSha ?? sha.slice(0, 7),
14307
+ branch: branch ?? existing?.branch,
14308
+ repo: repo ?? existing?.repo,
14309
+ message: message ?? existing?.message,
14310
+ author: author ?? existing?.author,
14311
+ authoredAt: authoredAt ?? existing?.authoredAt,
14312
+ files: files ?? existing?.files,
14313
+ sessionIds: Array.from(sessionSet),
14314
+ linkedAt: existing?.linkedAt ?? (/* @__PURE__ */ new Date()).toISOString()
14315
+ };
14316
+ await kv.set(KV.commits, sha, merged);
14317
+ return merged;
14318
+ });
14319
+ if (sessionId) await withKeyedLock(`session:${sessionId}`, async () => {
14320
+ const session = await kv.get(KV.sessions, sessionId);
14321
+ if (!session) return;
14322
+ const shaSet = new Set(session.commitShas ?? []);
14323
+ shaSet.add(sha);
14324
+ session.commitShas = Array.from(shaSet);
14325
+ await kv.set(KV.sessions, sessionId, session);
14326
+ });
14327
+ return {
14328
+ status_code: 200,
14329
+ body: { commit: link }
14330
+ };
14331
+ });
14332
+ sdk.registerTrigger({
14333
+ type: "http",
14334
+ function_id: "api::session::commit",
14335
+ config: {
14336
+ api_path: "/agentmemory/session/commit",
14337
+ http_method: "POST",
14338
+ middleware_function_ids: ["middleware::api-auth"]
14339
+ }
14340
+ });
14341
+ sdk.registerFunction("api::session::by-commit", async (req) => {
14342
+ const authErr = checkAuth(req, secret);
14343
+ if (authErr) return authErr;
14344
+ const sha = asNonEmptyString$1(req.query_params?.["sha"]);
14345
+ if (!sha) return {
14346
+ status_code: 400,
14347
+ body: { error: "sha is required and must be a non-empty string" }
14348
+ };
14349
+ const link = await kv.get(KV.commits, sha);
14350
+ if (!link) return {
14351
+ status_code: 404,
14352
+ body: { error: "no sessions linked to this commit" }
14353
+ };
14354
+ return {
14355
+ status_code: 200,
14356
+ body: {
14357
+ commit: link,
14358
+ sessions: (await Promise.all((link.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null)
14359
+ }
14360
+ };
14361
+ });
14362
+ sdk.registerTrigger({
14363
+ type: "http",
14364
+ function_id: "api::session::by-commit",
14365
+ config: {
14366
+ api_path: "/agentmemory/session/by-commit",
14367
+ http_method: "GET",
14368
+ middleware_function_ids: ["middleware::api-auth"]
14369
+ }
14370
+ });
14371
+ sdk.registerFunction("api::commits", async (req) => {
14372
+ const authErr = checkAuth(req, secret);
14373
+ if (authErr) return authErr;
14374
+ const branch = asNonEmptyString$1(req.query_params?.["branch"]);
14375
+ const repo = asNonEmptyString$1(req.query_params?.["repo"]);
14376
+ const rawLimit = parseOptionalInt(req.query_params?.["limit"]);
14377
+ const limit = Math.max(1, Math.min(500, rawLimit ?? 100));
14378
+ return {
14379
+ status_code: 200,
14380
+ 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) }
14381
+ };
14382
+ });
14383
+ sdk.registerTrigger({
14384
+ type: "http",
14385
+ function_id: "api::commits",
14386
+ config: {
14387
+ api_path: "/agentmemory/commits",
14388
+ http_method: "GET",
14389
+ middleware_function_ids: ["middleware::api-auth"]
14390
+ }
14391
+ });
14179
14392
  sdk.registerFunction("api::sessions", async (req) => {
14180
14393
  const authErr = checkAuth(req, secret);
14181
14394
  if (authErr) return authErr;
@@ -17176,6 +17389,39 @@ const CORE_TOOLS = [
17176
17389
  },
17177
17390
  required: ["memoryId"]
17178
17391
  }
17392
+ },
17393
+ {
17394
+ name: "memory_commit_lookup",
17395
+ description: "Look up the agent session(s) that produced a specific git commit, given its SHA. Returns the commit metadata and linked sessions.",
17396
+ inputSchema: {
17397
+ type: "object",
17398
+ properties: { sha: {
17399
+ type: "string",
17400
+ description: "Full git commit SHA"
17401
+ } },
17402
+ required: ["sha"]
17403
+ }
17404
+ },
17405
+ {
17406
+ name: "memory_commits",
17407
+ description: "List recent commits linked to agent sessions, optionally filtered by branch or repo.",
17408
+ inputSchema: {
17409
+ type: "object",
17410
+ properties: {
17411
+ branch: {
17412
+ type: "string",
17413
+ description: "Filter by branch name"
17414
+ },
17415
+ repo: {
17416
+ type: "string",
17417
+ description: "Filter by remote URL"
17418
+ },
17419
+ limit: {
17420
+ type: "number",
17421
+ description: "Max results (default 100, max 500)"
17422
+ }
17423
+ }
17424
+ }
17179
17425
  }
17180
17426
  ];
17181
17427
  const V040_TOOLS = [
@@ -19230,6 +19476,49 @@ function registerMcpEndpoints(sdk, kv, secret) {
19230
19476
  }] }
19231
19477
  };
19232
19478
  }
19479
+ case "memory_commit_lookup": {
19480
+ const sha = asNonEmptyString(args.sha);
19481
+ if (!sha) return {
19482
+ status_code: 400,
19483
+ body: { error: "sha required" }
19484
+ };
19485
+ const link = await kv.get(KV.commits, sha);
19486
+ if (!link) return {
19487
+ status_code: 200,
19488
+ body: { content: [{
19489
+ type: "text",
19490
+ text: JSON.stringify({
19491
+ commit: null,
19492
+ sessions: []
19493
+ }, null, 2)
19494
+ }] }
19495
+ };
19496
+ const linkRecord = link;
19497
+ const sessions = (await Promise.all((linkRecord.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null);
19498
+ return {
19499
+ status_code: 200,
19500
+ body: { content: [{
19501
+ type: "text",
19502
+ text: JSON.stringify({
19503
+ commit: link,
19504
+ sessions
19505
+ }, null, 2)
19506
+ }] }
19507
+ };
19508
+ }
19509
+ case "memory_commits": {
19510
+ const branch = typeof args.branch === "string" ? args.branch : void 0;
19511
+ const repo = typeof args.repo === "string" ? args.repo : void 0;
19512
+ const limit = Math.max(1, Math.min(500, asNumber(args.limit, 100) ?? 100));
19513
+ 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);
19514
+ return {
19515
+ status_code: 200,
19516
+ body: { content: [{
19517
+ type: "text",
19518
+ text: JSON.stringify({ commits: filtered }, null, 2)
19519
+ }] }
19520
+ };
19521
+ }
19233
19522
  default: return {
19234
19523
  status_code: 400,
19235
19524
  body: { error: `Unknown tool: ${name}` }
@@ -20142,7 +20431,7 @@ async function main() {
20142
20431
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
20143
20432
  }
20144
20433
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
20145
- bootLog(`REST API: 121 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
20434
+ bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
20146
20435
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
20147
20436
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
20148
20437
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);