@agentmemory/agentmemory 0.9.21 → 0.9.23

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 (95) hide show
  1. package/AGENTS.md +7 -2
  2. package/README.md +288 -33
  3. package/dist/cli.d.mts +5 -1
  4. package/dist/cli.d.mts.map +1 -0
  5. package/dist/cli.mjs +128 -703
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/connect-Cf9bmBqO.mjs +1020 -0
  8. package/dist/connect-Cf9bmBqO.mjs.map +1 -0
  9. package/dist/hooks/notification.mjs +46 -21
  10. package/dist/hooks/notification.mjs.map +1 -1
  11. package/dist/hooks/post-tool-failure.mjs +47 -21
  12. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  13. package/dist/hooks/post-tool-use.mjs +57 -22
  14. package/dist/hooks/post-tool-use.mjs.map +1 -1
  15. package/dist/hooks/pre-compact.mjs +26 -2
  16. package/dist/hooks/pre-compact.mjs.map +1 -1
  17. package/dist/hooks/pre-tool-use.mjs +19 -12
  18. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  19. package/dist/hooks/prompt-submit.mjs +39 -16
  20. package/dist/hooks/prompt-submit.mjs.map +1 -1
  21. package/dist/hooks/session-end.mjs +26 -33
  22. package/dist/hooks/session-end.mjs.map +1 -1
  23. package/dist/hooks/session-start.mjs +28 -3
  24. package/dist/hooks/session-start.mjs.map +1 -1
  25. package/dist/hooks/stop.mjs +14 -9
  26. package/dist/hooks/stop.mjs.map +1 -1
  27. package/dist/hooks/subagent-start.mjs +31 -4
  28. package/dist/hooks/subagent-start.mjs.map +1 -1
  29. package/dist/hooks/subagent-stop.mjs +45 -20
  30. package/dist/hooks/subagent-stop.mjs.map +1 -1
  31. package/dist/hooks/task-completed.mjs +44 -21
  32. package/dist/hooks/task-completed.mjs.map +1 -1
  33. package/dist/iii-config.docker.yaml +3 -2
  34. package/dist/iii-config.yaml +11 -2
  35. package/dist/{image-refs-R3tin9MR.mjs → image-refs-CJS5B9Gq.mjs} +2 -2
  36. package/dist/{image-refs-R3tin9MR.mjs.map → image-refs-CJS5B9Gq.mjs.map} +1 -1
  37. package/dist/{image-store-DyrKZKqZ.mjs → image-store-CdE0amb1.mjs} +1 -1
  38. package/dist/index.mjs +866 -380
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/logger-xlVlvCWX.mjs +43 -0
  41. package/dist/logger-xlVlvCWX.mjs.map +1 -0
  42. package/dist/schema-BkALl7Z_.mjs +74 -0
  43. package/dist/schema-BkALl7Z_.mjs.map +1 -0
  44. package/dist/{src-D5arboxc.mjs → src-DvS3bhMe.mjs} +844 -395
  45. package/dist/src-DvS3bhMe.mjs.map +1 -0
  46. package/dist/{standalone-C7BgzzIN.mjs → standalone-DHQcPX_g.mjs} +107 -14
  47. package/dist/standalone-DHQcPX_g.mjs.map +1 -0
  48. package/dist/standalone.d.mts.map +1 -1
  49. package/dist/standalone.mjs +108 -12
  50. package/dist/standalone.mjs.map +1 -1
  51. package/dist/{tools-registry-CRTWUFw9.mjs → tools-registry-DJizX9Az.mjs} +51 -12
  52. package/dist/tools-registry-DJizX9Az.mjs.map +1 -0
  53. package/dist/version-BPfyI4Kc.mjs +6 -0
  54. package/dist/version-BPfyI4Kc.mjs.map +1 -0
  55. package/dist/viewer/index.html +85 -10
  56. package/iii-config.docker.yaml +3 -2
  57. package/iii-config.yaml +11 -2
  58. package/package.json +6 -4
  59. package/plugin/.claude-plugin/plugin.json +2 -2
  60. package/plugin/.codex-plugin/plugin.json +2 -2
  61. package/plugin/.mcp.copilot.json +15 -0
  62. package/plugin/.mcp.json +3 -2
  63. package/plugin/hooks/hooks.copilot.json +72 -0
  64. package/plugin/opencode/agentmemory-capture.ts +34 -9
  65. package/plugin/plugin.json +15 -0
  66. package/plugin/scripts/diagnostics.d.mts +17 -0
  67. package/plugin/scripts/diagnostics.d.mts.map +1 -0
  68. package/plugin/scripts/diagnostics.mjs.map +1 -0
  69. package/plugin/scripts/notification.mjs +46 -21
  70. package/plugin/scripts/notification.mjs.map +1 -1
  71. package/plugin/scripts/post-tool-failure.mjs +47 -21
  72. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  73. package/plugin/scripts/post-tool-use.mjs +57 -22
  74. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  75. package/plugin/scripts/pre-compact.mjs +26 -2
  76. package/plugin/scripts/pre-compact.mjs.map +1 -1
  77. package/plugin/scripts/pre-tool-use.mjs +19 -12
  78. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  79. package/plugin/scripts/prompt-submit.mjs +39 -16
  80. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  81. package/plugin/scripts/session-end.mjs +26 -33
  82. package/plugin/scripts/session-end.mjs.map +1 -1
  83. package/plugin/scripts/session-start.mjs +28 -3
  84. package/plugin/scripts/session-start.mjs.map +1 -1
  85. package/plugin/scripts/stop.mjs +14 -9
  86. package/plugin/scripts/stop.mjs.map +1 -1
  87. package/plugin/scripts/subagent-start.mjs +31 -4
  88. package/plugin/scripts/subagent-start.mjs.map +1 -1
  89. package/plugin/scripts/subagent-stop.mjs +45 -20
  90. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  91. package/plugin/scripts/task-completed.mjs +44 -21
  92. package/plugin/scripts/task-completed.mjs.map +1 -1
  93. package/dist/src-D5arboxc.mjs.map +0 -1
  94. package/dist/standalone-C7BgzzIN.mjs.map +0 -1
  95. package/dist/tools-registry-CRTWUFw9.mjs.map +0 -1
@@ -1,9 +1,11 @@
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
- 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 loadSnapshotConfig, a as getConsolidationDecayDays, c as isAutoCompressEnabled, d as isDropStaleIndexEnabled, f as isGraphExtractionEnabled, g as loadFallbackConfig, h as loadEmbeddingConfig, i as detectLlmProviderKind, l as isConsolidationEnabled, m as loadConfig, n as getVisibleTools, o as getEnvVar, p as loadClaudeBridgeConfig, r as detectEmbeddingProvider, t as getAllTools, u as isContextInjectionEnabled, v as loadTeamConfig } from "./tools-registry-CRTWUFw9.mjs";
1
+ import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./schema-BkALl7Z_.mjs";
2
+ import { n as logger, t as bootLog } from "./logger-xlVlvCWX.mjs";
3
+ import { t as VERSION } from "./version-BPfyI4Kc.mjs";
4
+ import { a as isManagedImagePath, getImageRefCount, i as getMaxBytes, n as IMAGES_DIR, r as deleteImage, t as withKeyedLock } from "./image-refs-CJS5B9Gq.mjs";
5
+ import { _ as loadEmbeddingConfig, a as getAgentId, b as loadTeamConfig, d as isConsolidationEnabled, f as isContextInjectionEnabled, g as loadConfig, h as loadClaudeBridgeConfig, i as detectLlmProviderKind, l as isAgentScopeIsolated, m as isGraphExtractionEnabled, n as getVisibleTools, o as getConsolidationDecayDays, p as isDropStaleIndexEnabled, r as detectEmbeddingProvider, s as getEnvVar, t as getAllTools, u as isAutoCompressEnabled, v as loadFallbackConfig, y as loadSnapshotConfig } from "./tools-registry-DJizX9Az.mjs";
4
6
  import { createRequire } from "node:module";
5
7
  import { execFile } from "node:child_process";
6
- import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
7
9
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
8
10
  import { fileURLToPath } from "node:url";
9
11
  import { homedir } from "node:os";
@@ -229,13 +231,25 @@ function v1AzureUrl(baseUrl, path) {
229
231
  url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
230
232
  return url.toString();
231
233
  }
234
+ function appendOpenAIRoute(baseUrl, route) {
235
+ const trimmedBase = baseUrl.replace(/\/+$/, "");
236
+ const cleanRoute = route.startsWith("/") ? route : `/${route}`;
237
+ let pathname;
238
+ try {
239
+ pathname = new URL(trimmedBase).pathname.replace(/\/+$/, "");
240
+ } catch {
241
+ return `${trimmedBase}/v1${cleanRoute}`;
242
+ }
243
+ if (pathname === "" || pathname === "/") return `${trimmedBase}/v1${cleanRoute}`;
244
+ return `${trimmedBase}${cleanRoute}`;
245
+ }
232
246
  function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
233
247
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
234
- return `${baseUrl}/v1/chat/completions`;
248
+ return appendOpenAIRoute(baseUrl, "/chat/completions");
235
249
  }
236
250
  function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
237
251
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
238
- return `${baseUrl}/v1/embeddings`;
252
+ return appendOpenAIRoute(baseUrl, "/embeddings");
239
253
  }
240
254
  function buildAuthHeaders(apiKey, isAzure) {
241
255
  if (isAzure) return {
@@ -317,6 +331,7 @@ var OpenAIProvider = class {
317
331
  const body = {
318
332
  model: this.model,
319
333
  max_tokens: this.maxTokens,
334
+ stream: false,
320
335
  messages: [{
321
336
  role: "system",
322
337
  content: systemPrompt
@@ -345,7 +360,7 @@ var OpenAIProvider = class {
345
360
  const message = data.choices?.[0]?.message;
346
361
  const content = message?.content;
347
362
  if (content) return content;
348
- const reasoning = message?.reasoning;
363
+ const reasoning = message?.reasoning ?? message?.reasoning_content;
349
364
  if (reasoning) return reasoning;
350
365
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
351
366
  }
@@ -624,16 +639,30 @@ function resolveDimensions(model, override) {
624
639
  * `api-key` header instead of `Authorization: Bearer`.
625
640
  *
626
641
  * Required env vars:
627
- * OPENAI_API_KEY — API key
642
+ * OPENAI_API_KEY — API key (fallback for OPENAI_EMBEDDING_API_KEY)
628
643
  *
629
644
  * Optional:
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)
633
- * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
634
- * OPENAI_EMBEDDING_DIMENSIONS override reported dimensions (required for
635
- * custom / self-hosted models not in the
636
- * MODEL_DIMENSIONS table above)
645
+ * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
646
+ * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
647
+ * OPENAI_EMBEDDING_BASE_URL embedding-specific base URL override (defaults
648
+ * to OPENAI_BASE_URL). Lets operators run
649
+ * embeddings on a separate endpoint from chat —
650
+ * e.g. local Ollama / LM Studio / llama.cpp /
651
+ * vLLM at http://localhost:1234 for unlimited
652
+ * free embeddings, while keeping chat
653
+ * completions on a rate-limited but high-quality
654
+ * hosted provider. Azure detection runs on
655
+ * whichever URL ends up selected.
656
+ * OPENAI_EMBEDDING_API_KEY — separate API key for the embedding endpoint
657
+ * (defaults to OPENAI_API_KEY). Useful when the
658
+ * embedding endpoint requires a different key
659
+ * or no key at all (set to e.g. "local" for
660
+ * endpoints that ignore Authorization).
661
+ * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
662
+ * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
663
+ * OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
664
+ * custom / self-hosted models not in the
665
+ * MODEL_DIMENSIONS table above)
637
666
  */
638
667
  var OpenAIEmbeddingProvider = class {
639
668
  name = "openai";
@@ -644,9 +673,9 @@ var OpenAIEmbeddingProvider = class {
644
673
  isAzure;
645
674
  azureApiVersion;
646
675
  constructor(apiKey) {
647
- this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
648
- if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
649
- this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
676
+ this.apiKey = apiKey || getEnvVar("OPENAI_EMBEDDING_API_KEY") || getEnvVar("OPENAI_API_KEY") || "";
677
+ if (!this.apiKey) throw new Error("API key is required (via constructor, OPENAI_EMBEDDING_API_KEY, or OPENAI_API_KEY)");
678
+ this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_EMBEDDING_BASE_URL") || getEnvVar("OPENAI_BASE_URL"));
650
679
  this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
651
680
  this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
652
681
  this.isAzure = detectAzure(this.baseUrl);
@@ -1023,10 +1052,11 @@ var StateKV = class {
1023
1052
  //#endregion
1024
1053
  //#region src/state/vector-index.ts
1025
1054
  function float32ToBase64(arr) {
1026
- return Buffer.from(arr.buffer).toString("base64");
1055
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString("base64");
1027
1056
  }
1028
1057
  function base64ToFloat32(b64) {
1029
- return new Float32Array(Buffer.from(b64, "base64").buffer);
1058
+ const buf = Buffer.from(b64, "base64");
1059
+ return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / Float32Array.BYTES_PER_ELEMENT);
1030
1060
  }
1031
1061
  function cosineSimilarity(a, b) {
1032
1062
  if (a.length !== b.length) return 0;
@@ -1147,7 +1177,7 @@ var VectorIndex = class VectorIndex {
1147
1177
  function memoryToObservation(memory) {
1148
1178
  return {
1149
1179
  id: memory.id,
1150
- sessionId: memory.sessionIds[0] ?? "memory",
1180
+ sessionId: memory.sessionIds?.[0] ?? "memory",
1151
1181
  timestamp: memory.createdAt,
1152
1182
  type: "decision",
1153
1183
  title: memory.title,
@@ -2182,6 +2212,24 @@ var SearchIndex = class SearchIndex {
2182
2212
  has(id) {
2183
2213
  return this.entries.has(id);
2184
2214
  }
2215
+ remove(id) {
2216
+ const entry = this.entries.get(id);
2217
+ if (!entry) return;
2218
+ const termFreq = this.docTermCounts.get(id);
2219
+ if (termFreq) {
2220
+ for (const term of termFreq.keys()) {
2221
+ const postingList = this.invertedIndex.get(term);
2222
+ if (postingList) {
2223
+ postingList.delete(id);
2224
+ if (postingList.size === 0) this.invertedIndex.delete(term);
2225
+ }
2226
+ }
2227
+ this.docTermCounts.delete(id);
2228
+ }
2229
+ this.totalDocLength = Math.max(0, this.totalDocLength - entry.termCount);
2230
+ this.entries.delete(id);
2231
+ this.sortedTerms = null;
2232
+ }
2185
2233
  search(query, limit = 20) {
2186
2234
  const rawTerms = this.tokenize(query.toLowerCase());
2187
2235
  if (rawTerms.length === 0) return [];
@@ -2521,6 +2569,7 @@ function buildSyntheticCompression(raw) {
2521
2569
  };
2522
2570
  if (raw.modality) result.modality = raw.modality;
2523
2571
  if (raw.imageData) result.imageData = raw.imageData;
2572
+ if (raw.agentId) result.agentId = raw.agentId;
2524
2573
  return result;
2525
2574
  }
2526
2575
 
@@ -2610,6 +2659,16 @@ function setVectorIndex(idx) {
2610
2659
  function setEmbeddingProvider(provider) {
2611
2660
  currentEmbeddingProvider = provider;
2612
2661
  }
2662
+ function vectorIndexRemove(id) {
2663
+ vectorIndex?.remove(id);
2664
+ }
2665
+ let indexPersistence = null;
2666
+ function setIndexPersistence(p) {
2667
+ indexPersistence = p;
2668
+ }
2669
+ async function flushIndexSave() {
2670
+ await indexPersistence?.save();
2671
+ }
2613
2672
  const EMBED_MAX_CHARS = 16e3;
2614
2673
  function clipEmbedInput(text) {
2615
2674
  if (text.length <= EMBED_MAX_CHARS) return text;
@@ -2739,7 +2798,7 @@ async function rebuildIndex(kv) {
2739
2798
  idx.add(memoryToObservation(memory));
2740
2799
  await enqueue({
2741
2800
  id: memory.id,
2742
- sessionId: memory.sessionIds[0] ?? "memory",
2801
+ sessionId: memory.sessionIds?.[0] ?? "memory",
2743
2802
  text: memory.title + " " + memory.content,
2744
2803
  context: {
2745
2804
  kind: "memory",
@@ -2798,8 +2857,8 @@ function registerSearchFunction(sdk, kv) {
2798
2857
  if (!Number.isInteger(data.limit) || data.limit < 1) throw new Error("mem::search: limit must be a positive integer");
2799
2858
  effectiveLimit = Math.min(data.limit, MAX_LIMIT);
2800
2859
  }
2801
- const projectFilter = typeof data.project === "string" && data.project.length > 0 ? data.project : void 0;
2802
- const cwdFilter = typeof data.cwd === "string" && data.cwd.length > 0 ? data.cwd : void 0;
2860
+ const projectFilter = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
2861
+ const cwdFilter = typeof data.cwd === "string" && data.cwd.trim().length > 0 ? data.cwd.trim() : void 0;
2803
2862
  const format = typeof data.format === "string" ? data.format : "full";
2804
2863
  if (![
2805
2864
  "full",
@@ -2825,14 +2884,25 @@ function registerSearchFunction(sdk, kv) {
2825
2884
  sessionCache.set(sessionId, s ?? null);
2826
2885
  return s ?? null;
2827
2886
  };
2887
+ const memoryProjectCache = /* @__PURE__ */ new Map();
2888
+ const loadMemoryProject = async (obsId) => {
2889
+ if (memoryProjectCache.has(obsId)) return memoryProjectCache.get(obsId);
2890
+ const proj = (await kv.get(KV.memories, obsId).catch(() => null))?.project ?? null;
2891
+ memoryProjectCache.set(obsId, proj);
2892
+ return proj;
2893
+ };
2828
2894
  const candidates = [];
2829
2895
  for (const r of results) {
2830
2896
  if (candidates.length >= effectiveLimit) break;
2831
2897
  if (filtering) {
2832
2898
  const s = await loadSession(r.sessionId);
2833
- if (!s) continue;
2834
- if (projectFilter && s.project !== projectFilter) continue;
2835
- if (cwdFilter && s.cwd !== cwdFilter) continue;
2899
+ if (s) {
2900
+ if (projectFilter && s.project !== projectFilter) continue;
2901
+ if (cwdFilter && s.cwd !== cwdFilter) continue;
2902
+ } else if (projectFilter) {
2903
+ const memProject = await loadMemoryProject(r.obsId);
2904
+ if (memProject !== null && memProject !== projectFilter) continue;
2905
+ }
2836
2906
  }
2837
2907
  candidates.push(r);
2838
2908
  }
@@ -3004,11 +3074,14 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3004
3074
  error: `Session observation limit reached (${maxObservationsPerSession})`
3005
3075
  };
3006
3076
  }
3077
+ const existingSession = await kv.get(KV.sessions, payload.sessionId);
3078
+ const inheritedAgentId = existingSession ? existingSession.agentId : getAgentId();
3079
+ if (inheritedAgentId) raw.agentId = inheritedAgentId;
3007
3080
  if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
3008
- const { saveImageToDisk } = await import("./image-store-DyrKZKqZ.mjs");
3081
+ const { saveImageToDisk } = await import("./image-store-CdE0amb1.mjs");
3009
3082
  const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
3010
3083
  raw.imageData = filePath;
3011
- const { incrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
3084
+ const { incrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
3012
3085
  await incrementImageRef(kv, filePath);
3013
3086
  sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: bytesWritten });
3014
3087
  if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.triggerVoid("mem::vision-embed", {
@@ -3021,7 +3094,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3021
3094
  await kv.set(KV.observations(payload.sessionId), obsId, raw);
3022
3095
  } catch (error) {
3023
3096
  if (raw.imageData) {
3024
- const { deleteImage } = await import("./image-store-DyrKZKqZ.mjs");
3097
+ const { deleteImage } = await import("./image-store-CdE0amb1.mjs");
3025
3098
  const { deletedBytes } = await deleteImage(raw.imageData);
3026
3099
  if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
3027
3100
  }
@@ -3055,7 +3128,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3055
3128
  },
3056
3129
  action: TriggerAction.Void()
3057
3130
  });
3058
- const session = await kv.get(KV.sessions, payload.sessionId);
3131
+ const session = existingSession;
3059
3132
  if (session) {
3060
3133
  const updates = [{
3061
3134
  type: "set",
@@ -3075,6 +3148,20 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3075
3148
  });
3076
3149
  }
3077
3150
  await kv.update(KV.sessions, payload.sessionId, updates);
3151
+ } else if (typeof payload.project === "string" && payload.project.trim().length > 0 && typeof payload.cwd === "string" && payload.cwd.trim().length > 0) {
3152
+ const trimmedPrompt = typeof raw.userPrompt === "string" ? raw.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
3153
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3154
+ await kv.set(KV.sessions, payload.sessionId, {
3155
+ id: payload.sessionId,
3156
+ project: payload.project,
3157
+ cwd: payload.cwd,
3158
+ startedAt: payload.timestamp ?? ts,
3159
+ updatedAt: ts,
3160
+ status: "active",
3161
+ observationCount: 1,
3162
+ ...inheritedAgentId ? { agentId: inheritedAgentId } : {},
3163
+ ...trimmedPrompt && trimmedPrompt.length > 0 ? { firstPrompt: trimmedPrompt } : {}
3164
+ });
3078
3165
  }
3079
3166
  if (isAutoCompressEnabled()) await sdk.trigger({
3080
3167
  function_id: "mem::compress",
@@ -3460,10 +3547,10 @@ const DEFAULT_SLOTS = [
3460
3547
  }
3461
3548
  ];
3462
3549
  function isSlotsEnabled() {
3463
- return process.env["AGENTMEMORY_SLOTS"] === "true";
3550
+ return getEnvVar("AGENTMEMORY_SLOTS") === "true";
3464
3551
  }
3465
3552
  function isReflectEnabled() {
3466
- return process.env["AGENTMEMORY_REFLECT"] === "true";
3553
+ return getEnvVar("AGENTMEMORY_REFLECT") === "true";
3467
3554
  }
3468
3555
  function scopeKv(scope) {
3469
3556
  return scope === "global" ? KV.globalSlots : KV.slots;
@@ -4227,7 +4314,8 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
4227
4314
  confidence: qualityScore / 100,
4228
4315
  ...hasImage ? { modality: data.raw.modality } : {},
4229
4316
  ...imageDescription ? { imageDescription } : {},
4230
- ...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
4317
+ ...data.raw.imageData ? { imageRef: data.raw.imageData } : {},
4318
+ ...data.raw.agentId ? { agentId: data.raw.agentId } : {}
4231
4319
  };
4232
4320
  await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
4233
4321
  try {
@@ -4727,8 +4815,78 @@ function isAllowedPath(dbPath) {
4727
4815
  const resolved = resolve(dbPath);
4728
4816
  return ALLOWED_DIRS.some((dir) => resolved.startsWith(dir + "/"));
4729
4817
  }
4818
+ async function inferMemoryProjects(kv, dryRun = false) {
4819
+ const memories = await kv.list(KV.memories);
4820
+ const sessionCache = /* @__PURE__ */ new Map();
4821
+ const loadSession = async (sid) => {
4822
+ if (sessionCache.has(sid)) return sessionCache.get(sid);
4823
+ const s = await kv.get(KV.sessions, sid).catch(() => null);
4824
+ sessionCache.set(sid, s);
4825
+ return s;
4826
+ };
4827
+ let updated = 0;
4828
+ let skipped = 0;
4829
+ let ambiguous = 0;
4830
+ for (const memory of memories) {
4831
+ if (memory.project) {
4832
+ skipped++;
4833
+ continue;
4834
+ }
4835
+ const sessionIds = memory.sessionIds ?? [];
4836
+ if (sessionIds.length === 0) {
4837
+ ambiguous++;
4838
+ continue;
4839
+ }
4840
+ const projects = [];
4841
+ for (const sid of sessionIds) {
4842
+ const session = await loadSession(sid);
4843
+ if (session?.project) projects.push(session.project);
4844
+ }
4845
+ if (projects.length === 0) {
4846
+ ambiguous++;
4847
+ continue;
4848
+ }
4849
+ const freq = /* @__PURE__ */ new Map();
4850
+ for (const p of projects) freq.set(p, (freq.get(p) ?? 0) + 1);
4851
+ const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
4852
+ const [topProject, topCount] = sorted[0];
4853
+ if (topCount <= projects.length / 2 && sorted.length > 1) {
4854
+ ambiguous++;
4855
+ continue;
4856
+ }
4857
+ if (!dryRun) {
4858
+ memory.project = topProject;
4859
+ await kv.set(KV.memories, memory.id, memory);
4860
+ }
4861
+ updated++;
4862
+ }
4863
+ logger.info("inferMemoryProjects complete", {
4864
+ updated,
4865
+ skipped,
4866
+ ambiguous,
4867
+ dryRun
4868
+ });
4869
+ return {
4870
+ updated,
4871
+ skipped,
4872
+ ambiguous
4873
+ };
4874
+ }
4730
4875
  function registerMigrateFunction(sdk, kv) {
4731
4876
  sdk.registerFunction("mem::migrate", async (data) => {
4877
+ if (data.step === "infer-memory-projects") {
4878
+ const dryRun = data.dryRun ?? false;
4879
+ logger.info("Migration step: infer-memory-projects", { dryRun });
4880
+ return {
4881
+ success: true,
4882
+ step: "infer-memory-projects",
4883
+ ...await inferMemoryProjects(kv, dryRun)
4884
+ };
4885
+ }
4886
+ if (!data.dbPath) return {
4887
+ success: false,
4888
+ error: "Either step or dbPath is required"
4889
+ };
4732
4890
  logger.info("Migration started", { dbPath: data.dbPath });
4733
4891
  if (!isAllowedPath(data.dbPath)) return {
4734
4892
  success: false,
@@ -5004,9 +5162,10 @@ function registerConsolidateFunction(sdk, kv, provider) {
5004
5162
  llmCallCount++;
5005
5163
  const parsed = parseMemoryXml(response, sessionIds);
5006
5164
  if (!parsed) continue;
5007
- const existingMatch = existingMemories.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase());
5008
5165
  const now = (/* @__PURE__ */ new Date()).toISOString();
5009
5166
  const obsIds = [...new Set(top.map((o) => o.id))];
5167
+ const scopedProject = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
5168
+ const existingMatch = existingMemories.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase() && (!scopedProject || !m.project || m.project === scopedProject));
5010
5169
  if (existingMatch) {
5011
5170
  existingMatch.isLatest = false;
5012
5171
  await kv.set(KV.memories, existingMatch.id, existingMatch);
@@ -5023,7 +5182,8 @@ function registerConsolidateFunction(sdk, kv, provider) {
5023
5182
  parentId: existingMatch.id,
5024
5183
  supersedes: [existingMatch.id, ...existingMatch.supersedes || []],
5025
5184
  sourceObservationIds: obsIds,
5026
- isLatest: true
5185
+ isLatest: true,
5186
+ ...scopedProject !== void 0 && { project: scopedProject }
5027
5187
  };
5028
5188
  await kv.set(KV.memories, evolved.id, evolved);
5029
5189
  await recordAudit(kv, "evolve", "mem::consolidate", [evolved.id], {
@@ -5042,7 +5202,8 @@ function registerConsolidateFunction(sdk, kv, provider) {
5042
5202
  ...parsed,
5043
5203
  sourceObservationIds: obsIds,
5044
5204
  version: 1,
5045
- isLatest: true
5205
+ isLatest: true,
5206
+ ...scopedProject !== void 0 && { project: scopedProject }
5046
5207
  };
5047
5208
  await kv.set(KV.memories, memory.id, memory);
5048
5209
  await recordAudit(kv, "remember", "mem::consolidate", [memory.id], {
@@ -5183,6 +5344,7 @@ function registerRememberFunction(sdk, kv) {
5183
5344
  "fact"
5184
5345
  ]).has(data.type || "") ? data.type : "fact";
5185
5346
  const now = (/* @__PURE__ */ new Date()).toISOString();
5347
+ const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
5186
5348
  return withKeyedLock("mem:remember", async () => {
5187
5349
  const existingMemories = await kv.list(KV.memories);
5188
5350
  let supersededId;
@@ -5191,6 +5353,7 @@ function registerRememberFunction(sdk, kv) {
5191
5353
  const lowerContent = data.content.toLowerCase();
5192
5354
  for (const existing of existingMemories) {
5193
5355
  if (existing.isLatest === false) continue;
5356
+ if (project && existing.project && existing.project !== project) continue;
5194
5357
  if (jaccardSimilarity(lowerContent, existing.content.toLowerCase()) > .7) {
5195
5358
  supersededId = existing.id;
5196
5359
  supersededVersion = existing.version ?? 1;
@@ -5198,6 +5361,7 @@ function registerRememberFunction(sdk, kv) {
5198
5361
  break;
5199
5362
  }
5200
5363
  }
5364
+ const callAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim().slice(0, 128) : getAgentId();
5201
5365
  const memory = {
5202
5366
  id: generateId("mem"),
5203
5367
  createdAt: now,
@@ -5213,7 +5377,9 @@ function registerRememberFunction(sdk, kv) {
5213
5377
  parentId: supersededId,
5214
5378
  supersedes: supersededId ? [supersededId] : [],
5215
5379
  sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
5216
- isLatest: true
5380
+ isLatest: true,
5381
+ ...callAgentId ? { agentId: callAgentId } : {},
5382
+ ...project !== void 0 && { project }
5217
5383
  };
5218
5384
  if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
5219
5385
  if (supersededMemory) {
@@ -5229,7 +5395,7 @@ function registerRememberFunction(sdk, kv) {
5229
5395
  error: err instanceof Error ? err.message : String(err)
5230
5396
  });
5231
5397
  }
5232
- await vectorIndexAddGuarded(memory.id, memory.sessionIds[0] ?? "memory", memory.title + " " + memory.content, {
5398
+ await vectorIndexAddGuarded(memory.id, memory.sessionIds?.[0] ?? "memory", memory.title + " " + memory.content, {
5233
5399
  kind: "memory",
5234
5400
  logId: memory.id
5235
5401
  });
@@ -5240,7 +5406,8 @@ function registerRememberFunction(sdk, kv) {
5240
5406
  });
5241
5407
  logger.info("Memory saved", {
5242
5408
  memId: memory.id,
5243
- type: memory.type
5409
+ type: memory.type,
5410
+ project: memory.project
5244
5411
  });
5245
5412
  return {
5246
5413
  success: true,
@@ -5253,12 +5420,14 @@ function registerRememberFunction(sdk, kv) {
5253
5420
  const deletedMemoryIds = [];
5254
5421
  const deletedObservationIds = [];
5255
5422
  let deletedSession = false;
5256
- const { decrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
5423
+ const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
5257
5424
  if (data.memoryId) {
5258
5425
  const mem = await kv.get(KV.memories, data.memoryId);
5259
5426
  await kv.delete(KV.memories, data.memoryId);
5260
5427
  if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
5261
5428
  await deleteAccessLog(kv, data.memoryId);
5429
+ getSearchIndex().remove(data.memoryId);
5430
+ vectorIndexRemove(data.memoryId);
5262
5431
  deletedMemoryIds.push(data.memoryId);
5263
5432
  deleted++;
5264
5433
  }
@@ -5267,6 +5436,8 @@ function registerRememberFunction(sdk, kv) {
5267
5436
  await kv.delete(KV.observations(data.sessionId), obsId);
5268
5437
  if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5269
5438
  if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5439
+ getSearchIndex().remove(obsId);
5440
+ vectorIndexRemove(obsId);
5270
5441
  deletedObservationIds.push(obsId);
5271
5442
  deleted++;
5272
5443
  }
@@ -5276,6 +5447,8 @@ function registerRememberFunction(sdk, kv) {
5276
5447
  await kv.delete(KV.observations(data.sessionId), obs.id);
5277
5448
  if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5278
5449
  if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5450
+ getSearchIndex().remove(obs.id);
5451
+ vectorIndexRemove(obs.id);
5279
5452
  deletedObservationIds.push(obs.id);
5280
5453
  deleted++;
5281
5454
  }
@@ -5284,14 +5457,17 @@ function registerRememberFunction(sdk, kv) {
5284
5457
  deletedSession = true;
5285
5458
  deleted += 2;
5286
5459
  }
5287
- if (deleted > 0) await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5288
- sessionId: data.sessionId,
5289
- deleted,
5290
- memoriesDeleted: deletedMemoryIds.length,
5291
- observationsDeleted: deletedObservationIds.length,
5292
- sessionDeleted: deletedSession,
5293
- reason: "user-initiated forget"
5294
- });
5460
+ if (deleted > 0) {
5461
+ await flushIndexSave();
5462
+ await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5463
+ sessionId: data.sessionId,
5464
+ deleted,
5465
+ memoriesDeleted: deletedMemoryIds.length,
5466
+ observationsDeleted: deletedObservationIds.length,
5467
+ sessionDeleted: deletedSession,
5468
+ reason: "user-initiated forget"
5469
+ });
5470
+ }
5295
5471
  logger.info("Memory forgotten", { deleted });
5296
5472
  return {
5297
5473
  success: true,
@@ -5352,7 +5528,7 @@ async function runRecoveredSessionConsolidation(sdk) {
5352
5528
  function registerEvictFunction(sdk, kv) {
5353
5529
  sdk.registerFunction("mem::evict", async (data) => {
5354
5530
  const dryRun = data?.dryRun ?? false;
5355
- const { decrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
5531
+ const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
5356
5532
  const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
5357
5533
  const cfg = {
5358
5534
  ...DEFAULTS$1,
@@ -5806,6 +5982,9 @@ async function findByKeyword(kv, keyword, project) {
5806
5982
  const LESSON_CONTENT_PREVIEW_CHARS = 240;
5807
5983
  function registerSmartSearchFunction(sdk, kv, searchFn) {
5808
5984
  sdk.registerFunction("mem::smart-search", async (data) => {
5985
+ const isolated = isAgentScopeIsolated();
5986
+ const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
5987
+ const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
5809
5988
  if (data.expandIds && data.expandIds.length > 0) {
5810
5989
  const raw = data.expandIds.slice(0, 20);
5811
5990
  const items = raw.map((entry) => {
@@ -5826,17 +6005,19 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
5826
6005
  observation: obs
5827
6006
  } : null)));
5828
6007
  for (const r of results) if (r) expanded.push(r);
5829
- recordAccessBatch(kv, expanded.map((e) => e.observation.id));
6008
+ const scoped = filterAgentId ? expanded.filter((e) => e.observation.agentId === filterAgentId) : expanded;
6009
+ recordAccessBatch(kv, scoped.map((e) => e.observation.id));
5830
6010
  const truncated = data.expandIds.length > raw.length;
5831
6011
  logger.info("Smart search expanded", {
5832
6012
  requested: data.expandIds.length,
5833
6013
  attempted: raw.length,
5834
- returned: expanded.length,
6014
+ returned: scoped.length,
6015
+ filteredOutOfScope: expanded.length - scoped.length,
5835
6016
  truncated
5836
6017
  });
5837
6018
  return {
5838
6019
  mode: "expanded",
5839
- results: expanded,
6020
+ results: scoped,
5840
6021
  truncated
5841
6022
  };
5842
6023
  }
@@ -5848,8 +6029,9 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
5848
6029
  const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
5849
6030
  const lessonLimit = Math.min(limit, 10);
5850
6031
  const includeLessons = data.includeLessons !== false;
5851
- const [hybridResults, lessons] = await Promise.all([searchFn(data.query, limit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
5852
- const compact = hybridResults.map((r) => ({
6032
+ const overFetchLimit = filterAgentId ? Math.min(limit * 3, 300) : limit;
6033
+ const [hybridResults, lessons] = await Promise.all([searchFn(data.query, overFetchLimit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
6034
+ const compact = (filterAgentId ? hybridResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : hybridResults.slice(0, limit)).map((r) => ({
5853
6035
  obsId: r.observation.id,
5854
6036
  sessionId: r.sessionId,
5855
6037
  title: r.observation.title,
@@ -6006,7 +6188,7 @@ function registerAutoForgetFunction(sdk, kv) {
6006
6188
  sdk.registerFunction("mem::auto-forget", async (data) => {
6007
6189
  const dryRun = data?.dryRun ?? false;
6008
6190
  const now = Date.now();
6009
- const { decrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
6191
+ const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
6010
6192
  const result = {
6011
6193
  ttlExpired: [],
6012
6194
  contradictions: [],
@@ -6028,6 +6210,8 @@ function registerAutoForgetFunction(sdk, kv) {
6028
6210
  timestamp: mem.forgetAfter
6029
6211
  });
6030
6212
  await deleteAccessLog(kv, mem.id);
6213
+ getSearchIndex().remove(mem.id);
6214
+ vectorIndexRemove(mem.id);
6031
6215
  }
6032
6216
  }
6033
6217
  }
@@ -6105,10 +6289,13 @@ function registerAutoForgetFunction(sdk, kv) {
6105
6289
  sessionId: sessions[i].id,
6106
6290
  timestamp: obs.timestamp
6107
6291
  });
6292
+ getSearchIndex().remove(obs.id);
6293
+ vectorIndexRemove(obs.id);
6108
6294
  }
6109
6295
  }
6110
6296
  }
6111
6297
  }
6298
+ if (!dryRun && (result.ttlExpired.length > 0 || result.lowValueObs.length > 0)) await flushIndexSave();
6112
6299
  logger.info("Auto-forget complete", {
6113
6300
  ttlExpired: result.ttlExpired.length,
6114
6301
  contradictions: result.contradictions.length,
@@ -6256,7 +6443,9 @@ function registerExportImportFunction(sdk, kv) {
6256
6443
  "0.9.18",
6257
6444
  "0.9.19",
6258
6445
  "0.9.20",
6259
- "0.9.21"
6446
+ "0.9.21",
6447
+ "0.9.22",
6448
+ "0.9.23"
6260
6449
  ]).has(importData.version)) return {
6261
6450
  success: false,
6262
6451
  error: `Unsupported export version: ${importData.version}`
@@ -6379,6 +6568,7 @@ function registerExportImportFunction(sdk, kv) {
6379
6568
  continue;
6380
6569
  }
6381
6570
  }
6571
+ if (!Array.isArray(memory.sessionIds)) memory.sessionIds = [];
6382
6572
  await kv.set(KV.memories, memory.id, memory);
6383
6573
  stats.memories++;
6384
6574
  }
@@ -6582,6 +6772,7 @@ function escapeXml(s) {
6582
6772
  }
6583
6773
  function registerEnrichFunction(sdk, kv) {
6584
6774
  sdk.registerFunction("mem::enrich", async (data) => {
6775
+ const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
6585
6776
  const parts = [];
6586
6777
  const fileContextPromise = sdk.trigger({
6587
6778
  function_id: "mem::file-context",
@@ -6595,10 +6786,11 @@ function registerEnrichFunction(sdk, kv) {
6595
6786
  function_id: "mem::search",
6596
6787
  payload: {
6597
6788
  query: searchQueries.join(" "),
6598
- limit: 5
6789
+ limit: 5,
6790
+ ...project !== void 0 && { project }
6599
6791
  }
6600
6792
  }).catch(() => ({ results: [] })) : Promise.resolve({ results: [] });
6601
- const bugMemoriesPromise = kv.list(KV.memories).then((memories) => memories.filter((m) => m.type === "bug" && m.isLatest && m.files.some((f) => data.files.some((df) => f.includes(df) || df.includes(f)))).sort((a, b) => new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime())).catch(() => []);
6793
+ const bugMemoriesPromise = kv.list(KV.memories).then((memories) => memories.filter((m) => m.type === "bug" && m.isLatest && (!project || !m.project || m.project === project) && m.files.some((f) => data.files.some((df) => f.includes(df) || df.includes(f)))).sort((a, b) => new Date(b.updatedAt || b.createdAt).getTime() - new Date(a.updatedAt || a.createdAt).getTime())).catch(() => []);
6602
6794
  const [fileContext, searchResult, bugMemories] = await Promise.all([
6603
6795
  fileContextPromise,
6604
6796
  searchPromise,
@@ -6621,6 +6813,7 @@ function registerEnrichFunction(sdk, kv) {
6621
6813
  }
6622
6814
  logger.info("Enrichment completed", {
6623
6815
  sessionId: data.sessionId,
6816
+ project,
6624
6817
  fileCount: data.files.length,
6625
6818
  contextLength: context.length,
6626
6819
  truncated
@@ -6778,16 +6971,24 @@ function buildGraphExtractionPrompt(observations) {
6778
6971
 
6779
6972
  //#endregion
6780
6973
  //#region src/functions/graph.ts
6974
+ function parseAttrs(raw) {
6975
+ const attrs = {};
6976
+ const attrRegex = /([A-Za-z_][\w:-]*)="([^"]*)"/g;
6977
+ let m;
6978
+ while ((m = attrRegex.exec(raw)) !== null) attrs[m[1]] = m[2];
6979
+ return attrs;
6980
+ }
6781
6981
  function parseGraphXml(xml, observationIds) {
6782
6982
  const nodes = [];
6783
6983
  const edges = [];
6784
6984
  const now = (/* @__PURE__ */ new Date()).toISOString();
6785
- const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/entity>/g;
6786
- let match;
6787
- while ((match = entityRegex.exec(xml)) !== null) {
6788
- const type = match[1];
6789
- const name = match[2];
6790
- const propsBlock = match[3];
6985
+ const entitySelfClose = /<entity\b([^>]*?)\/>/g;
6986
+ const entityWithBody = /<entity\b([^>]*[^/])>([\s\S]*?)<\/entity>/g;
6987
+ const addEntity = (rawAttrs, propsBlock = "") => {
6988
+ const attrs = parseAttrs(rawAttrs);
6989
+ const type = attrs["type"];
6990
+ const name = attrs["name"];
6991
+ if (!type || !name) return;
6791
6992
  const properties = {};
6792
6993
  const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
6793
6994
  let propMatch;
@@ -6800,17 +7001,23 @@ function parseGraphXml(xml, observationIds) {
6800
7001
  sourceObservationIds: observationIds,
6801
7002
  createdAt: now
6802
7003
  });
6803
- }
6804
- const relRegex = /<relationship\s+type="([^"]+)"\s+source="([^"]+)"\s+target="([^"]+)"\s+weight="([^"]+)"\s*\/>/g;
7004
+ };
7005
+ let match;
7006
+ while ((match = entitySelfClose.exec(xml)) !== null) addEntity(match[1]);
7007
+ while ((match = entityWithBody.exec(xml)) !== null) addEntity(match[1], match[2]);
7008
+ const relRegex = /<relationship\b([^>]*?)\/>/g;
6805
7009
  while ((match = relRegex.exec(xml)) !== null) {
6806
- const type = match[1];
6807
- const sourceName = match[2];
6808
- const targetName = match[3];
6809
- const parsedWeight = parseFloat(match[4]);
6810
- const weight = Number.isNaN(parsedWeight) ? .5 : parsedWeight;
7010
+ const attrs = parseAttrs(match[1]);
7011
+ const type = attrs["type"];
7012
+ const sourceName = attrs["source"];
7013
+ const targetName = attrs["target"];
7014
+ if (!type || !sourceName || !targetName) continue;
7015
+ const parsedWeight = parseFloat(attrs["weight"] ?? "");
7016
+ const weight = Number.isFinite(parsedWeight) ? parsedWeight : .5;
6811
7017
  const sourceNode = nodes.find((n) => n.name === sourceName);
6812
7018
  const targetNode = nodes.find((n) => n.name === targetName);
6813
- if (sourceNode && targetNode) edges.push({
7019
+ if (!sourceNode || !targetNode) continue;
7020
+ edges.push({
6814
7021
  id: generateId("ge"),
6815
7022
  type,
6816
7023
  sourceNodeId: sourceNode.id,
@@ -7024,7 +7231,7 @@ function registerConsolidationPipelineFunction(sdk, kv, provider) {
7024
7231
  if (!data?.force && !isConsolidationEnabled()) return {
7025
7232
  success: false,
7026
7233
  skipped: true,
7027
- reason: "CONSOLIDATION_ENABLED is not set to true"
7234
+ reason: "Consolidation disabled: set CONSOLIDATION_ENABLED=true or configure an LLM provider (ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GEMINI_API_KEY / GOOGLE_API_KEY / MINIMAX_API_KEY / OPENAI_BASE_URL / AGENTMEMORY_PROVIDER=agent-sdk)"
7028
7235
  };
7029
7236
  const tier = data?.tier || "all";
7030
7237
  const decayDays = getConsolidationDecayDays();
@@ -7315,8 +7522,11 @@ function registerGovernanceFunction(sdk, kv) {
7315
7522
  for (const id of data.memoryIds) if (await kv.get(KV.memories, id)) {
7316
7523
  await kv.delete(KV.memories, id);
7317
7524
  await deleteAccessLog(kv, id);
7525
+ getSearchIndex().remove(id);
7526
+ vectorIndexRemove(id);
7318
7527
  deleted++;
7319
7528
  }
7529
+ if (deleted > 0) await flushIndexSave();
7320
7530
  await recordAudit(kv, "delete", "mem::governance-delete", data.memoryIds, {
7321
7531
  reason: data.reason || "manual deletion",
7322
7532
  deleted
@@ -7369,6 +7579,8 @@ function registerGovernanceFunction(sdk, kv) {
7369
7579
  (await Promise.allSettled(batch.map(async (mem) => {
7370
7580
  await kv.delete(KV.memories, mem.id);
7371
7581
  await deleteAccessLog(kv, mem.id);
7582
+ getSearchIndex().remove(mem.id);
7583
+ vectorIndexRemove(mem.id);
7372
7584
  }))).forEach((result, j) => {
7373
7585
  const mem = batch[j];
7374
7586
  if (result.status === "fulfilled") successfulIds.push(mem.id);
@@ -7384,6 +7596,7 @@ function registerGovernanceFunction(sdk, kv) {
7384
7596
  }
7385
7597
  });
7386
7598
  }
7599
+ if (successfulIds.length > 0) await flushIndexSave();
7387
7600
  await safeAudit(kv, "delete", "mem::governance-bulk", successfulIds, {
7388
7601
  filter: data,
7389
7602
  deleted: successfulIds.length,
@@ -10188,11 +10401,34 @@ function registerDiagnosticsFunction(sdk, kv) {
10188
10401
  });
10189
10402
  memoryIssues++;
10190
10403
  }
10404
+ const latestMemories = memories.filter((m) => m.isLatest);
10405
+ const unscopedCount = latestMemories.filter((m) => !m.project).length;
10406
+ if (unscopedCount === 0) checks.push({
10407
+ name: "memory-project-coverage",
10408
+ category: "memories",
10409
+ status: "pass",
10410
+ message: `All ${latestMemories.length} latest memories have a project scope`,
10411
+ fixable: false
10412
+ });
10413
+ else if (unscopedCount <= 10) checks.push({
10414
+ name: "memory-project-coverage",
10415
+ category: "memories",
10416
+ status: "warn",
10417
+ message: `${unscopedCount} of ${latestMemories.length} latest memories have no project scope — run POST /agentmemory/migrate {"step":"infer-memory-projects"} to backfill`,
10418
+ fixable: true
10419
+ });
10420
+ else checks.push({
10421
+ name: "memory-project-coverage",
10422
+ category: "memories",
10423
+ status: "fail",
10424
+ message: `${unscopedCount} of ${latestMemories.length} latest memories have no project scope — run POST /agentmemory/migrate {"step":"infer-memory-projects"} to backfill`,
10425
+ fixable: true
10426
+ });
10191
10427
  if (memoryIssues === 0) checks.push({
10192
10428
  name: "memories-ok",
10193
10429
  category: "memories",
10194
10430
  status: "pass",
10195
- message: `All ${memories.length} memories are consistent`,
10431
+ message: `All ${memories.length} memories are structurally consistent`,
10196
10432
  fixable: false
10197
10433
  });
10198
10434
  }
@@ -12746,7 +12982,7 @@ function registerRetentionFunctions(sdk, kv) {
12746
12982
  const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
12747
12983
  const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
12748
12984
  const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
12749
- const { decrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
12985
+ const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
12750
12986
  const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
12751
12987
  if (data?.dryRun) return {
12752
12988
  success: true,
@@ -12783,6 +13019,8 @@ function registerRetentionFunctions(sdk, kv) {
12783
13019
  await kv.delete(scope, candidate.memoryId);
12784
13020
  await kv.delete(KV.retentionScores, candidate.memoryId);
12785
13021
  await deleteAccessLog(kv, candidate.memoryId);
13022
+ getSearchIndex().remove(candidate.memoryId);
13023
+ vectorIndexRemove(candidate.memoryId);
12786
13024
  evicted++;
12787
13025
  evictedIds.push(candidate.memoryId);
12788
13026
  if (resolvedSource === "semantic") evictedSemantic++;
@@ -12790,13 +13028,16 @@ function registerRetentionFunctions(sdk, kv) {
12790
13028
  } catch {
12791
13029
  continue;
12792
13030
  }
12793
- if (evicted > 0) await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
12794
- threshold,
12795
- evicted,
12796
- evictedEpisodic,
12797
- evictedSemantic,
12798
- reason: "retention score below threshold"
12799
- });
13031
+ if (evicted > 0) {
13032
+ await flushIndexSave();
13033
+ await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
13034
+ threshold,
13035
+ evicted,
13036
+ evictedEpisodic,
13037
+ evictedSemantic,
13038
+ reason: "retention score below threshold"
13039
+ });
13040
+ }
12800
13041
  logger.info("Retention-based eviction complete", {
12801
13042
  evicted,
12802
13043
  evictedEpisodic,
@@ -13701,122 +13942,355 @@ function renderViewerDocument() {
13701
13942
  }
13702
13943
 
13703
13944
  //#endregion
13704
- //#region src/triggers/api.ts
13705
- function parseOptionalInt(raw) {
13706
- if (raw === void 0 || raw === null || raw === "") return void 0;
13707
- const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
13708
- return Number.isFinite(n) ? n : void 0;
13709
- }
13710
- function checkAuth(req, secret) {
13711
- if (!secret) return null;
13712
- const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
13713
- if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
13714
- status_code: 401,
13715
- body: { error: "unauthorized" }
13716
- };
13945
+ //#region src/viewer/server.ts
13946
+ function loadViewerFavicon() {
13947
+ const base = dirname(fileURLToPath(import.meta.url));
13948
+ const candidates = [
13949
+ join(base, "..", "src", "viewer", "favicon.svg"),
13950
+ join(base, "..", "viewer", "favicon.svg"),
13951
+ join(base, "viewer", "favicon.svg")
13952
+ ];
13953
+ for (const path of candidates) try {
13954
+ return readFileSync(path);
13955
+ } catch {}
13717
13956
  return null;
13718
13957
  }
13719
- function requireConfiguredSecret(secret, feature) {
13720
- if (secret) return null;
13721
- return {
13722
- status_code: 503,
13723
- body: { error: `${feature} requires AGENTMEMORY_SECRET` }
13724
- };
13958
+ 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());
13959
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
13960
+ function buildAllowedHosts(origins, listenPort) {
13961
+ const hosts = /* @__PURE__ */ new Set();
13962
+ for (const o of origins) try {
13963
+ const parsed = new URL(o);
13964
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
13965
+ } catch {}
13966
+ hosts.add(`localhost:${listenPort}`);
13967
+ hosts.add(`127.0.0.1:${listenPort}`);
13968
+ hosts.add(`[::1]:${listenPort}`);
13969
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
13970
+ return hosts;
13725
13971
  }
13726
- function flagDisabledResponse(opts) {
13972
+ function isHostAllowed(headerHost, allowed) {
13973
+ if (typeof headerHost !== "string") return false;
13974
+ const lower = headerHost.toLowerCase().trim();
13975
+ if (!lower) return false;
13976
+ return allowed.has(lower);
13977
+ }
13978
+ function corsHeaders(req) {
13979
+ const origin = req.headers.origin || "";
13980
+ const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
13727
13981
  return {
13728
- status_code: 503,
13729
- body: opts
13982
+ "Access-Control-Allow-Origin": allowed,
13983
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
13984
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
13985
+ Vary: "Origin"
13730
13986
  };
13731
13987
  }
13732
- function graphDisabledResponse() {
13733
- return flagDisabledResponse({
13734
- error: "Knowledge graph not enabled",
13735
- flag: "GRAPH_EXTRACTION_ENABLED",
13736
- enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and restart. Requires an LLM provider key.",
13737
- docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
13988
+ function json(res, status, data, req) {
13989
+ const body = JSON.stringify(data);
13990
+ const cors = req ? corsHeaders(req) : {
13991
+ "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
13992
+ Vary: "Origin"
13993
+ };
13994
+ res.writeHead(status, {
13995
+ ...cors,
13996
+ "Content-Type": "application/json"
13738
13997
  });
13998
+ res.end(body);
13739
13999
  }
13740
- function consolidationDisabledResponse() {
13741
- return flagDisabledResponse({
13742
- error: "Consolidation pipeline not enabled",
13743
- flag: "CONSOLIDATION_ENABLED",
13744
- enableHow: "Set CONSOLIDATION_ENABLED=true and restart. Requires an LLM provider key.",
13745
- docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
14000
+ function readBody(req) {
14001
+ return new Promise((resolve, reject) => {
14002
+ let data = "";
14003
+ let size = 0;
14004
+ req.on("data", (chunk) => {
14005
+ size += chunk.length;
14006
+ if (size > 1e6) {
14007
+ req.destroy();
14008
+ reject(/* @__PURE__ */ new Error("too large"));
14009
+ return;
14010
+ }
14011
+ data += chunk.toString();
14012
+ });
14013
+ req.on("end", () => resolve(data));
14014
+ req.on("error", reject);
13746
14015
  });
13747
14016
  }
13748
- function asNonEmptyString$1(value) {
13749
- if (typeof value !== "string") return null;
13750
- const trimmed = value.trim();
13751
- return trimmed ? trimmed : null;
13752
- }
13753
- function parseOptionalFiniteNumber(value) {
13754
- if (value === void 0 || value === null) return void 0;
13755
- if (typeof value === "number") return Number.isFinite(value) ? value : null;
13756
- if (typeof value === "string") {
13757
- const trimmed = value.trim();
13758
- if (!trimmed) return void 0;
13759
- const parsed = Number(trimmed);
13760
- return Number.isFinite(parsed) ? parsed : null;
13761
- }
13762
- return null;
14017
+ const MAX_VIEWER_PORT_RETRIES = 10;
14018
+ let boundViewerPort = null;
14019
+ let viewerSkipped = false;
14020
+ function getBoundViewerPort() {
14021
+ return boundViewerPort;
13763
14022
  }
13764
- function parseOptionalPositiveInt(value) {
13765
- const parsed = parseOptionalFiniteNumber(value);
13766
- if (parsed === void 0 || parsed === null) return parsed;
13767
- if (!Number.isInteger(parsed) || parsed < 1) return null;
13768
- return parsed;
14023
+ function getViewerSkipped() {
14024
+ return viewerSkipped;
13769
14025
  }
13770
- function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13771
- sdk.registerFunction("middleware::api-auth", async (input) => {
13772
- if (!secret) return { action: "continue" };
13773
- const headers = input?.request?.headers || {};
13774
- const auth = headers["authorization"] || headers["Authorization"];
13775
- if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
13776
- action: "respond",
13777
- response: {
13778
- status_code: 401,
13779
- body: { error: "unauthorized" }
13780
- }
13781
- };
13782
- return { action: "continue" };
13783
- });
13784
- sdk.registerFunction("api::liveness", async () => ({
13785
- status_code: 200,
13786
- body: {
13787
- status: "ok",
13788
- service: "agentmemory"
14026
+ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14027
+ boundViewerPort = null;
14028
+ viewerSkipped = false;
14029
+ const resolvedRestPort = restPort ?? port - 2;
14030
+ const requestedPort = port;
14031
+ let allowedHosts = null;
14032
+ const server = createServer(async (req, res) => {
14033
+ if (!allowedHosts) {
14034
+ const addr = server.address();
14035
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
13789
14036
  }
13790
- }));
13791
- sdk.registerTrigger({
13792
- type: "http",
13793
- function_id: "api::liveness",
13794
- config: {
13795
- api_path: "/agentmemory/livez",
13796
- http_method: "GET"
14037
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
14038
+ res.writeHead(403, { "Content-Type": "text/plain" });
14039
+ res.end("forbidden host");
14040
+ return;
13797
14041
  }
13798
- });
13799
- sdk.registerFunction("api::config-flags", async (req) => {
13800
- const authErr = checkAuth(req, secret);
13801
- if (authErr) return authErr;
13802
- return {
13803
- status_code: 200,
13804
- body: {
13805
- version: VERSION,
13806
- provider: detectLlmProviderKind(),
13807
- embeddingProvider: detectEmbeddingProvider() ? "embeddings" : "none",
13808
- flags: [
13809
- {
13810
- key: "GRAPH_EXTRACTION_ENABLED",
13811
- label: "Knowledge graph extraction",
13812
- enabled: isGraphExtractionEnabled(),
13813
- default: false,
13814
- affects: ["Graph", "Dashboard"],
13815
- needsLlm: true,
13816
- description: "Extracts entities and relations from observations into a knowledge graph.",
13817
- enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and provide an LLM key, then restart.",
13818
- docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
13819
- },
14042
+ const raw = req.url || "/";
14043
+ const qIdx = raw.indexOf("?");
14044
+ const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
14045
+ const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
14046
+ const method = req.method || "GET";
14047
+ if (method === "OPTIONS") {
14048
+ res.writeHead(204, {
14049
+ ...corsHeaders(req),
14050
+ "Access-Control-Max-Age": "86400"
14051
+ });
14052
+ res.end();
14053
+ return;
14054
+ }
14055
+ if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
14056
+ const rendered = renderViewerDocument();
14057
+ if (rendered.found) {
14058
+ res.writeHead(200, {
14059
+ "Content-Type": "text/html; charset=utf-8",
14060
+ "Content-Security-Policy": rendered.csp,
14061
+ "Cache-Control": "no-cache"
14062
+ });
14063
+ res.end(rendered.html);
14064
+ return;
14065
+ }
14066
+ res.writeHead(404, { "Content-Type": "text/plain" });
14067
+ res.end("viewer not found");
14068
+ return;
14069
+ }
14070
+ if (method === "GET" && pathname === "/favicon.svg") {
14071
+ const favicon = loadViewerFavicon();
14072
+ if (favicon) {
14073
+ res.writeHead(200, {
14074
+ "Content-Type": "image/svg+xml",
14075
+ "Cache-Control": "public, max-age=3600"
14076
+ });
14077
+ res.end(favicon);
14078
+ return;
14079
+ }
14080
+ res.writeHead(404, { "Content-Type": "text/plain" });
14081
+ res.end("favicon not found");
14082
+ return;
14083
+ }
14084
+ try {
14085
+ await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
14086
+ } catch (err) {
14087
+ console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
14088
+ json(res, 502, { error: "upstream error" }, req);
14089
+ }
14090
+ });
14091
+ let attempt = 0;
14092
+ let currentPort = requestedPort;
14093
+ const tryListen = () => {
14094
+ server.listen(currentPort, "127.0.0.1");
14095
+ };
14096
+ server.on("listening", () => {
14097
+ const addr = server.address();
14098
+ boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
14099
+ viewerSkipped = false;
14100
+ if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
14101
+ else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
14102
+ });
14103
+ server.on("error", (err) => {
14104
+ if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
14105
+ attempt++;
14106
+ currentPort = requestedPort + attempt;
14107
+ setImmediate(tryListen);
14108
+ return;
14109
+ }
14110
+ if (err.code === "EADDRINUSE") {
14111
+ boundViewerPort = null;
14112
+ viewerSkipped = true;
14113
+ console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
14114
+ } else {
14115
+ boundViewerPort = null;
14116
+ viewerSkipped = true;
14117
+ console.error(`[agentmemory] Viewer error:`, err.message);
14118
+ }
14119
+ });
14120
+ tryListen();
14121
+ return server;
14122
+ }
14123
+ async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
14124
+ const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
14125
+ const headers = {};
14126
+ if (secret) headers["Authorization"] = `Bearer ${secret}`;
14127
+ const ct = req.headers["content-type"];
14128
+ if (ct) headers["Content-Type"] = ct;
14129
+ let body;
14130
+ if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
14131
+ const controller = new AbortController();
14132
+ const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
14133
+ let upstream;
14134
+ try {
14135
+ upstream = await fetch(upstreamUrl, {
14136
+ method,
14137
+ headers,
14138
+ body: body || void 0,
14139
+ signal: controller.signal
14140
+ });
14141
+ clearTimeout(fetchTimeout);
14142
+ } catch (err) {
14143
+ clearTimeout(fetchTimeout);
14144
+ if (err instanceof Error && err.name === "AbortError") {
14145
+ json(res, 504, { error: "upstream timeout" }, req);
14146
+ return;
14147
+ }
14148
+ throw err;
14149
+ }
14150
+ const cors = corsHeaders(req);
14151
+ const responseBody = await upstream.text();
14152
+ const responseHeaders = { ...cors };
14153
+ const upstreamCt = upstream.headers.get("content-type");
14154
+ if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
14155
+ res.writeHead(upstream.status, responseHeaders);
14156
+ res.end(responseBody);
14157
+ }
14158
+
14159
+ //#endregion
14160
+ //#region src/triggers/api.ts
14161
+ function parseOptionalInt(raw) {
14162
+ if (raw === void 0 || raw === null || raw === "") return void 0;
14163
+ const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
14164
+ return Number.isFinite(n) ? n : void 0;
14165
+ }
14166
+ function checkAuth(req, secret) {
14167
+ if (!secret) return null;
14168
+ const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
14169
+ if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
14170
+ status_code: 401,
14171
+ body: { error: "unauthorized" }
14172
+ };
14173
+ return null;
14174
+ }
14175
+ function requireConfiguredSecret(secret, feature) {
14176
+ if (secret) return null;
14177
+ return {
14178
+ status_code: 503,
14179
+ body: { error: `${feature} requires AGENTMEMORY_SECRET` }
14180
+ };
14181
+ }
14182
+ function flagDisabledResponse(opts) {
14183
+ return {
14184
+ status_code: 503,
14185
+ body: opts
14186
+ };
14187
+ }
14188
+ function graphDisabledResponse() {
14189
+ return flagDisabledResponse({
14190
+ error: "Knowledge graph not enabled",
14191
+ flag: "GRAPH_EXTRACTION_ENABLED",
14192
+ enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and restart. Requires an LLM provider key.",
14193
+ docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
14194
+ });
14195
+ }
14196
+ function consolidationDisabledResponse() {
14197
+ return flagDisabledResponse({
14198
+ error: "Consolidation pipeline not enabled",
14199
+ flag: "CONSOLIDATION_ENABLED",
14200
+ enableHow: "Set CONSOLIDATION_ENABLED=true and restart. Requires an LLM provider key.",
14201
+ docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
14202
+ });
14203
+ }
14204
+ function slotsDisabledResponse() {
14205
+ return flagDisabledResponse({
14206
+ error: "Memory slots not enabled",
14207
+ flag: "AGENTMEMORY_SLOTS",
14208
+ enableHow: "Set AGENTMEMORY_SLOTS=true (in ~/.agentmemory/.env or the shell) and restart.",
14209
+ docsHref: "https://github.com/rohitg00/agentmemory#memory-slots"
14210
+ });
14211
+ }
14212
+ function reflectDisabledResponse() {
14213
+ return flagDisabledResponse({
14214
+ error: "Slot reflection not enabled",
14215
+ flag: "AGENTMEMORY_REFLECT",
14216
+ enableHow: "Set AGENTMEMORY_REFLECT=true (in ~/.agentmemory/.env or the shell) and restart. Requires AGENTMEMORY_SLOTS=true.",
14217
+ docsHref: "https://github.com/rohitg00/agentmemory#memory-slots"
14218
+ });
14219
+ }
14220
+ function asNonEmptyString$1(value) {
14221
+ if (typeof value !== "string") return null;
14222
+ const trimmed = value.trim();
14223
+ return trimmed ? trimmed : null;
14224
+ }
14225
+ function parseOptionalFiniteNumber(value) {
14226
+ if (value === void 0 || value === null) return void 0;
14227
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
14228
+ if (typeof value === "string") {
14229
+ const trimmed = value.trim();
14230
+ if (!trimmed) return void 0;
14231
+ const parsed = Number(trimmed);
14232
+ return Number.isFinite(parsed) ? parsed : null;
14233
+ }
14234
+ return null;
14235
+ }
14236
+ function parseOptionalPositiveInt(value) {
14237
+ const parsed = parseOptionalFiniteNumber(value);
14238
+ if (parsed === void 0 || parsed === null) return parsed;
14239
+ if (!Number.isInteger(parsed) || parsed < 1) return null;
14240
+ return parsed;
14241
+ }
14242
+ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14243
+ sdk.registerFunction("middleware::api-auth", async (input) => {
14244
+ if (!secret) return { action: "continue" };
14245
+ const headers = input?.request?.headers || {};
14246
+ const auth = headers["authorization"] || headers["Authorization"];
14247
+ if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
14248
+ action: "respond",
14249
+ response: {
14250
+ status_code: 401,
14251
+ body: { error: "unauthorized" }
14252
+ }
14253
+ };
14254
+ return { action: "continue" };
14255
+ });
14256
+ sdk.registerFunction("api::liveness", async () => ({
14257
+ status_code: 200,
14258
+ body: {
14259
+ status: "ok",
14260
+ service: "agentmemory",
14261
+ viewerPort: getBoundViewerPort(),
14262
+ viewerSkipped: getViewerSkipped()
14263
+ }
14264
+ }));
14265
+ sdk.registerTrigger({
14266
+ type: "http",
14267
+ function_id: "api::liveness",
14268
+ config: {
14269
+ api_path: "/agentmemory/livez",
14270
+ http_method: "GET"
14271
+ }
14272
+ });
14273
+ sdk.registerFunction("api::config-flags", async (req) => {
14274
+ const authErr = checkAuth(req, secret);
14275
+ if (authErr) return authErr;
14276
+ return {
14277
+ status_code: 200,
14278
+ body: {
14279
+ version: VERSION,
14280
+ provider: detectLlmProviderKind(),
14281
+ embeddingProvider: detectEmbeddingProvider() ? "embeddings" : "none",
14282
+ flags: [
14283
+ {
14284
+ key: "GRAPH_EXTRACTION_ENABLED",
14285
+ label: "Knowledge graph extraction",
14286
+ enabled: isGraphExtractionEnabled(),
14287
+ default: false,
14288
+ affects: ["Graph", "Dashboard"],
14289
+ needsLlm: true,
14290
+ description: "Extracts entities and relations from observations into a knowledge graph.",
14291
+ enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and provide an LLM key, then restart.",
14292
+ docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
14293
+ },
13820
14294
  {
13821
14295
  key: "CONSOLIDATION_ENABLED",
13822
14296
  label: "Memory consolidation",
@@ -13880,7 +14354,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13880
14354
  version: VERSION,
13881
14355
  health: health || null,
13882
14356
  functionMetrics,
13883
- circuitBreaker
14357
+ circuitBreaker,
14358
+ viewerPort: getBoundViewerPort(),
14359
+ viewerSkipped: getViewerSkipped()
13884
14360
  }
13885
14361
  };
13886
14362
  });
@@ -14134,6 +14610,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14134
14610
  body: { error: "sessionId, project, and cwd are required non-empty strings" }
14135
14611
  };
14136
14612
  const title = typeof body.title === "string" ? body.title.trim() : void 0;
14613
+ const agentId = (typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim().slice(0, 128) : void 0) ?? getAgentId();
14137
14614
  const session = {
14138
14615
  id: sessionId,
14139
14616
  project,
@@ -14142,7 +14619,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14142
14619
  status: "active",
14143
14620
  observationCount: 0,
14144
14621
  ...title ? { summary: title.slice(0, 200) } : {},
14145
- ...title ? { firstPrompt: title.slice(0, 200) } : {}
14622
+ ...title ? { firstPrompt: title.slice(0, 200) } : {},
14623
+ ...agentId ? { agentId } : {}
14146
14624
  };
14147
14625
  await kv.set(KV.sessions, sessionId, session);
14148
14626
  return {
@@ -14183,6 +14661,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14183
14661
  path: "status",
14184
14662
  value: "completed"
14185
14663
  }]);
14664
+ try {
14665
+ sdk.triggerVoid("event::session::stopped", { sessionId });
14666
+ } catch (err) {
14667
+ logger.warn("event::session::stopped triggerVoid failed", {
14668
+ sessionId,
14669
+ error: err instanceof Error ? err.message : String(err)
14670
+ });
14671
+ }
14186
14672
  return {
14187
14673
  status_code: 200,
14188
14674
  body: { success: true }
@@ -14329,9 +14815,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14329
14815
  sdk.registerFunction("api::sessions", async (req) => {
14330
14816
  const authErr = checkAuth(req, secret);
14331
14817
  if (authErr) return authErr;
14818
+ const sessions = await kv.list(KV.sessions);
14819
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
14820
+ const wildcardAgent = normalizedAgentId === "*";
14821
+ const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
14332
14822
  return {
14333
14823
  status_code: 200,
14334
- body: { sessions: await kv.list(KV.sessions) }
14824
+ body: { sessions: filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions }
14335
14825
  };
14336
14826
  });
14337
14827
  sdk.registerTrigger({
@@ -14350,9 +14840,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14350
14840
  status_code: 400,
14351
14841
  body: { error: "sessionId required" }
14352
14842
  };
14843
+ const observations = await kv.list(KV.observations(sessionId));
14844
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
14845
+ const wildcardAgent = normalizedAgentId === "*";
14846
+ const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
14353
14847
  return {
14354
14848
  status_code: 200,
14355
- body: { observations: await kv.list(KV.observations(sessionId)) }
14849
+ body: { observations: filterAgentId ? observations.filter((o) => o.agentId === filterAgentId) : observations }
14356
14850
  };
14357
14851
  });
14358
14852
  sdk.registerTrigger({
@@ -14393,11 +14887,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14393
14887
  status_code: 400,
14394
14888
  body: { error: "terms must be an array of strings" }
14395
14889
  };
14890
+ if (req.body.project !== void 0 && (typeof req.body.project !== "string" || !req.body.project.trim())) return {
14891
+ status_code: 400,
14892
+ body: { error: "project must be a non-empty string" }
14893
+ };
14396
14894
  return {
14397
14895
  status_code: 200,
14398
14896
  body: await sdk.trigger({
14399
14897
  function_id: "mem::enrich",
14400
- payload: req.body
14898
+ payload: {
14899
+ sessionId: req.body.sessionId,
14900
+ files: req.body.files,
14901
+ ...req.body.terms !== void 0 && { terms: req.body.terms },
14902
+ ...req.body.toolName !== void 0 && { toolName: req.body.toolName },
14903
+ ...req.body.project !== void 0 && { project: req.body.project }
14904
+ }
14401
14905
  })
14402
14906
  };
14403
14907
  });
@@ -14416,11 +14920,23 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14416
14920
  status_code: 400,
14417
14921
  body: { error: "content is required" }
14418
14922
  };
14923
+ if (req.body.project !== void 0 && (typeof req.body.project !== "string" || !req.body.project.trim())) return {
14924
+ status_code: 400,
14925
+ body: { error: "project must be a non-empty string" }
14926
+ };
14419
14927
  return {
14420
14928
  status_code: 201,
14421
14929
  body: await sdk.trigger({
14422
14930
  function_id: "mem::remember",
14423
- payload: req.body
14931
+ payload: {
14932
+ content: req.body.content,
14933
+ ...req.body.type !== void 0 && { type: req.body.type },
14934
+ ...req.body.concepts !== void 0 && { concepts: req.body.concepts },
14935
+ ...req.body.files !== void 0 && { files: req.body.files },
14936
+ ...req.body.ttlDays !== void 0 && { ttlDays: req.body.ttlDays },
14937
+ ...req.body.sourceObservationIds !== void 0 && { sourceObservationIds: req.body.sourceObservationIds },
14938
+ ...req.body.project !== void 0 && { project: req.body.project }
14939
+ }
14424
14940
  })
14425
14941
  };
14426
14942
  });
@@ -14515,15 +15031,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14515
15031
  sdk.registerFunction("api::migrate", async (req) => {
14516
15032
  const authErr = checkAuth(req, secret);
14517
15033
  if (authErr) return authErr;
14518
- if (!req.body?.dbPath || typeof req.body.dbPath !== "string") return {
15034
+ const hasStep = typeof req.body?.step === "string" && req.body.step.trim().length > 0;
15035
+ const hasDbPath = typeof req.body?.dbPath === "string" && req.body.dbPath.trim().length > 0;
15036
+ if (!hasStep && !hasDbPath) return {
14519
15037
  status_code: 400,
14520
- body: { error: "dbPath is required" }
15038
+ body: { error: "Either step (string) or dbPath (string) is required" }
14521
15039
  };
14522
15040
  return {
14523
15041
  status_code: 200,
14524
15042
  body: await sdk.trigger({
14525
15043
  function_id: "mem::migrate",
14526
- payload: req.body
15044
+ payload: {
15045
+ ...req.body.step !== void 0 && { step: req.body.step },
15046
+ ...req.body.dbPath !== void 0 && { dbPath: req.body.dbPath },
15047
+ ...req.body.dryRun !== void 0 && { dryRun: req.body.dryRun }
15048
+ }
14527
15049
  })
14528
15050
  };
14529
15051
  });
@@ -14628,11 +15150,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14628
15150
  sdk.registerFunction("api::export", async (req) => {
14629
15151
  const authErr = checkAuth(req, secret);
14630
15152
  if (authErr) return authErr;
15153
+ const rawMax = req.query_params?.["maxSessions"];
15154
+ const rawOffset = req.query_params?.["offset"];
15155
+ const payload = {};
15156
+ if (typeof rawMax === "string") {
15157
+ const n = Number(rawMax);
15158
+ if (Number.isInteger(n) && n > 0) payload.maxSessions = n;
15159
+ }
15160
+ if (typeof rawOffset === "string") {
15161
+ const n = Number(rawOffset);
15162
+ if (Number.isInteger(n) && n >= 0) payload.offset = n;
15163
+ }
14631
15164
  return {
14632
15165
  status_code: 200,
14633
15166
  body: await sdk.trigger({
14634
15167
  function_id: "mem::export",
14635
- payload: {}
15168
+ payload
14636
15169
  })
14637
15170
  };
14638
15171
  });
@@ -14858,6 +15391,63 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14858
15391
  http_method: "POST"
14859
15392
  }
14860
15393
  });
15394
+ sdk.registerFunction("api::graph-build", async (req) => {
15395
+ const authErr = checkAuth(req, secret);
15396
+ if (authErr) return authErr;
15397
+ const batchSize = Math.max(1, Math.min(100, Number(req.body?.batchSize) || 25));
15398
+ try {
15399
+ const sessions = await kv.list(KV.sessions);
15400
+ let totalNodes = 0;
15401
+ let totalEdges = 0;
15402
+ let batchesRun = 0;
15403
+ for (const session of sessions) {
15404
+ const sid = session?.id;
15405
+ if (typeof sid !== "string" || sid.length === 0) continue;
15406
+ const compressed = (await kv.list(KV.observations(sid))).filter((o) => o && typeof o.title === "string" && o.title.length > 0);
15407
+ if (compressed.length === 0) continue;
15408
+ for (let i = 0; i < compressed.length; i += batchSize) {
15409
+ const batch = compressed.slice(i, i + batchSize);
15410
+ try {
15411
+ const result = await sdk.trigger({
15412
+ function_id: "mem::graph-extract",
15413
+ payload: { observations: batch }
15414
+ });
15415
+ if (result?.success) {
15416
+ totalNodes += Number(result.nodesAdded) || 0;
15417
+ totalEdges += Number(result.edgesAdded) || 0;
15418
+ }
15419
+ batchesRun++;
15420
+ } catch (err) {
15421
+ logger.warn("graph-build batch failed", {
15422
+ sessionId: sid,
15423
+ batchIndex: Math.floor(i / batchSize),
15424
+ error: err instanceof Error ? err.message : String(err)
15425
+ });
15426
+ }
15427
+ }
15428
+ }
15429
+ return {
15430
+ status_code: 200,
15431
+ body: {
15432
+ success: true,
15433
+ sessions: sessions.length,
15434
+ batches: batchesRun,
15435
+ nodes: totalNodes,
15436
+ edges: totalEdges
15437
+ }
15438
+ };
15439
+ } catch {
15440
+ return graphDisabledResponse();
15441
+ }
15442
+ });
15443
+ sdk.registerTrigger({
15444
+ type: "http",
15445
+ function_id: "api::graph-build",
15446
+ config: {
15447
+ api_path: "/agentmemory/graph/build",
15448
+ http_method: "POST"
15449
+ }
15450
+ });
14861
15451
  sdk.registerFunction("api::consolidate-pipeline", async (req) => {
14862
15452
  const authErr = checkAuth(req, secret);
14863
15453
  if (authErr) return authErr;
@@ -15118,9 +15708,35 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15118
15708
  const authErr = checkAuth(req, secret);
15119
15709
  if (authErr) return authErr;
15120
15710
  const memories = await kv.list(KV.memories);
15711
+ const latest = req.query_params?.["latest"] === "true";
15712
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
15713
+ const wildcardAgent = normalizedAgentId === "*";
15714
+ const explicitAgentId = normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0;
15715
+ const includeOrphans = req.query_params?.["includeOrphans"] === "true";
15716
+ const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
15717
+ let filtered = latest ? memories.filter((m) => m.isLatest) : memories;
15718
+ if (filterAgentId) filtered = filtered.filter((m) => m.agentId === filterAgentId || includeOrphans && m.agentId === void 0);
15719
+ if (req.query_params?.["count"] === "true") return {
15720
+ status_code: 200,
15721
+ body: {
15722
+ total: filtered.length,
15723
+ latestCount: filtered.filter((m) => m.isLatest).length
15724
+ }
15725
+ };
15726
+ const rawLimit = req.query_params?.["limit"];
15727
+ const rawOffset = req.query_params?.["offset"];
15728
+ const parsedLimit = typeof rawLimit === "string" ? Number(rawLimit) : NaN;
15729
+ const parsedOffset = typeof rawOffset === "string" ? Number(rawOffset) : NaN;
15730
+ const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 5e3) : void 0;
15731
+ const offset = Number.isInteger(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
15121
15732
  return {
15122
15733
  status_code: 200,
15123
- body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
15734
+ body: {
15735
+ memories: limit !== void 0 ? filtered.slice(offset, offset + limit) : filtered,
15736
+ total: filtered.length,
15737
+ offset,
15738
+ limit: limit ?? null
15739
+ }
15124
15740
  };
15125
15741
  });
15126
15742
  sdk.registerTrigger({
@@ -15289,6 +15905,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15289
15905
  sdk.registerFunction("api::slot-list", async (req) => {
15290
15906
  const authErr = checkAuth(req, secret);
15291
15907
  if (authErr) return authErr;
15908
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15292
15909
  return {
15293
15910
  status_code: 200,
15294
15911
  body: await sdk.trigger({
@@ -15308,6 +15925,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15308
15925
  sdk.registerFunction("api::slot-get", async (req) => {
15309
15926
  const authErr = checkAuth(req, secret);
15310
15927
  if (authErr) return authErr;
15928
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15311
15929
  const label = asNonEmptyString$1(req.query_params?.["label"]);
15312
15930
  if (!label) return {
15313
15931
  status_code: 400,
@@ -15338,6 +15956,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15338
15956
  sdk.registerFunction("api::slot-create", async (req) => {
15339
15957
  const authErr = checkAuth(req, secret);
15340
15958
  if (authErr) return authErr;
15959
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15341
15960
  const body = req.body ?? {};
15342
15961
  const label = asNonEmptyString$1(body["label"]);
15343
15962
  if (!label) return {
@@ -15400,6 +16019,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15400
16019
  sdk.registerFunction("api::slot-append", async (req) => {
15401
16020
  const authErr = checkAuth(req, secret);
15402
16021
  if (authErr) return authErr;
16022
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15403
16023
  const body = req.body ?? {};
15404
16024
  const label = asNonEmptyString$1(body["label"]);
15405
16025
  const text = typeof body["text"] === "string" ? body["text"] : null;
@@ -15439,6 +16059,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15439
16059
  sdk.registerFunction("api::slot-replace", async (req) => {
15440
16060
  const authErr = checkAuth(req, secret);
15441
16061
  if (authErr) return authErr;
16062
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15442
16063
  const body = req.body ?? {};
15443
16064
  const label = asNonEmptyString$1(body["label"]);
15444
16065
  const content = body["content"];
@@ -15478,6 +16099,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15478
16099
  sdk.registerFunction("api::slot-delete", async (req) => {
15479
16100
  const authErr = checkAuth(req, secret);
15480
16101
  if (authErr) return authErr;
16102
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15481
16103
  const label = asNonEmptyString$1(req.query_params?.["label"]);
15482
16104
  if (!label) return {
15483
16105
  status_code: 400,
@@ -15508,6 +16130,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15508
16130
  sdk.registerFunction("api::slot-reflect", async (req) => {
15509
16131
  const authErr = checkAuth(req, secret);
15510
16132
  if (authErr) return authErr;
16133
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16134
+ if (!isReflectEnabled()) return reflectDisabledResponse();
15511
16135
  const body = req.body ?? {};
15512
16136
  const sessionId = asNonEmptyString$1(body["sessionId"]);
15513
16137
  if (!sessionId) return {
@@ -17210,13 +17834,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
17210
17834
  const type = args.type || "fact";
17211
17835
  const concepts = typeof args.concepts === "string" ? args.concepts.split(",").map((c) => c.trim()).filter(Boolean) : [];
17212
17836
  const files = typeof args.files === "string" ? args.files.split(",").map((f) => f.trim()).filter(Boolean) : [];
17837
+ const project = typeof args.project === "string" && args.project.trim().length > 0 ? args.project.trim() : void 0;
17213
17838
  const result = await sdk.trigger({
17214
17839
  function_id: "mem::remember",
17215
17840
  payload: {
17216
17841
  content: args.content,
17217
17842
  type,
17218
17843
  concepts,
17219
- files
17844
+ files,
17845
+ ...project !== void 0 && { project }
17220
17846
  }
17221
17847
  });
17222
17848
  return {
@@ -18733,201 +19359,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
18733
19359
  });
18734
19360
  }
18735
19361
 
18736
- //#endregion
18737
- //#region src/viewer/server.ts
18738
- function loadViewerFavicon() {
18739
- const base = dirname(fileURLToPath(import.meta.url));
18740
- const candidates = [
18741
- join(base, "..", "src", "viewer", "favicon.svg"),
18742
- join(base, "..", "viewer", "favicon.svg"),
18743
- join(base, "viewer", "favicon.svg")
18744
- ];
18745
- for (const path of candidates) try {
18746
- return readFileSync(path);
18747
- } catch {}
18748
- return null;
18749
- }
18750
- 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());
18751
- const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
18752
- function buildAllowedHosts(origins, listenPort) {
18753
- const hosts = /* @__PURE__ */ new Set();
18754
- for (const o of origins) try {
18755
- const parsed = new URL(o);
18756
- if (parsed.host) hosts.add(parsed.host.toLowerCase());
18757
- } catch {}
18758
- hosts.add(`localhost:${listenPort}`);
18759
- hosts.add(`127.0.0.1:${listenPort}`);
18760
- hosts.add(`[::1]:${listenPort}`);
18761
- for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
18762
- return hosts;
18763
- }
18764
- function isHostAllowed(headerHost, allowed) {
18765
- if (typeof headerHost !== "string") return false;
18766
- const lower = headerHost.toLowerCase().trim();
18767
- if (!lower) return false;
18768
- return allowed.has(lower);
18769
- }
18770
- function corsHeaders(req) {
18771
- const origin = req.headers.origin || "";
18772
- const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
18773
- return {
18774
- "Access-Control-Allow-Origin": allowed,
18775
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
18776
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
18777
- Vary: "Origin"
18778
- };
18779
- }
18780
- function json(res, status, data, req) {
18781
- const body = JSON.stringify(data);
18782
- const cors = req ? corsHeaders(req) : {
18783
- "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
18784
- Vary: "Origin"
18785
- };
18786
- res.writeHead(status, {
18787
- ...cors,
18788
- "Content-Type": "application/json"
18789
- });
18790
- res.end(body);
18791
- }
18792
- function readBody(req) {
18793
- return new Promise((resolve, reject) => {
18794
- let data = "";
18795
- let size = 0;
18796
- req.on("data", (chunk) => {
18797
- size += chunk.length;
18798
- if (size > 1e6) {
18799
- req.destroy();
18800
- reject(/* @__PURE__ */ new Error("too large"));
18801
- return;
18802
- }
18803
- data += chunk.toString();
18804
- });
18805
- req.on("end", () => resolve(data));
18806
- req.on("error", reject);
18807
- });
18808
- }
18809
- const MAX_VIEWER_PORT_RETRIES = 10;
18810
- function startViewerServer(port, _kv, _sdk, secret, restPort) {
18811
- const resolvedRestPort = restPort ?? port - 2;
18812
- const requestedPort = port;
18813
- let allowedHosts = null;
18814
- const server = createServer(async (req, res) => {
18815
- if (!allowedHosts) {
18816
- const addr = server.address();
18817
- allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
18818
- }
18819
- if (!isHostAllowed(req.headers.host, allowedHosts)) {
18820
- res.writeHead(403, { "Content-Type": "text/plain" });
18821
- res.end("forbidden host");
18822
- return;
18823
- }
18824
- const raw = req.url || "/";
18825
- const qIdx = raw.indexOf("?");
18826
- const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
18827
- const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
18828
- const method = req.method || "GET";
18829
- if (method === "OPTIONS") {
18830
- res.writeHead(204, {
18831
- ...corsHeaders(req),
18832
- "Access-Control-Max-Age": "86400"
18833
- });
18834
- res.end();
18835
- return;
18836
- }
18837
- if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
18838
- const rendered = renderViewerDocument();
18839
- if (rendered.found) {
18840
- res.writeHead(200, {
18841
- "Content-Type": "text/html; charset=utf-8",
18842
- "Content-Security-Policy": rendered.csp,
18843
- "Cache-Control": "no-cache"
18844
- });
18845
- res.end(rendered.html);
18846
- return;
18847
- }
18848
- res.writeHead(404, { "Content-Type": "text/plain" });
18849
- res.end("viewer not found");
18850
- return;
18851
- }
18852
- if (method === "GET" && pathname === "/favicon.svg") {
18853
- const favicon = loadViewerFavicon();
18854
- if (favicon) {
18855
- res.writeHead(200, {
18856
- "Content-Type": "image/svg+xml",
18857
- "Cache-Control": "public, max-age=3600"
18858
- });
18859
- res.end(favicon);
18860
- return;
18861
- }
18862
- res.writeHead(404, { "Content-Type": "text/plain" });
18863
- res.end("favicon not found");
18864
- return;
18865
- }
18866
- try {
18867
- await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
18868
- } catch (err) {
18869
- console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
18870
- json(res, 502, { error: "upstream error" }, req);
18871
- }
18872
- });
18873
- let attempt = 0;
18874
- let currentPort = requestedPort;
18875
- const tryListen = () => {
18876
- server.listen(currentPort, "127.0.0.1");
18877
- };
18878
- server.on("listening", () => {
18879
- if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
18880
- else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
18881
- });
18882
- server.on("error", (err) => {
18883
- if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
18884
- attempt++;
18885
- currentPort = requestedPort + attempt;
18886
- setImmediate(tryListen);
18887
- return;
18888
- }
18889
- if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
18890
- else console.error(`[agentmemory] Viewer error:`, err.message);
18891
- });
18892
- tryListen();
18893
- return server;
18894
- }
18895
- async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
18896
- const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
18897
- const headers = {};
18898
- if (secret) headers["Authorization"] = `Bearer ${secret}`;
18899
- const ct = req.headers["content-type"];
18900
- if (ct) headers["Content-Type"] = ct;
18901
- let body;
18902
- if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
18903
- const controller = new AbortController();
18904
- const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
18905
- let upstream;
18906
- try {
18907
- upstream = await fetch(upstreamUrl, {
18908
- method,
18909
- headers,
18910
- body: body || void 0,
18911
- signal: controller.signal
18912
- });
18913
- clearTimeout(fetchTimeout);
18914
- } catch (err) {
18915
- clearTimeout(fetchTimeout);
18916
- if (err instanceof Error && err.name === "AbortError") {
18917
- json(res, 504, { error: "upstream timeout" }, req);
18918
- return;
18919
- }
18920
- throw err;
18921
- }
18922
- const cors = corsHeaders(req);
18923
- const responseBody = await upstream.text();
18924
- const responseHeaders = { ...cors };
18925
- const upstreamCt = upstream.headers.get("content-type");
18926
- if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
18927
- res.writeHead(upstream.status, responseHeaders);
18928
- res.end(responseBody);
18929
- }
18930
-
18931
19362
  //#endregion
18932
19363
  //#region src/eval/metrics-store.ts
18933
19364
  var MetricsStore = class {
@@ -19065,6 +19496,21 @@ function initMetrics(getMeter) {
19065
19496
 
19066
19497
  //#endregion
19067
19498
  //#region src/index.ts
19499
+ function workerPidfilePath() {
19500
+ return join(homedir(), ".agentmemory", "worker.pid");
19501
+ }
19502
+ function writeWorkerPidfile() {
19503
+ try {
19504
+ const p = workerPidfilePath();
19505
+ mkdirSync(dirname(p), { recursive: true });
19506
+ writeFileSync(p, `${process.pid}\n`, { encoding: "utf-8" });
19507
+ } catch {}
19508
+ }
19509
+ function clearWorkerPidfile() {
19510
+ try {
19511
+ unlinkSync(workerPidfilePath());
19512
+ } catch {}
19513
+ }
19068
19514
  function hasGetMeter(sdk) {
19069
19515
  return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
19070
19516
  }
@@ -19105,6 +19551,7 @@ async function main() {
19105
19551
  framework: "iii-sdk"
19106
19552
  }
19107
19553
  });
19554
+ writeWorkerPidfile();
19108
19555
  const kv = new StateKV(sdk);
19109
19556
  const secret = getEnvVar("AGENTMEMORY_SECRET");
19110
19557
  const metricsStore = new MetricsStore(kv);
@@ -19200,6 +19647,7 @@ async function main() {
19200
19647
  registerMcpEndpoints(sdk, kv, secret);
19201
19648
  const healthMonitor = registerHealthMonitor(sdk, kv);
19202
19649
  const indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
19650
+ setIndexPersistence(indexPersistence);
19203
19651
  const loaded = await indexPersistence.load().catch((err) => {
19204
19652
  console.warn(`[agentmemory] Failed to load persisted index:`, err);
19205
19653
  return null;
@@ -19241,7 +19689,7 @@ async function main() {
19241
19689
  if (bm25Index.has(memory.id)) continue;
19242
19690
  bm25Index.add({
19243
19691
  id: memory.id,
19244
- sessionId: memory.sessionIds[0] ?? "memory",
19692
+ sessionId: memory.sessionIds?.[0] ?? "memory",
19245
19693
  timestamp: memory.createdAt,
19246
19694
  type: "decision",
19247
19695
  title: memory.title,
@@ -19261,7 +19709,7 @@ async function main() {
19261
19709
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
19262
19710
  }
19263
19711
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
19264
- bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
19712
+ bootLog(`REST API: 125 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
19265
19713
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
19266
19714
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
19267
19715
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
@@ -19317,6 +19765,7 @@ async function main() {
19317
19765
  console.warn(`[agentmemory] Failed to save index on shutdown:`, err);
19318
19766
  });
19319
19767
  await sdk.shutdown();
19768
+ clearWorkerPidfile();
19320
19769
  process.exit(0);
19321
19770
  };
19322
19771
  process.on("SIGINT", shutdown);
@@ -19329,4 +19778,4 @@ main().catch((err) => {
19329
19778
 
19330
19779
  //#endregion
19331
19780
  export { };
19332
- //# sourceMappingURL=src-D5arboxc.mjs.map
19781
+ //# sourceMappingURL=src-DvS3bhMe.mjs.map