@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
@@ -1,6 +1,6 @@
1
1
  import { a as STREAM, c as jaccardSimilarity, i as KV, n as bootLog, o as fingerprintId, r as logger, s as generateId, t as VERSION } from "./cli.mjs";
2
2
  import { a as isManagedImagePath, getImageRefCount, i as getMaxBytes, n as IMAGES_DIR, r as deleteImage, t as withKeyedLock } from "./image-refs-R3tin9MR.mjs";
3
- import { _ as loadTeamConfig, a as getConsolidationDecayDays, c as isAutoCompressEnabled, d as isGraphExtractionEnabled, f as loadClaudeBridgeConfig, g as loadSnapshotConfig, h as loadFallbackConfig, i as detectLlmProviderKind, l as isConsolidationEnabled, m as loadEmbeddingConfig, n as getVisibleTools, o as getEnvVar, p as loadConfig, r as detectEmbeddingProvider, t as getAllTools, u as isContextInjectionEnabled } from "./tools-registry-BFKFKmYh.mjs";
3
+ import { _ as loadTeamConfig, a as getConsolidationDecayDays, c as isAutoCompressEnabled, d as isGraphExtractionEnabled, f as loadClaudeBridgeConfig, g as loadSnapshotConfig, h as loadFallbackConfig, i as detectLlmProviderKind, l as isConsolidationEnabled, m as loadEmbeddingConfig, n as getVisibleTools, o as getEnvVar, p as loadConfig, r as detectEmbeddingProvider, t as getAllTools, u as isContextInjectionEnabled } from "./tools-registry-Dz8ssuMf.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { execFile } from "node:child_process";
6
6
  import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -104,6 +104,20 @@ var AnthropicProvider = class {
104
104
  }
105
105
  };
106
106
 
107
+ //#endregion
108
+ //#region src/providers/_fetch.ts
109
+ function fetchWithTimeout(url, init, timeoutMs) {
110
+ const parsed = timeoutMs ?? Number.parseInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS") ?? "60000", 10);
111
+ const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : 6e4;
112
+ const ctl = new AbortController();
113
+ const signal = init.signal ? AbortSignal.any([init.signal, ctl.signal]) : ctl.signal;
114
+ const t = setTimeout(() => ctl.abort(), ms);
115
+ return fetch(url, {
116
+ ...init,
117
+ signal
118
+ }).finally(() => clearTimeout(t));
119
+ }
120
+
107
121
  //#endregion
108
122
  //#region src/providers/minimax.ts
109
123
  /**
@@ -139,8 +153,7 @@ var MinimaxProvider = class {
139
153
  return this.call(systemPrompt, userPrompt);
140
154
  }
141
155
  async call(systemPrompt, userPrompt) {
142
- const url = `${this.baseUrl}/v1/messages`;
143
- const response = await fetch(url, {
156
+ const response = await fetchWithTimeout(`${this.baseUrl}/v1/messages`, {
144
157
  method: "POST",
145
158
  headers: {
146
159
  "Content-Type": "application/json",
@@ -184,11 +197,63 @@ var NoopProvider = class {
184
197
  }
185
198
  };
186
199
 
200
+ //#endregion
201
+ //#region src/providers/_openai-shared.ts
202
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
203
+ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
204
+ function detectAzure(baseUrl) {
205
+ try {
206
+ return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+ function azureStyleOf(baseUrl) {
212
+ try {
213
+ const u = new URL(baseUrl);
214
+ if (/\/openai\/deployments\//.test(u.pathname)) return "legacy";
215
+ return "v1";
216
+ } catch {
217
+ return "v1";
218
+ }
219
+ }
220
+ function legacyAzureUrl(baseUrl, path, apiVersion) {
221
+ const url = new URL(baseUrl);
222
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
223
+ url.searchParams.set("api-version", apiVersion);
224
+ return url.toString();
225
+ }
226
+ function v1AzureUrl(baseUrl, path) {
227
+ const url = new URL(baseUrl);
228
+ const route = path.startsWith("/") ? path.slice(1) : path;
229
+ url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
230
+ return url.toString();
231
+ }
232
+ function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
233
+ if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
234
+ return `${baseUrl}/v1/chat/completions`;
235
+ }
236
+ function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
237
+ if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
238
+ return `${baseUrl}/v1/embeddings`;
239
+ }
240
+ function buildAuthHeaders(apiKey, isAzure) {
241
+ if (isAzure) return {
242
+ "Content-Type": "application/json",
243
+ "api-key": apiKey
244
+ };
245
+ return {
246
+ "Content-Type": "application/json",
247
+ Authorization: `Bearer ${apiKey}`
248
+ };
249
+ }
250
+ function normalizeBaseUrl(raw) {
251
+ return (raw || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
252
+ }
253
+
187
254
  //#endregion
188
255
  //#region src/providers/openai.ts
189
- const DEFAULT_BASE_URL$1 = "https://api.openai.com";
190
256
  const DEFAULT_TIMEOUT_MS = 6e4;
191
- const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
192
257
  /**
193
258
  * OpenAI-compatible LLM provider.
194
259
  *
@@ -208,7 +273,12 @@ const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
208
273
  * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
209
274
  * OPENAI_MODEL — model name (default: gpt-4o-mini)
210
275
  * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
211
- * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (default: 60000)
276
+ * OPENAI_TIMEOUT_MS — outbound fetch timeout in ms (OpenAI-scoped alias,
277
+ * takes precedence over AGENTMEMORY_LLM_TIMEOUT_MS
278
+ * for back-compat with the v0.9.17 shipping name).
279
+ * AGENTMEMORY_LLM_TIMEOUT_MS — outbound fetch timeout in ms shared across all
280
+ * raw-fetch LLM + embedding providers. Used when
281
+ * OPENAI_TIMEOUT_MS is not set. Default: 60000.
212
282
  * MAX_TOKENS — max output tokens (default: from config or 4096)
213
283
  * OPENAI_REASONING_EFFORT — "low" | "medium" | "high" | "none"
214
284
  * Passthrough for reasoning models (e.g. Ollama Cloud
@@ -230,9 +300,9 @@ var OpenAIProvider = class {
230
300
  this.apiKey = apiKey;
231
301
  this.model = model;
232
302
  this.maxTokens = maxTokens;
233
- this.baseUrl = (baseURL || getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL$1).replace(/\/+$/, "");
303
+ this.baseUrl = normalizeBaseUrl(baseURL || getEnvVar("OPENAI_BASE_URL"));
234
304
  this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
235
- this.timeoutMs = parseTimeout(getEnvVar("OPENAI_TIMEOUT_MS"));
305
+ this.timeoutMs = resolveTimeout();
236
306
  this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
237
307
  this.isAzure = detectAzure(this.baseUrl);
238
308
  }
@@ -242,25 +312,8 @@ var OpenAIProvider = class {
242
312
  async summarize(systemPrompt, userPrompt) {
243
313
  return this.call(systemPrompt, userPrompt);
244
314
  }
245
- buildUrl() {
246
- if (this.isAzure) {
247
- const sep = this.baseUrl.includes("?") ? "&" : "?";
248
- return `${this.baseUrl}/chat/completions${sep}api-version=${encodeURIComponent(this.azureApiVersion)}`;
249
- }
250
- return `${this.baseUrl}/v1/chat/completions`;
251
- }
252
- buildHeaders() {
253
- if (this.isAzure) return {
254
- "Content-Type": "application/json",
255
- "api-key": this.apiKey
256
- };
257
- return {
258
- "Content-Type": "application/json",
259
- Authorization: `Bearer ${this.apiKey}`
260
- };
261
- }
262
315
  async call(systemPrompt, userPrompt) {
263
- const url = this.buildUrl();
316
+ const url = buildChatUrl(this.baseUrl, this.isAzure, this.azureApiVersion);
264
317
  const body = {
265
318
  model: this.model,
266
319
  max_tokens: this.maxTokens,
@@ -273,21 +326,16 @@ var OpenAIProvider = class {
273
326
  }]
274
327
  };
275
328
  if (this.reasoningEffort) body.reasoning_effort = this.reasoningEffort;
276
- const ac = new AbortController();
277
- const t = setTimeout(() => ac.abort(), this.timeoutMs);
278
329
  let response;
279
330
  try {
280
- response = await fetch(url, {
331
+ response = await fetchWithTimeout(url, {
281
332
  method: "POST",
282
- headers: this.buildHeaders(),
283
- body: JSON.stringify(body),
284
- signal: ac.signal
285
- });
333
+ headers: buildAuthHeaders(this.apiKey, this.isAzure),
334
+ body: JSON.stringify(body)
335
+ }, this.timeoutMs);
286
336
  } catch (err) {
287
- 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.`);
337
+ 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.`);
288
338
  throw err;
289
- } finally {
290
- clearTimeout(t);
291
339
  }
292
340
  if (!response.ok) {
293
341
  const text = await response.text();
@@ -302,17 +350,19 @@ var OpenAIProvider = class {
302
350
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
303
351
  }
304
352
  };
305
- function parseTimeout(raw) {
306
- if (!raw) return DEFAULT_TIMEOUT_MS;
307
- const n = parseInt(raw, 10);
308
- return Number.isFinite(n) && n > 0 ? n : DEFAULT_TIMEOUT_MS;
309
- }
310
- function detectAzure(baseUrl) {
311
- try {
312
- return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
313
- } catch {
314
- return false;
315
- }
353
+ function resolveTimeout() {
354
+ const openai = parsePositiveInt(getEnvVar("OPENAI_TIMEOUT_MS"));
355
+ if (openai !== void 0) return openai;
356
+ const globalMs = parsePositiveInt(getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS"));
357
+ if (globalMs !== void 0) return globalMs;
358
+ return DEFAULT_TIMEOUT_MS;
359
+ }
360
+ function parsePositiveInt(raw) {
361
+ if (!raw) return void 0;
362
+ const trimmed = raw.trim();
363
+ if (!/^\d+$/.test(trimmed)) return void 0;
364
+ const n = Number(trimmed);
365
+ return Number.isFinite(n) && n > 0 ? n : void 0;
316
366
  }
317
367
 
318
368
  //#endregion
@@ -337,7 +387,7 @@ var OpenRouterProvider = class {
337
387
  return this.call(systemPrompt, userPrompt);
338
388
  }
339
389
  async call(systemPrompt, userPrompt) {
340
- const response = await fetch(this.baseUrl, {
390
+ const response = await fetchWithTimeout(this.baseUrl, {
341
391
  method: "POST",
342
392
  headers: {
343
393
  "Content-Type": "application/json",
@@ -506,7 +556,7 @@ var GeminiEmbeddingProvider = class {
506
556
  const results = [];
507
557
  for (let i = 0; i < texts.length; i += BATCH_LIMIT) {
508
558
  const chunk = texts.slice(i, i + BATCH_LIMIT);
509
- const response = await fetch(`${API_BASE}?key=${this.apiKey}`, {
559
+ const response = await fetchWithTimeout(`${API_BASE}?key=${this.apiKey}`, {
510
560
  method: "POST",
511
561
  headers: { "Content-Type": "application/json" },
512
562
  body: JSON.stringify({ requests: chunk.map((t) => ({
@@ -543,7 +593,6 @@ function l2Normalize(vec) {
543
593
 
544
594
  //#endregion
545
595
  //#region src/providers/embedding/openai.ts
546
- const DEFAULT_BASE_URL = "https://api.openai.com";
547
596
  const DEFAULT_MODEL$1 = "text-embedding-3-small";
548
597
  /**
549
598
  * Known OpenAI embedding model dimensions. Extend as new models ship.
@@ -567,11 +616,20 @@ function resolveDimensions(model, override) {
567
616
  /**
568
617
  * OpenAI-compatible embedding provider.
569
618
  *
619
+ * Shares transport (URL builder, auth header, Azure detection) with
620
+ * the OpenAI LLM provider via `_openai-shared` (#371). Same env knobs
621
+ * pick up automatically: when `OPENAI_BASE_URL` points at an Azure
622
+ * resource (`.openai.azure.com` hostname) the embedding request uses
623
+ * Azure's `/embeddings` path with the `api-version` query param and
624
+ * `api-key` header instead of `Authorization: Bearer`.
625
+ *
570
626
  * Required env vars:
571
627
  * OPENAI_API_KEY — API key
572
628
  *
573
629
  * Optional:
574
- * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com)
630
+ * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
631
+ * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
632
+ * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
575
633
  * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
576
634
  * OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
577
635
  * custom / self-hosted models not in the
@@ -583,25 +641,25 @@ var OpenAIEmbeddingProvider = class {
583
641
  apiKey;
584
642
  baseUrl;
585
643
  model;
644
+ isAzure;
645
+ azureApiVersion;
586
646
  constructor(apiKey) {
587
647
  this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
588
648
  if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
589
- this.baseUrl = getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL;
649
+ this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
590
650
  this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
591
651
  this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
652
+ this.isAzure = detectAzure(this.baseUrl);
653
+ this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
592
654
  }
593
655
  async embed(text) {
594
656
  const [result] = await this.embedBatch([text]);
595
657
  return result;
596
658
  }
597
659
  async embedBatch(texts) {
598
- const url = `${this.baseUrl}/v1/embeddings`;
599
- const response = await fetch(url, {
660
+ const response = await fetchWithTimeout(buildEmbeddingUrl(this.baseUrl, this.isAzure, this.azureApiVersion), {
600
661
  method: "POST",
601
- headers: {
602
- Authorization: `Bearer ${this.apiKey}`,
603
- "Content-Type": "application/json"
604
- },
662
+ headers: buildAuthHeaders(this.apiKey, this.isAzure),
605
663
  body: JSON.stringify({
606
664
  model: this.model,
607
665
  input: texts
@@ -631,7 +689,7 @@ var VoyageEmbeddingProvider = class {
631
689
  return result;
632
690
  }
633
691
  async embedBatch(texts) {
634
- const response = await fetch(API_URL$2, {
692
+ const response = await fetchWithTimeout(API_URL$2, {
635
693
  method: "POST",
636
694
  headers: {
637
695
  Authorization: `Bearer ${this.apiKey}`,
@@ -667,7 +725,7 @@ var CohereEmbeddingProvider = class {
667
725
  return result;
668
726
  }
669
727
  async embedBatch(texts) {
670
- const response = await fetch(API_URL$1, {
728
+ const response = await fetchWithTimeout(API_URL$1, {
671
729
  method: "POST",
672
730
  headers: {
673
731
  Authorization: `Bearer ${this.apiKey}`,
@@ -705,7 +763,7 @@ var OpenRouterEmbeddingProvider = class {
705
763
  return result;
706
764
  }
707
765
  async embedBatch(texts) {
708
- const response = await fetch(API_URL, {
766
+ const response = await fetchWithTimeout(API_URL, {
709
767
  method: "POST",
710
768
  headers: {
711
769
  Authorization: `Bearer ${this.apiKey}`,
@@ -1133,7 +1191,7 @@ var GraphRetrieval = class {
1133
1191
  const results = [];
1134
1192
  const visitedObs = /* @__PURE__ */ new Set();
1135
1193
  for (const startNode of matchingNodes) {
1136
- const paths = this.bfsTraversal(startNode, allNodes, allEdges, maxDepth);
1194
+ const paths = this.dijkstraTraversal(startNode, allNodes, allEdges, maxDepth);
1137
1195
  for (const path of paths) {
1138
1196
  const lastNode = path[path.length - 1].node;
1139
1197
  for (const obsId of lastNode.sourceObservationIds) {
@@ -1173,7 +1231,7 @@ var GraphRetrieval = class {
1173
1231
  const results = [];
1174
1232
  const visitedObs = new Set(obsIds);
1175
1233
  for (const node of linkedNodes) {
1176
- const paths = this.bfsTraversal(node, allNodes, allEdges, maxDepth);
1234
+ const paths = this.dijkstraTraversal(node, allNodes, allEdges, maxDepth);
1177
1235
  for (const path of paths) {
1178
1236
  const lastNode = path[path.length - 1].node;
1179
1237
  for (const obsId of lastNode.sourceObservationIds) {
@@ -1245,37 +1303,104 @@ var GraphRetrieval = class {
1245
1303
  }
1246
1304
  return latest;
1247
1305
  }
1248
- bfsTraversal(startNode, allNodes, allEdges, maxDepth) {
1249
- const paths = [];
1250
- const visited = /* @__PURE__ */ new Set();
1251
- const queue = [{
1306
+ dijkstraTraversal(startNode, allNodes, allEdges, maxDepth) {
1307
+ const nodeIndex = /* @__PURE__ */ new Map();
1308
+ for (const n of allNodes) nodeIndex.set(n.id, n);
1309
+ const adjacency = /* @__PURE__ */ new Map();
1310
+ for (const edge of allEdges) {
1311
+ const a = edge.sourceNodeId;
1312
+ const b = edge.targetNodeId;
1313
+ if (!adjacency.has(a)) adjacency.set(a, []);
1314
+ if (!adjacency.has(b)) adjacency.set(b, []);
1315
+ adjacency.get(a).push({
1316
+ neighborId: b,
1317
+ edge
1318
+ });
1319
+ adjacency.get(b).push({
1320
+ neighborId: a,
1321
+ edge
1322
+ });
1323
+ }
1324
+ const dist = /* @__PURE__ */ new Map();
1325
+ const pathTo = /* @__PURE__ */ new Map();
1326
+ dist.set(startNode.id, 0);
1327
+ pathTo.set(startNode.id, [{ node: startNode }]);
1328
+ const heap = new MinHeap((a, b) => a.cost - b.cost);
1329
+ heap.push({
1252
1330
  nodeId: startNode.id,
1253
1331
  depth: 0,
1254
- path: [{ node: startNode }]
1255
- }];
1256
- visited.add(startNode.id);
1257
- while (queue.length > 0) {
1258
- const { nodeId, depth, path } = queue.shift();
1259
- paths.push(path);
1332
+ cost: 0
1333
+ });
1334
+ while (heap.size() > 0) {
1335
+ const { nodeId, depth, cost } = heap.pop();
1336
+ if (cost > (dist.get(nodeId) ?? Infinity)) continue;
1260
1337
  if (depth >= maxDepth) continue;
1261
- const neighborEdges = allEdges.filter((e) => e.sourceNodeId === nodeId || e.targetNodeId === nodeId);
1262
- for (const edge of neighborEdges) {
1263
- const nextId = edge.sourceNodeId === nodeId ? edge.targetNodeId : edge.sourceNodeId;
1264
- if (visited.has(nextId)) continue;
1265
- visited.add(nextId);
1266
- const nextNode = allNodes.find((n) => n.id === nextId);
1338
+ const neighbors = adjacency.get(nodeId) ?? [];
1339
+ for (const { neighborId, edge } of neighbors) {
1340
+ const nextNode = nodeIndex.get(neighborId);
1267
1341
  if (!nextNode) continue;
1268
- queue.push({
1269
- nodeId: nextId,
1270
- depth: depth + 1,
1271
- path: [...path, {
1342
+ const newCost = cost + 1 / Math.max(edge.weight, .01);
1343
+ if (newCost < (dist.get(neighborId) ?? Infinity)) {
1344
+ dist.set(neighborId, newCost);
1345
+ pathTo.set(neighborId, [...pathTo.get(nodeId), {
1272
1346
  node: nextNode,
1273
1347
  edge
1274
- }]
1275
- });
1348
+ }]);
1349
+ heap.push({
1350
+ nodeId: neighborId,
1351
+ depth: depth + 1,
1352
+ cost: newCost
1353
+ });
1354
+ }
1276
1355
  }
1277
1356
  }
1278
- return paths;
1357
+ pathTo.delete(startNode.id);
1358
+ return Array.from(pathTo.values());
1359
+ }
1360
+ };
1361
+ var MinHeap = class {
1362
+ heap = [];
1363
+ constructor(compare) {
1364
+ this.compare = compare;
1365
+ }
1366
+ size() {
1367
+ return this.heap.length;
1368
+ }
1369
+ push(value) {
1370
+ this.heap.push(value);
1371
+ this.bubbleUp(this.heap.length - 1);
1372
+ }
1373
+ pop() {
1374
+ if (this.heap.length === 0) return void 0;
1375
+ const top = this.heap[0];
1376
+ const last = this.heap.pop();
1377
+ if (this.heap.length > 0) {
1378
+ this.heap[0] = last;
1379
+ this.sinkDown(0);
1380
+ }
1381
+ return top;
1382
+ }
1383
+ bubbleUp(i) {
1384
+ while (i > 0) {
1385
+ const parent = i - 1 >> 1;
1386
+ if (this.compare(this.heap[i], this.heap[parent]) < 0) {
1387
+ [this.heap[i], this.heap[parent]] = [this.heap[parent], this.heap[i]];
1388
+ i = parent;
1389
+ } else break;
1390
+ }
1391
+ }
1392
+ sinkDown(i) {
1393
+ const n = this.heap.length;
1394
+ while (true) {
1395
+ const left = 2 * i + 1;
1396
+ const right = 2 * i + 2;
1397
+ let smallest = i;
1398
+ if (left < n && this.compare(this.heap[left], this.heap[smallest]) < 0) smallest = left;
1399
+ if (right < n && this.compare(this.heap[right], this.heap[smallest]) < 0) smallest = right;
1400
+ if (smallest === i) break;
1401
+ [this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]];
1402
+ i = smallest;
1403
+ }
1279
1404
  }
1280
1405
  };
1281
1406
 
@@ -4095,7 +4220,11 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4095
4220
  sdk.registerFunction("mem::context", async (data) => {
4096
4221
  const budget = data.budget || tokenBudget;
4097
4222
  const blocks = [];
4098
- const [pinnedSlots, profile] = await Promise.all([isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]), kv.get(KV.profiles, data.project).catch(() => null)]);
4223
+ const [pinnedSlots, profile, lessons] = await Promise.all([
4224
+ isSlotsEnabled() ? listPinnedSlots(kv).catch(() => []) : Promise.resolve([]),
4225
+ kv.get(KV.profiles, data.project).catch(() => null),
4226
+ kv.list(KV.lessons).catch(() => [])
4227
+ ]);
4099
4228
  const slotContent = renderPinnedContext(pinnedSlots);
4100
4229
  if (slotContent) blocks.push({
4101
4230
  type: "memory",
@@ -4119,6 +4248,24 @@ function registerContextFunction(sdk, kv, tokenBudget) {
4119
4248
  });
4120
4249
  }
4121
4250
  }
4251
+ const relevantLessons = lessons.filter((l) => !l.deleted && (!l.project || l.project === data.project)).sort((a, b) => {
4252
+ const scoreA = (a.project === data.project ? 1.5 : 1) * a.confidence;
4253
+ return (b.project === data.project ? 1.5 : 1) * b.confidence - scoreA;
4254
+ }).slice(0, 10);
4255
+ if (relevantLessons.length > 0) {
4256
+ const lessonsContent = `## Lessons Learned\n${relevantLessons.map((l) => `- (${l.confidence.toFixed(2)}) ${l.content}${l.context ? ` — ${l.context}` : ""}`).join("\n")}`;
4257
+ const mostRecent = relevantLessons.reduce((acc, l) => {
4258
+ const t = new Date(l.lastReinforcedAt || l.updatedAt).getTime();
4259
+ return t > acc ? t : acc;
4260
+ }, 0);
4261
+ blocks.push({
4262
+ type: "memory",
4263
+ content: lessonsContent,
4264
+ tokens: estimateTokens$1(lessonsContent),
4265
+ recency: mostRecent,
4266
+ sourceIds: relevantLessons.map((l) => l.id)
4267
+ });
4268
+ }
4122
4269
  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);
4123
4270
  const summariesPerSession = await Promise.all(sessions.map((s) => kv.get(KV.summaries, s.id).catch(() => null)));
4124
4271
  const sessionsNeedingObs = [];
@@ -4941,6 +5088,46 @@ const DEFAULTS$1 = {
4941
5088
  lowImportanceThreshold: 3,
4942
5089
  maxObservationsPerProject: 1e4
4943
5090
  };
5091
+ function isValidRecoveryResult(result) {
5092
+ if (!result || typeof result !== "object") return false;
5093
+ if (!("success" in result)) return true;
5094
+ return result.success !== false;
5095
+ }
5096
+ function isCompressedObservation(observation) {
5097
+ return "title" in observation && typeof observation.title === "string" && observation.title.length > 0;
5098
+ }
5099
+ async function recoverStaleSession(sdk, sessionId) {
5100
+ try {
5101
+ const result = await sdk.trigger({
5102
+ function_id: "event::session::stopped",
5103
+ payload: { sessionId }
5104
+ });
5105
+ if (!isValidRecoveryResult(result)) {
5106
+ logger.warn("Stale session recovery failed", {
5107
+ sessionId,
5108
+ result
5109
+ });
5110
+ return false;
5111
+ }
5112
+ return true;
5113
+ } catch (err) {
5114
+ logger.warn("Stale session recovery failed", {
5115
+ sessionId,
5116
+ error: err instanceof Error ? err.message : String(err)
5117
+ });
5118
+ return false;
5119
+ }
5120
+ }
5121
+ async function runRecoveredSessionConsolidation(sdk) {
5122
+ try {
5123
+ await sdk.trigger({
5124
+ function_id: "mem::consolidate-pipeline",
5125
+ payload: { tier: "all" }
5126
+ });
5127
+ } catch (err) {
5128
+ logger.warn("Recovered session consolidation failed", { error: err instanceof Error ? err.message : String(err) });
5129
+ }
5130
+ }
4944
5131
  function registerEvictFunction(sdk, kv) {
4945
5132
  sdk.registerFunction("mem::evict", async (data) => {
4946
5133
  const dryRun = data?.dryRun ?? false;
@@ -4959,6 +5146,7 @@ function registerEvictFunction(sdk, kv) {
4959
5146
  nonLatestMemories: 0,
4960
5147
  dryRun
4961
5148
  };
5149
+ let recoveredStaleSessions = 0;
4962
5150
  const sessions = await kv.list(KV.sessions).catch(() => []);
4963
5151
  const summaries = await kv.list(KV.summaries).catch(() => []);
4964
5152
  const summaryIds = new Set(summaries.map((s) => s.sessionId));
@@ -4966,6 +5154,23 @@ function registerEvictFunction(sdk, kv) {
4966
5154
  if (!session.startedAt) continue;
4967
5155
  if (now - new Date(session.startedAt).getTime() > cfg.staleSessionDays * MS_PER_DAY$1 && !summaryIds.has(session.id)) if (dryRun) stats.staleSessions++;
4968
5156
  else {
5157
+ const observations = await kv.list(KV.observations(session.id)).catch((err) => {
5158
+ logger.warn("Stale session observation scan failed", {
5159
+ sessionId: session.id,
5160
+ error: err instanceof Error ? err.message : String(err)
5161
+ });
5162
+ return null;
5163
+ });
5164
+ if (!observations) continue;
5165
+ let recovered = false;
5166
+ if (observations.some(isCompressedObservation)) {
5167
+ recovered = await recoverStaleSession(sdk, session.id);
5168
+ if (!recovered) continue;
5169
+ recoveredStaleSessions++;
5170
+ } else if (observations.length > 0) {
5171
+ logger.warn("Stale session has no compressed observations", { sessionId: session.id });
5172
+ continue;
5173
+ }
4969
5174
  try {
4970
5175
  await kv.delete(KV.sessions, session.id);
4971
5176
  stats.staleSessions++;
@@ -4979,11 +5184,12 @@ function registerEvictFunction(sdk, kv) {
4979
5184
  }
4980
5185
  await recordAudit(kv, "delete", "mem::evict", [session.id], {
4981
5186
  resource: "session",
4982
- reason: "stale_session_without_summary",
5187
+ reason: recovered ? "stale_session_recovered_then_evicted" : "stale_session_without_summary",
4983
5188
  dryRun
4984
5189
  });
4985
5190
  }
4986
5191
  }
5192
+ if (!dryRun && recoveredStaleSessions > 0) await runRecoveredSessionConsolidation(sdk);
4987
5193
  const projectObs = /* @__PURE__ */ new Map();
4988
5194
  for (const session of sessions) {
4989
5195
  const compressed = (await kv.list(KV.observations(session.id)).catch(() => [])).filter((o) => o.title);
@@ -5793,7 +5999,9 @@ function registerExportImportFunction(sdk, kv) {
5793
5999
  "0.9.14",
5794
6000
  "0.9.15",
5795
6001
  "0.9.16",
5796
- "0.9.17"
6002
+ "0.9.17",
6003
+ "0.9.18",
6004
+ "0.9.19"
5797
6005
  ]).has(importData.version)) return {
5798
6006
  success: false,
5799
6007
  error: `Unsupported export version: ${importData.version}`
@@ -13621,6 +13829,112 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13621
13829
  middleware_function_ids: ["middleware::api-auth"]
13622
13830
  }
13623
13831
  });
13832
+ sdk.registerFunction("api::session::commit", async (req) => {
13833
+ const body = req.body ?? {};
13834
+ const sha = asNonEmptyString$1(body.sha);
13835
+ if (!sha) return {
13836
+ status_code: 400,
13837
+ body: { error: "sha is required and must be a non-empty string" }
13838
+ };
13839
+ const sessionId = asNonEmptyString$1(body.sessionId) ?? void 0;
13840
+ const branch = asNonEmptyString$1(body.branch) ?? void 0;
13841
+ const repo = asNonEmptyString$1(body.repo) ?? void 0;
13842
+ const message = asNonEmptyString$1(body.message) ?? void 0;
13843
+ const author = asNonEmptyString$1(body.author) ?? void 0;
13844
+ const authoredAt = asNonEmptyString$1(body.authoredAt) ?? void 0;
13845
+ const files = Array.isArray(body.files) ? body.files.filter((f) => typeof f === "string" && f.length > 0) : void 0;
13846
+ const link = await withKeyedLock(`commit:${sha}`, async () => {
13847
+ const existing = await kv.get(KV.commits, sha);
13848
+ const sessionSet = new Set(existing?.sessionIds ?? []);
13849
+ if (sessionId) sessionSet.add(sessionId);
13850
+ const merged = {
13851
+ sha,
13852
+ shortSha: existing?.shortSha ?? sha.slice(0, 7),
13853
+ branch: branch ?? existing?.branch,
13854
+ repo: repo ?? existing?.repo,
13855
+ message: message ?? existing?.message,
13856
+ author: author ?? existing?.author,
13857
+ authoredAt: authoredAt ?? existing?.authoredAt,
13858
+ files: files ?? existing?.files,
13859
+ sessionIds: Array.from(sessionSet),
13860
+ linkedAt: existing?.linkedAt ?? (/* @__PURE__ */ new Date()).toISOString()
13861
+ };
13862
+ await kv.set(KV.commits, sha, merged);
13863
+ return merged;
13864
+ });
13865
+ if (sessionId) await withKeyedLock(`session:${sessionId}`, async () => {
13866
+ const session = await kv.get(KV.sessions, sessionId);
13867
+ if (!session) return;
13868
+ const shaSet = new Set(session.commitShas ?? []);
13869
+ shaSet.add(sha);
13870
+ session.commitShas = Array.from(shaSet);
13871
+ await kv.set(KV.sessions, sessionId, session);
13872
+ });
13873
+ return {
13874
+ status_code: 200,
13875
+ body: { commit: link }
13876
+ };
13877
+ });
13878
+ sdk.registerTrigger({
13879
+ type: "http",
13880
+ function_id: "api::session::commit",
13881
+ config: {
13882
+ api_path: "/agentmemory/session/commit",
13883
+ http_method: "POST",
13884
+ middleware_function_ids: ["middleware::api-auth"]
13885
+ }
13886
+ });
13887
+ sdk.registerFunction("api::session::by-commit", async (req) => {
13888
+ const authErr = checkAuth(req, secret);
13889
+ if (authErr) return authErr;
13890
+ const sha = asNonEmptyString$1(req.query_params?.["sha"]);
13891
+ if (!sha) return {
13892
+ status_code: 400,
13893
+ body: { error: "sha is required and must be a non-empty string" }
13894
+ };
13895
+ const link = await kv.get(KV.commits, sha);
13896
+ if (!link) return {
13897
+ status_code: 404,
13898
+ body: { error: "no sessions linked to this commit" }
13899
+ };
13900
+ return {
13901
+ status_code: 200,
13902
+ body: {
13903
+ commit: link,
13904
+ sessions: (await Promise.all((link.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null)
13905
+ }
13906
+ };
13907
+ });
13908
+ sdk.registerTrigger({
13909
+ type: "http",
13910
+ function_id: "api::session::by-commit",
13911
+ config: {
13912
+ api_path: "/agentmemory/session/by-commit",
13913
+ http_method: "GET",
13914
+ middleware_function_ids: ["middleware::api-auth"]
13915
+ }
13916
+ });
13917
+ sdk.registerFunction("api::commits", async (req) => {
13918
+ const authErr = checkAuth(req, secret);
13919
+ if (authErr) return authErr;
13920
+ const branch = asNonEmptyString$1(req.query_params?.["branch"]);
13921
+ const repo = asNonEmptyString$1(req.query_params?.["repo"]);
13922
+ const rawLimit = parseOptionalInt(req.query_params?.["limit"]);
13923
+ const limit = Math.max(1, Math.min(500, rawLimit ?? 100));
13924
+ return {
13925
+ status_code: 200,
13926
+ 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) }
13927
+ };
13928
+ });
13929
+ sdk.registerTrigger({
13930
+ type: "http",
13931
+ function_id: "api::commits",
13932
+ config: {
13933
+ api_path: "/agentmemory/commits",
13934
+ http_method: "GET",
13935
+ middleware_function_ids: ["middleware::api-auth"]
13936
+ }
13937
+ });
13624
13938
  sdk.registerFunction("api::sessions", async (req) => {
13625
13939
  const authErr = checkAuth(req, secret);
13626
13940
  if (authErr) return authErr;
@@ -17602,6 +17916,49 @@ function registerMcpEndpoints(sdk, kv, secret) {
17602
17916
  }] }
17603
17917
  };
17604
17918
  }
17919
+ case "memory_commit_lookup": {
17920
+ const sha = asNonEmptyString(args.sha);
17921
+ if (!sha) return {
17922
+ status_code: 400,
17923
+ body: { error: "sha required" }
17924
+ };
17925
+ const link = await kv.get(KV.commits, sha);
17926
+ if (!link) return {
17927
+ status_code: 200,
17928
+ body: { content: [{
17929
+ type: "text",
17930
+ text: JSON.stringify({
17931
+ commit: null,
17932
+ sessions: []
17933
+ }, null, 2)
17934
+ }] }
17935
+ };
17936
+ const linkRecord = link;
17937
+ const sessions = (await Promise.all((linkRecord.sessionIds ?? []).map((sid) => kv.get(KV.sessions, sid)))).filter((s) => s !== null);
17938
+ return {
17939
+ status_code: 200,
17940
+ body: { content: [{
17941
+ type: "text",
17942
+ text: JSON.stringify({
17943
+ commit: link,
17944
+ sessions
17945
+ }, null, 2)
17946
+ }] }
17947
+ };
17948
+ }
17949
+ case "memory_commits": {
17950
+ const branch = typeof args.branch === "string" ? args.branch : void 0;
17951
+ const repo = typeof args.repo === "string" ? args.repo : void 0;
17952
+ const limit = Math.max(1, Math.min(500, asNumber(args.limit, 100) ?? 100));
17953
+ 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);
17954
+ return {
17955
+ status_code: 200,
17956
+ body: { content: [{
17957
+ type: "text",
17958
+ text: JSON.stringify({ commits: filtered }, null, 2)
17959
+ }] }
17960
+ };
17961
+ }
17605
17962
  default: return {
17606
17963
  status_code: 400,
17607
17964
  body: { error: `Unknown tool: ${name}` }
@@ -17987,7 +18344,38 @@ function registerMcpEndpoints(sdk, kv, secret) {
17987
18344
 
17988
18345
  //#endregion
17989
18346
  //#region src/viewer/server.ts
18347
+ function loadViewerFavicon() {
18348
+ const base = dirname(fileURLToPath(import.meta.url));
18349
+ const candidates = [
18350
+ join(base, "..", "src", "viewer", "favicon.svg"),
18351
+ join(base, "..", "viewer", "favicon.svg"),
18352
+ join(base, "viewer", "favicon.svg")
18353
+ ];
18354
+ for (const path of candidates) try {
18355
+ return readFileSync(path);
18356
+ } catch {}
18357
+ return null;
18358
+ }
17990
18359
  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());
18360
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
18361
+ function buildAllowedHosts(origins, listenPort) {
18362
+ const hosts = /* @__PURE__ */ new Set();
18363
+ for (const o of origins) try {
18364
+ const parsed = new URL(o);
18365
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
18366
+ } catch {}
18367
+ hosts.add(`localhost:${listenPort}`);
18368
+ hosts.add(`127.0.0.1:${listenPort}`);
18369
+ hosts.add(`[::1]:${listenPort}`);
18370
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
18371
+ return hosts;
18372
+ }
18373
+ function isHostAllowed(headerHost, allowed) {
18374
+ if (typeof headerHost !== "string") return false;
18375
+ const lower = headerHost.toLowerCase().trim();
18376
+ if (!lower) return false;
18377
+ return allowed.has(lower);
18378
+ }
17991
18379
  function corsHeaders(req) {
17992
18380
  const origin = req.headers.origin || "";
17993
18381
  const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
@@ -18031,7 +18419,17 @@ const MAX_VIEWER_PORT_RETRIES = 10;
18031
18419
  function startViewerServer(port, _kv, _sdk, secret, restPort) {
18032
18420
  const resolvedRestPort = restPort ?? port - 2;
18033
18421
  const requestedPort = port;
18422
+ let allowedHosts = null;
18034
18423
  const server = createServer(async (req, res) => {
18424
+ if (!allowedHosts) {
18425
+ const addr = server.address();
18426
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
18427
+ }
18428
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
18429
+ res.writeHead(403, { "Content-Type": "text/plain" });
18430
+ res.end("forbidden host");
18431
+ return;
18432
+ }
18035
18433
  const raw = req.url || "/";
18036
18434
  const qIdx = raw.indexOf("?");
18037
18435
  const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
@@ -18060,6 +18458,20 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
18060
18458
  res.end("viewer not found");
18061
18459
  return;
18062
18460
  }
18461
+ if (method === "GET" && pathname === "/favicon.svg") {
18462
+ const favicon = loadViewerFavicon();
18463
+ if (favicon) {
18464
+ res.writeHead(200, {
18465
+ "Content-Type": "image/svg+xml",
18466
+ "Cache-Control": "public, max-age=3600"
18467
+ });
18468
+ res.end(favicon);
18469
+ return;
18470
+ }
18471
+ res.writeHead(404, { "Content-Type": "text/plain" });
18472
+ res.end("favicon not found");
18473
+ return;
18474
+ }
18063
18475
  try {
18064
18476
  await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
18065
18477
  } catch (err) {
@@ -18459,7 +18871,7 @@ async function main() {
18459
18871
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
18460
18872
  }
18461
18873
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
18462
- bootLog(`REST API: 121 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
18874
+ bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
18463
18875
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
18464
18876
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
18465
18877
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
@@ -18527,4 +18939,4 @@ main().catch((err) => {
18527
18939
 
18528
18940
  //#endregion
18529
18941
  export { };
18530
- //# sourceMappingURL=src-TiNuQ3Ub.mjs.map
18942
+ //# sourceMappingURL=src-2wwYDPGA.mjs.map