@agentmemory/agentmemory 0.9.22 → 0.9.24

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 (84) hide show
  1. package/AGENTS.md +7 -2
  2. package/README.md +144 -32
  3. package/dist/cli.d.mts.map +1 -1
  4. package/dist/cli.mjs +42 -25
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{connect-BQQXpyDS.mjs → connect-Cf9bmBqO.mjs} +290 -33
  7. package/dist/connect-Cf9bmBqO.mjs.map +1 -0
  8. package/dist/hooks/notification.mjs +46 -21
  9. package/dist/hooks/notification.mjs.map +1 -1
  10. package/dist/hooks/post-tool-failure.mjs +47 -21
  11. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  12. package/dist/hooks/post-tool-use.mjs +57 -22
  13. package/dist/hooks/post-tool-use.mjs.map +1 -1
  14. package/dist/hooks/pre-compact.mjs +26 -2
  15. package/dist/hooks/pre-compact.mjs.map +1 -1
  16. package/dist/hooks/pre-tool-use.mjs +19 -12
  17. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  18. package/dist/hooks/prompt-submit.mjs +39 -16
  19. package/dist/hooks/prompt-submit.mjs.map +1 -1
  20. package/dist/hooks/session-end.mjs +26 -33
  21. package/dist/hooks/session-end.mjs.map +1 -1
  22. package/dist/hooks/session-start.mjs +28 -3
  23. package/dist/hooks/session-start.mjs.map +1 -1
  24. package/dist/hooks/stop.mjs +14 -17
  25. package/dist/hooks/stop.mjs.map +1 -1
  26. package/dist/hooks/subagent-start.mjs +31 -4
  27. package/dist/hooks/subagent-start.mjs.map +1 -1
  28. package/dist/hooks/subagent-stop.mjs +45 -20
  29. package/dist/hooks/subagent-stop.mjs.map +1 -1
  30. package/dist/hooks/task-completed.mjs +44 -21
  31. package/dist/hooks/task-completed.mjs.map +1 -1
  32. package/dist/iii-config.docker.yaml +3 -2
  33. package/dist/iii-config.yaml +11 -2
  34. package/dist/index.mjs +336 -57
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/{src-gpTAJuBy.mjs → src-B8J9Exum.mjs} +323 -58
  37. package/dist/src-B8J9Exum.mjs.map +1 -0
  38. package/dist/{standalone-C4i7ktpn.mjs → standalone-CPfsVTBA.mjs} +92 -11
  39. package/dist/standalone-CPfsVTBA.mjs.map +1 -0
  40. package/dist/standalone.mjs +94 -9
  41. package/dist/standalone.mjs.map +1 -1
  42. package/dist/{tools-registry-B7Y6nJsr.mjs → tools-registry-DJizX9Az.mjs} +16 -2
  43. package/dist/tools-registry-DJizX9Az.mjs.map +1 -0
  44. package/dist/version-BWEBnKAp.mjs +6 -0
  45. package/dist/version-BWEBnKAp.mjs.map +1 -0
  46. package/dist/viewer/index.html +9 -2
  47. package/iii-config.docker.yaml +3 -2
  48. package/iii-config.yaml +11 -2
  49. package/package.json +1 -1
  50. package/plugin/.claude-plugin/plugin.json +2 -2
  51. package/plugin/.codex-plugin/plugin.json +2 -2
  52. package/plugin/.mcp.copilot.json +15 -0
  53. package/plugin/hooks/hooks.copilot.json +72 -0
  54. package/plugin/plugin.json +15 -0
  55. package/plugin/scripts/notification.mjs +46 -21
  56. package/plugin/scripts/notification.mjs.map +1 -1
  57. package/plugin/scripts/post-tool-failure.mjs +47 -21
  58. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  59. package/plugin/scripts/post-tool-use.mjs +57 -22
  60. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  61. package/plugin/scripts/pre-compact.mjs +26 -2
  62. package/plugin/scripts/pre-compact.mjs.map +1 -1
  63. package/plugin/scripts/pre-tool-use.mjs +19 -12
  64. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  65. package/plugin/scripts/prompt-submit.mjs +39 -16
  66. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  67. package/plugin/scripts/session-end.mjs +26 -33
  68. package/plugin/scripts/session-end.mjs.map +1 -1
  69. package/plugin/scripts/session-start.mjs +28 -3
  70. package/plugin/scripts/session-start.mjs.map +1 -1
  71. package/plugin/scripts/stop.mjs +14 -17
  72. package/plugin/scripts/stop.mjs.map +1 -1
  73. package/plugin/scripts/subagent-start.mjs +31 -4
  74. package/plugin/scripts/subagent-start.mjs.map +1 -1
  75. package/plugin/scripts/subagent-stop.mjs +45 -20
  76. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  77. package/plugin/scripts/task-completed.mjs +44 -21
  78. package/plugin/scripts/task-completed.mjs.map +1 -1
  79. package/dist/connect-BQQXpyDS.mjs.map +0 -1
  80. package/dist/src-gpTAJuBy.mjs.map +0 -1
  81. package/dist/standalone-C4i7ktpn.mjs.map +0 -1
  82. package/dist/tools-registry-B7Y6nJsr.mjs.map +0 -1
  83. package/dist/version-DvQMNbEH.mjs +0 -6
  84. package/dist/version-DvQMNbEH.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -233,7 +233,17 @@ function isGraphExtractionEnabled() {
233
233
  return getMergedEnv()["GRAPH_EXTRACTION_ENABLED"] === "true";
234
234
  }
235
235
  function isConsolidationEnabled() {
236
- return getMergedEnv()["CONSOLIDATION_ENABLED"] === "true";
236
+ const env = getMergedEnv();
237
+ const explicit = env["CONSOLIDATION_ENABLED"];
238
+ if (explicit === "false" || explicit === "0") return false;
239
+ if (explicit === "true" || explicit === "1") return true;
240
+ return hasLLMProviderConfigured(env);
241
+ }
242
+ function hasLLMProviderConfigured(env) {
243
+ const provider = (env["AGENTMEMORY_PROVIDER"] || "").toLowerCase();
244
+ if (provider === "noop") return false;
245
+ const openaiKeyForLlm = env["OPENAI_API_KEY"] && (env["OPENAI_API_KEY_FOR_LLM"] || "").toLowerCase() !== "false";
246
+ return Boolean(env["ANTHROPIC_API_KEY"] || openaiKeyForLlm || env["OPENROUTER_API_KEY"] || env["GEMINI_API_KEY"] || env["GOOGLE_API_KEY"] || env["MINIMAX_API_KEY"] || env["OPENAI_BASE_URL"] || provider === "agent-sdk");
237
247
  }
238
248
  function isAutoCompressEnabled() {
239
249
  return getMergedEnv()["AGENTMEMORY_AUTO_COMPRESS"] === "true";
@@ -886,16 +896,30 @@ function resolveDimensions(model, override) {
886
896
  * `api-key` header instead of `Authorization: Bearer`.
887
897
  *
888
898
  * Required env vars:
889
- * OPENAI_API_KEY — API key
899
+ * OPENAI_API_KEY — API key (fallback for OPENAI_EMBEDDING_API_KEY)
890
900
  *
891
901
  * Optional:
892
- * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
893
- * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
894
- * OPENAI_API_VERSION Azure api-version query param (default: 2024-08-01-preview)
895
- * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
896
- * OPENAI_EMBEDDING_DIMENSIONS override reported dimensions (required for
897
- * custom / self-hosted models not in the
898
- * MODEL_DIMENSIONS table above)
902
+ * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
903
+ * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
904
+ * OPENAI_EMBEDDING_BASE_URL embedding-specific base URL override (defaults
905
+ * to OPENAI_BASE_URL). Lets operators run
906
+ * embeddings on a separate endpoint from chat —
907
+ * e.g. local Ollama / LM Studio / llama.cpp /
908
+ * vLLM at http://localhost:1234 for unlimited
909
+ * free embeddings, while keeping chat
910
+ * completions on a rate-limited but high-quality
911
+ * hosted provider. Azure detection runs on
912
+ * whichever URL ends up selected.
913
+ * OPENAI_EMBEDDING_API_KEY — separate API key for the embedding endpoint
914
+ * (defaults to OPENAI_API_KEY). Useful when the
915
+ * embedding endpoint requires a different key
916
+ * or no key at all (set to e.g. "local" for
917
+ * endpoints that ignore Authorization).
918
+ * OPENAI_API_VERSION — Azure api-version query param (default: 2024-08-01-preview)
919
+ * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
920
+ * OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
921
+ * custom / self-hosted models not in the
922
+ * MODEL_DIMENSIONS table above)
899
923
  */
900
924
  var OpenAIEmbeddingProvider = class {
901
925
  name = "openai";
@@ -906,9 +930,9 @@ var OpenAIEmbeddingProvider = class {
906
930
  isAzure;
907
931
  azureApiVersion;
908
932
  constructor(apiKey) {
909
- this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
910
- if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
911
- this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_BASE_URL"));
933
+ this.apiKey = apiKey || getEnvVar("OPENAI_EMBEDDING_API_KEY") || getEnvVar("OPENAI_API_KEY") || "";
934
+ if (!this.apiKey) throw new Error("API key is required (via constructor, OPENAI_EMBEDDING_API_KEY, or OPENAI_API_KEY)");
935
+ this.baseUrl = normalizeBaseUrl(getEnvVar("OPENAI_EMBEDDING_BASE_URL") || getEnvVar("OPENAI_BASE_URL"));
912
936
  this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
913
937
  this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
914
938
  this.isAzure = detectAzure(this.baseUrl);
@@ -1355,10 +1379,11 @@ function jaccardSimilarity(a, b) {
1355
1379
  //#endregion
1356
1380
  //#region src/state/vector-index.ts
1357
1381
  function float32ToBase64(arr) {
1358
- return Buffer.from(arr.buffer).toString("base64");
1382
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString("base64");
1359
1383
  }
1360
1384
  function base64ToFloat32(b64) {
1361
- return new Float32Array(Buffer.from(b64, "base64").buffer);
1385
+ const buf = Buffer.from(b64, "base64");
1386
+ return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / Float32Array.BYTES_PER_ELEMENT);
1362
1387
  }
1363
1388
  function cosineSimilarity(a, b) {
1364
1389
  if (a.length !== b.length) return 0;
@@ -1479,7 +1504,7 @@ var VectorIndex = class VectorIndex {
1479
1504
  function memoryToObservation(memory) {
1480
1505
  return {
1481
1506
  id: memory.id,
1482
- sessionId: memory.sessionIds[0] ?? "memory",
1507
+ sessionId: memory.sessionIds?.[0] ?? "memory",
1483
1508
  timestamp: memory.createdAt,
1484
1509
  type: "decision",
1485
1510
  title: memory.title,
@@ -3151,7 +3176,7 @@ async function rebuildIndex(kv) {
3151
3176
  idx.add(memoryToObservation(memory));
3152
3177
  await enqueue({
3153
3178
  id: memory.id,
3154
- sessionId: memory.sessionIds[0] ?? "memory",
3179
+ sessionId: memory.sessionIds?.[0] ?? "memory",
3155
3180
  text: memory.title + " " + memory.content,
3156
3181
  context: {
3157
3182
  kind: "memory",
@@ -3210,8 +3235,8 @@ function registerSearchFunction(sdk, kv) {
3210
3235
  if (!Number.isInteger(data.limit) || data.limit < 1) throw new Error("mem::search: limit must be a positive integer");
3211
3236
  effectiveLimit = Math.min(data.limit, MAX_LIMIT);
3212
3237
  }
3213
- const projectFilter = typeof data.project === "string" && data.project.length > 0 ? data.project : void 0;
3214
- const cwdFilter = typeof data.cwd === "string" && data.cwd.length > 0 ? data.cwd : void 0;
3238
+ const projectFilter = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
3239
+ const cwdFilter = typeof data.cwd === "string" && data.cwd.trim().length > 0 ? data.cwd.trim() : void 0;
3215
3240
  const format = typeof data.format === "string" ? data.format : "full";
3216
3241
  if (![
3217
3242
  "full",
@@ -3237,14 +3262,25 @@ function registerSearchFunction(sdk, kv) {
3237
3262
  sessionCache.set(sessionId, s ?? null);
3238
3263
  return s ?? null;
3239
3264
  };
3265
+ const memoryProjectCache = /* @__PURE__ */ new Map();
3266
+ const loadMemoryProject = async (obsId) => {
3267
+ if (memoryProjectCache.has(obsId)) return memoryProjectCache.get(obsId);
3268
+ const proj = (await kv.get(KV.memories, obsId).catch(() => null))?.project ?? null;
3269
+ memoryProjectCache.set(obsId, proj);
3270
+ return proj;
3271
+ };
3240
3272
  const candidates = [];
3241
3273
  for (const r of results) {
3242
3274
  if (candidates.length >= effectiveLimit) break;
3243
3275
  if (filtering) {
3244
3276
  const s = await loadSession(r.sessionId);
3245
- if (!s) continue;
3246
- if (projectFilter && s.project !== projectFilter) continue;
3247
- if (cwdFilter && s.cwd !== cwdFilter) continue;
3277
+ if (s) {
3278
+ if (projectFilter && s.project !== projectFilter) continue;
3279
+ if (cwdFilter && s.cwd !== cwdFilter) continue;
3280
+ } else if (projectFilter) {
3281
+ const memProject = await loadMemoryProject(r.obsId);
3282
+ if (memProject !== null && memProject !== projectFilter) continue;
3283
+ }
3248
3284
  }
3249
3285
  candidates.push(r);
3250
3286
  }
@@ -3996,10 +4032,10 @@ const DEFAULT_SLOTS = [
3996
4032
  }
3997
4033
  ];
3998
4034
  function isSlotsEnabled() {
3999
- return process.env["AGENTMEMORY_SLOTS"] === "true";
4035
+ return getEnvVar("AGENTMEMORY_SLOTS") === "true";
4000
4036
  }
4001
4037
  function isReflectEnabled() {
4002
- return process.env["AGENTMEMORY_REFLECT"] === "true";
4038
+ return getEnvVar("AGENTMEMORY_REFLECT") === "true";
4003
4039
  }
4004
4040
  function scopeKv(scope) {
4005
4041
  return scope === "global" ? KV.globalSlots : KV.slots;
@@ -5264,8 +5300,78 @@ function isAllowedPath(dbPath) {
5264
5300
  const resolved = resolve(dbPath);
5265
5301
  return ALLOWED_DIRS.some((dir) => resolved.startsWith(dir + "/"));
5266
5302
  }
5303
+ async function inferMemoryProjects(kv, dryRun = false) {
5304
+ const memories = await kv.list(KV.memories);
5305
+ const sessionCache = /* @__PURE__ */ new Map();
5306
+ const loadSession = async (sid) => {
5307
+ if (sessionCache.has(sid)) return sessionCache.get(sid);
5308
+ const s = await kv.get(KV.sessions, sid).catch(() => null);
5309
+ sessionCache.set(sid, s);
5310
+ return s;
5311
+ };
5312
+ let updated = 0;
5313
+ let skipped = 0;
5314
+ let ambiguous = 0;
5315
+ for (const memory of memories) {
5316
+ if (memory.project) {
5317
+ skipped++;
5318
+ continue;
5319
+ }
5320
+ const sessionIds = memory.sessionIds ?? [];
5321
+ if (sessionIds.length === 0) {
5322
+ ambiguous++;
5323
+ continue;
5324
+ }
5325
+ const projects = [];
5326
+ for (const sid of sessionIds) {
5327
+ const session = await loadSession(sid);
5328
+ if (session?.project) projects.push(session.project);
5329
+ }
5330
+ if (projects.length === 0) {
5331
+ ambiguous++;
5332
+ continue;
5333
+ }
5334
+ const freq = /* @__PURE__ */ new Map();
5335
+ for (const p of projects) freq.set(p, (freq.get(p) ?? 0) + 1);
5336
+ const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
5337
+ const [topProject, topCount] = sorted[0];
5338
+ if (topCount <= projects.length / 2 && sorted.length > 1) {
5339
+ ambiguous++;
5340
+ continue;
5341
+ }
5342
+ if (!dryRun) {
5343
+ memory.project = topProject;
5344
+ await kv.set(KV.memories, memory.id, memory);
5345
+ }
5346
+ updated++;
5347
+ }
5348
+ logger.info("inferMemoryProjects complete", {
5349
+ updated,
5350
+ skipped,
5351
+ ambiguous,
5352
+ dryRun
5353
+ });
5354
+ return {
5355
+ updated,
5356
+ skipped,
5357
+ ambiguous
5358
+ };
5359
+ }
5267
5360
  function registerMigrateFunction(sdk, kv) {
5268
5361
  sdk.registerFunction("mem::migrate", async (data) => {
5362
+ if (data.step === "infer-memory-projects") {
5363
+ const dryRun = data.dryRun ?? false;
5364
+ logger.info("Migration step: infer-memory-projects", { dryRun });
5365
+ return {
5366
+ success: true,
5367
+ step: "infer-memory-projects",
5368
+ ...await inferMemoryProjects(kv, dryRun)
5369
+ };
5370
+ }
5371
+ if (!data.dbPath) return {
5372
+ success: false,
5373
+ error: "Either step or dbPath is required"
5374
+ };
5269
5375
  logger.info("Migration started", { dbPath: data.dbPath });
5270
5376
  if (!isAllowedPath(data.dbPath)) return {
5271
5377
  success: false,
@@ -5541,9 +5647,10 @@ function registerConsolidateFunction(sdk, kv, provider) {
5541
5647
  llmCallCount++;
5542
5648
  const parsed = parseMemoryXml(response, sessionIds);
5543
5649
  if (!parsed) continue;
5544
- const existingMatch = existingMemories.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase());
5545
5650
  const now = (/* @__PURE__ */ new Date()).toISOString();
5546
5651
  const obsIds = [...new Set(top.map((o) => o.id))];
5652
+ const scopedProject = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
5653
+ const existingMatch = existingMemories.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase() && (!scopedProject || !m.project || m.project === scopedProject));
5547
5654
  if (existingMatch) {
5548
5655
  existingMatch.isLatest = false;
5549
5656
  await kv.set(KV.memories, existingMatch.id, existingMatch);
@@ -5560,7 +5667,8 @@ function registerConsolidateFunction(sdk, kv, provider) {
5560
5667
  parentId: existingMatch.id,
5561
5668
  supersedes: [existingMatch.id, ...existingMatch.supersedes || []],
5562
5669
  sourceObservationIds: obsIds,
5563
- isLatest: true
5670
+ isLatest: true,
5671
+ ...scopedProject !== void 0 && { project: scopedProject }
5564
5672
  };
5565
5673
  await kv.set(KV.memories, evolved.id, evolved);
5566
5674
  await recordAudit(kv, "evolve", "mem::consolidate", [evolved.id], {
@@ -5579,7 +5687,8 @@ function registerConsolidateFunction(sdk, kv, provider) {
5579
5687
  ...parsed,
5580
5688
  sourceObservationIds: obsIds,
5581
5689
  version: 1,
5582
- isLatest: true
5690
+ isLatest: true,
5691
+ ...scopedProject !== void 0 && { project: scopedProject }
5583
5692
  };
5584
5693
  await kv.set(KV.memories, memory.id, memory);
5585
5694
  await recordAudit(kv, "remember", "mem::consolidate", [memory.id], {
@@ -5720,6 +5829,7 @@ function registerRememberFunction(sdk, kv) {
5720
5829
  "fact"
5721
5830
  ]).has(data.type || "") ? data.type : "fact";
5722
5831
  const now = (/* @__PURE__ */ new Date()).toISOString();
5832
+ const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
5723
5833
  return withKeyedLock("mem:remember", async () => {
5724
5834
  const existingMemories = await kv.list(KV.memories);
5725
5835
  let supersededId;
@@ -5728,6 +5838,7 @@ function registerRememberFunction(sdk, kv) {
5728
5838
  const lowerContent = data.content.toLowerCase();
5729
5839
  for (const existing of existingMemories) {
5730
5840
  if (existing.isLatest === false) continue;
5841
+ if (project && existing.project && existing.project !== project) continue;
5731
5842
  if (jaccardSimilarity(lowerContent, existing.content.toLowerCase()) > .7) {
5732
5843
  supersededId = existing.id;
5733
5844
  supersededVersion = existing.version ?? 1;
@@ -5752,7 +5863,8 @@ function registerRememberFunction(sdk, kv) {
5752
5863
  supersedes: supersededId ? [supersededId] : [],
5753
5864
  sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
5754
5865
  isLatest: true,
5755
- ...callAgentId ? { agentId: callAgentId } : {}
5866
+ ...callAgentId ? { agentId: callAgentId } : {},
5867
+ ...project !== void 0 && { project }
5756
5868
  };
5757
5869
  if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
5758
5870
  if (supersededMemory) {
@@ -5768,7 +5880,7 @@ function registerRememberFunction(sdk, kv) {
5768
5880
  error: err instanceof Error ? err.message : String(err)
5769
5881
  });
5770
5882
  }
5771
- await vectorIndexAddGuarded(memory.id, memory.sessionIds[0] ?? "memory", memory.title + " " + memory.content, {
5883
+ await vectorIndexAddGuarded(memory.id, memory.sessionIds?.[0] ?? "memory", memory.title + " " + memory.content, {
5772
5884
  kind: "memory",
5773
5885
  logId: memory.id
5774
5886
  });
@@ -5779,7 +5891,8 @@ function registerRememberFunction(sdk, kv) {
5779
5891
  });
5780
5892
  logger.info("Memory saved", {
5781
5893
  memId: memory.id,
5782
- type: memory.type
5894
+ type: memory.type,
5895
+ project: memory.project
5783
5896
  });
5784
5897
  return {
5785
5898
  success: true,
@@ -6680,7 +6793,7 @@ function registerAutoForgetFunction(sdk, kv) {
6680
6793
 
6681
6794
  //#endregion
6682
6795
  //#region src/version.ts
6683
- const VERSION = "0.9.22";
6796
+ const VERSION = "0.9.24";
6684
6797
 
6685
6798
  //#endregion
6686
6799
  //#region src/functions/export-import.ts
@@ -6820,7 +6933,9 @@ function registerExportImportFunction(sdk, kv) {
6820
6933
  "0.9.19",
6821
6934
  "0.9.20",
6822
6935
  "0.9.21",
6823
- "0.9.22"
6936
+ "0.9.22",
6937
+ "0.9.23",
6938
+ "0.9.24"
6824
6939
  ]).has(importData.version)) return {
6825
6940
  success: false,
6826
6941
  error: `Unsupported export version: ${importData.version}`
@@ -6943,6 +7058,7 @@ function registerExportImportFunction(sdk, kv) {
6943
7058
  continue;
6944
7059
  }
6945
7060
  }
7061
+ if (!Array.isArray(memory.sessionIds)) memory.sessionIds = [];
6946
7062
  await kv.set(KV.memories, memory.id, memory);
6947
7063
  stats.memories++;
6948
7064
  }
@@ -7146,6 +7262,7 @@ function escapeXml(s) {
7146
7262
  }
7147
7263
  function registerEnrichFunction(sdk, kv) {
7148
7264
  sdk.registerFunction("mem::enrich", async (data) => {
7265
+ const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
7149
7266
  const parts = [];
7150
7267
  const fileContextPromise = sdk.trigger({
7151
7268
  function_id: "mem::file-context",
@@ -7159,10 +7276,11 @@ function registerEnrichFunction(sdk, kv) {
7159
7276
  function_id: "mem::search",
7160
7277
  payload: {
7161
7278
  query: searchQueries.join(" "),
7162
- limit: 5
7279
+ limit: 5,
7280
+ ...project !== void 0 && { project }
7163
7281
  }
7164
7282
  }).catch(() => ({ results: [] })) : Promise.resolve({ results: [] });
7165
- 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(() => []);
7283
+ 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(() => []);
7166
7284
  const [fileContext, searchResult, bugMemories] = await Promise.all([
7167
7285
  fileContextPromise,
7168
7286
  searchPromise,
@@ -7185,6 +7303,7 @@ function registerEnrichFunction(sdk, kv) {
7185
7303
  }
7186
7304
  logger.info("Enrichment completed", {
7187
7305
  sessionId: data.sessionId,
7306
+ project,
7188
7307
  fileCount: data.files.length,
7189
7308
  contextLength: context.length,
7190
7309
  truncated
@@ -7342,16 +7461,24 @@ function buildGraphExtractionPrompt(observations) {
7342
7461
 
7343
7462
  //#endregion
7344
7463
  //#region src/functions/graph.ts
7464
+ function parseAttrs(raw) {
7465
+ const attrs = {};
7466
+ const attrRegex = /([A-Za-z_][\w:-]*)="([^"]*)"/g;
7467
+ let m;
7468
+ while ((m = attrRegex.exec(raw)) !== null) attrs[m[1]] = m[2];
7469
+ return attrs;
7470
+ }
7345
7471
  function parseGraphXml(xml, observationIds) {
7346
7472
  const nodes = [];
7347
7473
  const edges = [];
7348
7474
  const now = (/* @__PURE__ */ new Date()).toISOString();
7349
- const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*?(?:\/>|>([\s\S]*?)<\/entity>)/g;
7350
- let match;
7351
- while ((match = entityRegex.exec(xml)) !== null) {
7352
- const type = match[1];
7353
- const name = match[2];
7354
- const propsBlock = match[3] ?? "";
7475
+ const entitySelfClose = /<entity\b([^>]*?)\/>/g;
7476
+ const entityWithBody = /<entity\b([^>]*[^/])>([\s\S]*?)<\/entity>/g;
7477
+ const addEntity = (rawAttrs, propsBlock = "") => {
7478
+ const attrs = parseAttrs(rawAttrs);
7479
+ const type = attrs["type"];
7480
+ const name = attrs["name"];
7481
+ if (!type || !name) return;
7355
7482
  const properties = {};
7356
7483
  const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
7357
7484
  let propMatch;
@@ -7364,17 +7491,23 @@ function parseGraphXml(xml, observationIds) {
7364
7491
  sourceObservationIds: observationIds,
7365
7492
  createdAt: now
7366
7493
  });
7367
- }
7368
- const relRegex = /<relationship\s+type="([^"]+)"\s+source="([^"]+)"\s+target="([^"]+)"\s+weight="([^"]+)"\s*\/>/g;
7494
+ };
7495
+ let match;
7496
+ while ((match = entitySelfClose.exec(xml)) !== null) addEntity(match[1]);
7497
+ while ((match = entityWithBody.exec(xml)) !== null) addEntity(match[1], match[2]);
7498
+ const relRegex = /<relationship\b([^>]*?)\/>/g;
7369
7499
  while ((match = relRegex.exec(xml)) !== null) {
7370
- const type = match[1];
7371
- const sourceName = match[2];
7372
- const targetName = match[3];
7373
- const parsedWeight = parseFloat(match[4]);
7374
- const weight = Number.isNaN(parsedWeight) ? .5 : parsedWeight;
7500
+ const attrs = parseAttrs(match[1]);
7501
+ const type = attrs["type"];
7502
+ const sourceName = attrs["source"];
7503
+ const targetName = attrs["target"];
7504
+ if (!type || !sourceName || !targetName) continue;
7505
+ const parsedWeight = parseFloat(attrs["weight"] ?? "");
7506
+ const weight = Number.isFinite(parsedWeight) ? parsedWeight : .5;
7375
7507
  const sourceNode = nodes.find((n) => n.name === sourceName);
7376
7508
  const targetNode = nodes.find((n) => n.name === targetName);
7377
- if (sourceNode && targetNode) edges.push({
7509
+ if (!sourceNode || !targetNode) continue;
7510
+ edges.push({
7378
7511
  id: generateId("ge"),
7379
7512
  type,
7380
7513
  sourceNodeId: sourceNode.id,
@@ -7588,7 +7721,7 @@ function registerConsolidationPipelineFunction(sdk, kv, provider) {
7588
7721
  if (!data?.force && !isConsolidationEnabled()) return {
7589
7722
  success: false,
7590
7723
  skipped: true,
7591
- reason: "CONSOLIDATION_ENABLED is not set to true"
7724
+ 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)"
7592
7725
  };
7593
7726
  const tier = data?.tier || "all";
7594
7727
  const decayDays = getConsolidationDecayDays();
@@ -10758,11 +10891,34 @@ function registerDiagnosticsFunction(sdk, kv) {
10758
10891
  });
10759
10892
  memoryIssues++;
10760
10893
  }
10894
+ const latestMemories = memories.filter((m) => m.isLatest);
10895
+ const unscopedCount = latestMemories.filter((m) => !m.project).length;
10896
+ if (unscopedCount === 0) checks.push({
10897
+ name: "memory-project-coverage",
10898
+ category: "memories",
10899
+ status: "pass",
10900
+ message: `All ${latestMemories.length} latest memories have a project scope`,
10901
+ fixable: false
10902
+ });
10903
+ else if (unscopedCount <= 10) checks.push({
10904
+ name: "memory-project-coverage",
10905
+ category: "memories",
10906
+ status: "warn",
10907
+ message: `${unscopedCount} of ${latestMemories.length} latest memories have no project scope — run POST /agentmemory/migrate {"step":"infer-memory-projects"} to backfill`,
10908
+ fixable: true
10909
+ });
10910
+ else checks.push({
10911
+ name: "memory-project-coverage",
10912
+ category: "memories",
10913
+ status: "fail",
10914
+ message: `${unscopedCount} of ${latestMemories.length} latest memories have no project scope — run POST /agentmemory/migrate {"step":"infer-memory-projects"} to backfill`,
10915
+ fixable: true
10916
+ });
10761
10917
  if (memoryIssues === 0) checks.push({
10762
10918
  name: "memories-ok",
10763
10919
  category: "memories",
10764
10920
  status: "pass",
10765
- message: `All ${memories.length} memories are consistent`,
10921
+ message: `All ${memories.length} memories are structurally consistent`,
10766
10922
  fixable: false
10767
10923
  });
10768
10924
  }
@@ -14535,6 +14691,22 @@ function consolidationDisabledResponse() {
14535
14691
  docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
14536
14692
  });
14537
14693
  }
14694
+ function slotsDisabledResponse() {
14695
+ return flagDisabledResponse({
14696
+ error: "Memory slots not enabled",
14697
+ flag: "AGENTMEMORY_SLOTS",
14698
+ enableHow: "Set AGENTMEMORY_SLOTS=true (in ~/.agentmemory/.env or the shell) and restart.",
14699
+ docsHref: "https://github.com/rohitg00/agentmemory#memory-slots"
14700
+ });
14701
+ }
14702
+ function reflectDisabledResponse() {
14703
+ return flagDisabledResponse({
14704
+ error: "Slot reflection not enabled",
14705
+ flag: "AGENTMEMORY_REFLECT",
14706
+ enableHow: "Set AGENTMEMORY_REFLECT=true (in ~/.agentmemory/.env or the shell) and restart. Requires AGENTMEMORY_SLOTS=true.",
14707
+ docsHref: "https://github.com/rohitg00/agentmemory#memory-slots"
14708
+ });
14709
+ }
14538
14710
  function asNonEmptyString$1(value) {
14539
14711
  if (typeof value !== "string") return null;
14540
14712
  const trimmed = value.trim();
@@ -14979,6 +15151,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14979
15151
  path: "status",
14980
15152
  value: "completed"
14981
15153
  }]);
15154
+ try {
15155
+ sdk.triggerVoid("event::session::stopped", { sessionId });
15156
+ } catch (err) {
15157
+ logger.warn("event::session::stopped triggerVoid failed", {
15158
+ sessionId,
15159
+ error: err instanceof Error ? err.message : String(err)
15160
+ });
15161
+ }
14982
15162
  return {
14983
15163
  status_code: 200,
14984
15164
  body: { success: true }
@@ -15197,11 +15377,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15197
15377
  status_code: 400,
15198
15378
  body: { error: "terms must be an array of strings" }
15199
15379
  };
15380
+ if (req.body.project !== void 0 && (typeof req.body.project !== "string" || !req.body.project.trim())) return {
15381
+ status_code: 400,
15382
+ body: { error: "project must be a non-empty string" }
15383
+ };
15200
15384
  return {
15201
15385
  status_code: 200,
15202
15386
  body: await sdk.trigger({
15203
15387
  function_id: "mem::enrich",
15204
- payload: req.body
15388
+ payload: {
15389
+ sessionId: req.body.sessionId,
15390
+ files: req.body.files,
15391
+ ...req.body.terms !== void 0 && { terms: req.body.terms },
15392
+ ...req.body.toolName !== void 0 && { toolName: req.body.toolName },
15393
+ ...req.body.project !== void 0 && { project: req.body.project }
15394
+ }
15205
15395
  })
15206
15396
  };
15207
15397
  });
@@ -15220,11 +15410,23 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15220
15410
  status_code: 400,
15221
15411
  body: { error: "content is required" }
15222
15412
  };
15413
+ if (req.body.project !== void 0 && (typeof req.body.project !== "string" || !req.body.project.trim())) return {
15414
+ status_code: 400,
15415
+ body: { error: "project must be a non-empty string" }
15416
+ };
15223
15417
  return {
15224
15418
  status_code: 201,
15225
15419
  body: await sdk.trigger({
15226
15420
  function_id: "mem::remember",
15227
- payload: req.body
15421
+ payload: {
15422
+ content: req.body.content,
15423
+ ...req.body.type !== void 0 && { type: req.body.type },
15424
+ ...req.body.concepts !== void 0 && { concepts: req.body.concepts },
15425
+ ...req.body.files !== void 0 && { files: req.body.files },
15426
+ ...req.body.ttlDays !== void 0 && { ttlDays: req.body.ttlDays },
15427
+ ...req.body.sourceObservationIds !== void 0 && { sourceObservationIds: req.body.sourceObservationIds },
15428
+ ...req.body.project !== void 0 && { project: req.body.project }
15429
+ }
15228
15430
  })
15229
15431
  };
15230
15432
  });
@@ -15319,15 +15521,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15319
15521
  sdk.registerFunction("api::migrate", async (req) => {
15320
15522
  const authErr = checkAuth(req, secret);
15321
15523
  if (authErr) return authErr;
15322
- if (!req.body?.dbPath || typeof req.body.dbPath !== "string") return {
15524
+ const hasStep = typeof req.body?.step === "string" && req.body.step.trim().length > 0;
15525
+ const hasDbPath = typeof req.body?.dbPath === "string" && req.body.dbPath.trim().length > 0;
15526
+ if (!hasStep && !hasDbPath) return {
15323
15527
  status_code: 400,
15324
- body: { error: "dbPath is required" }
15528
+ body: { error: "Either step (string) or dbPath (string) is required" }
15325
15529
  };
15326
15530
  return {
15327
15531
  status_code: 200,
15328
15532
  body: await sdk.trigger({
15329
15533
  function_id: "mem::migrate",
15330
- payload: req.body
15534
+ payload: {
15535
+ ...req.body.step !== void 0 && { step: req.body.step },
15536
+ ...req.body.dbPath !== void 0 && { dbPath: req.body.dbPath },
15537
+ ...req.body.dryRun !== void 0 && { dryRun: req.body.dryRun }
15538
+ }
15331
15539
  })
15332
15540
  };
15333
15541
  });
@@ -15673,6 +15881,63 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15673
15881
  http_method: "POST"
15674
15882
  }
15675
15883
  });
15884
+ sdk.registerFunction("api::graph-build", async (req) => {
15885
+ const authErr = checkAuth(req, secret);
15886
+ if (authErr) return authErr;
15887
+ const batchSize = Math.max(1, Math.min(100, Number(req.body?.batchSize) || 25));
15888
+ try {
15889
+ const sessions = await kv.list(KV.sessions);
15890
+ let totalNodes = 0;
15891
+ let totalEdges = 0;
15892
+ let batchesRun = 0;
15893
+ for (const session of sessions) {
15894
+ const sid = session?.id;
15895
+ if (typeof sid !== "string" || sid.length === 0) continue;
15896
+ const compressed = (await kv.list(KV.observations(sid))).filter((o) => o && typeof o.title === "string" && o.title.length > 0);
15897
+ if (compressed.length === 0) continue;
15898
+ for (let i = 0; i < compressed.length; i += batchSize) {
15899
+ const batch = compressed.slice(i, i + batchSize);
15900
+ try {
15901
+ const result = await sdk.trigger({
15902
+ function_id: "mem::graph-extract",
15903
+ payload: { observations: batch }
15904
+ });
15905
+ if (result?.success) {
15906
+ totalNodes += Number(result.nodesAdded) || 0;
15907
+ totalEdges += Number(result.edgesAdded) || 0;
15908
+ }
15909
+ batchesRun++;
15910
+ } catch (err) {
15911
+ logger.warn("graph-build batch failed", {
15912
+ sessionId: sid,
15913
+ batchIndex: Math.floor(i / batchSize),
15914
+ error: err instanceof Error ? err.message : String(err)
15915
+ });
15916
+ }
15917
+ }
15918
+ }
15919
+ return {
15920
+ status_code: 200,
15921
+ body: {
15922
+ success: true,
15923
+ sessions: sessions.length,
15924
+ batches: batchesRun,
15925
+ nodes: totalNodes,
15926
+ edges: totalEdges
15927
+ }
15928
+ };
15929
+ } catch {
15930
+ return graphDisabledResponse();
15931
+ }
15932
+ });
15933
+ sdk.registerTrigger({
15934
+ type: "http",
15935
+ function_id: "api::graph-build",
15936
+ config: {
15937
+ api_path: "/agentmemory/graph/build",
15938
+ http_method: "POST"
15939
+ }
15940
+ });
15676
15941
  sdk.registerFunction("api::consolidate-pipeline", async (req) => {
15677
15942
  const authErr = checkAuth(req, secret);
15678
15943
  if (authErr) return authErr;
@@ -16130,6 +16395,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16130
16395
  sdk.registerFunction("api::slot-list", async (req) => {
16131
16396
  const authErr = checkAuth(req, secret);
16132
16397
  if (authErr) return authErr;
16398
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16133
16399
  return {
16134
16400
  status_code: 200,
16135
16401
  body: await sdk.trigger({
@@ -16149,6 +16415,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16149
16415
  sdk.registerFunction("api::slot-get", async (req) => {
16150
16416
  const authErr = checkAuth(req, secret);
16151
16417
  if (authErr) return authErr;
16418
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16152
16419
  const label = asNonEmptyString$1(req.query_params?.["label"]);
16153
16420
  if (!label) return {
16154
16421
  status_code: 400,
@@ -16179,6 +16446,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16179
16446
  sdk.registerFunction("api::slot-create", async (req) => {
16180
16447
  const authErr = checkAuth(req, secret);
16181
16448
  if (authErr) return authErr;
16449
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16182
16450
  const body = req.body ?? {};
16183
16451
  const label = asNonEmptyString$1(body["label"]);
16184
16452
  if (!label) return {
@@ -16241,6 +16509,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16241
16509
  sdk.registerFunction("api::slot-append", async (req) => {
16242
16510
  const authErr = checkAuth(req, secret);
16243
16511
  if (authErr) return authErr;
16512
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16244
16513
  const body = req.body ?? {};
16245
16514
  const label = asNonEmptyString$1(body["label"]);
16246
16515
  const text = typeof body["text"] === "string" ? body["text"] : null;
@@ -16280,6 +16549,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16280
16549
  sdk.registerFunction("api::slot-replace", async (req) => {
16281
16550
  const authErr = checkAuth(req, secret);
16282
16551
  if (authErr) return authErr;
16552
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16283
16553
  const body = req.body ?? {};
16284
16554
  const label = asNonEmptyString$1(body["label"]);
16285
16555
  const content = body["content"];
@@ -16319,6 +16589,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16319
16589
  sdk.registerFunction("api::slot-delete", async (req) => {
16320
16590
  const authErr = checkAuth(req, secret);
16321
16591
  if (authErr) return authErr;
16592
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16322
16593
  const label = asNonEmptyString$1(req.query_params?.["label"]);
16323
16594
  if (!label) return {
16324
16595
  status_code: 400,
@@ -16349,6 +16620,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16349
16620
  sdk.registerFunction("api::slot-reflect", async (req) => {
16350
16621
  const authErr = checkAuth(req, secret);
16351
16622
  if (authErr) return authErr;
16623
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16624
+ if (!isReflectEnabled()) return reflectDisabledResponse();
16352
16625
  const body = req.body ?? {};
16353
16626
  const sessionId = asNonEmptyString$1(body["sessionId"]);
16354
16627
  if (!sessionId) return {
@@ -18001,6 +18274,10 @@ const CORE_TOOLS = [
18001
18274
  files: {
18002
18275
  type: "string",
18003
18276
  description: "Comma-separated relevant file paths"
18277
+ },
18278
+ project: {
18279
+ type: "string",
18280
+ description: "Stable canonical project identifier this memory belongs to (e.g. a slug, UUID, or registry key). Must match the value used when the session was started. Do not use filesystem paths or ad-hoc display names — those change across machines and will silently break project scoping."
18004
18281
  }
18005
18282
  },
18006
18283
  required: ["content"]
@@ -19157,13 +19434,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
19157
19434
  const type = args.type || "fact";
19158
19435
  const concepts = typeof args.concepts === "string" ? args.concepts.split(",").map((c) => c.trim()).filter(Boolean) : [];
19159
19436
  const files = typeof args.files === "string" ? args.files.split(",").map((f) => f.trim()).filter(Boolean) : [];
19437
+ const project = typeof args.project === "string" && args.project.trim().length > 0 ? args.project.trim() : void 0;
19160
19438
  const result = await sdk.trigger({
19161
19439
  function_id: "mem::remember",
19162
19440
  payload: {
19163
19441
  content: args.content,
19164
19442
  type,
19165
19443
  concepts,
19166
- files
19444
+ files,
19445
+ ...project !== void 0 && { project }
19167
19446
  }
19168
19447
  });
19169
19448
  return {
@@ -21010,7 +21289,7 @@ async function main() {
21010
21289
  if (bm25Index.has(memory.id)) continue;
21011
21290
  bm25Index.add({
21012
21291
  id: memory.id,
21013
- sessionId: memory.sessionIds[0] ?? "memory",
21292
+ sessionId: memory.sessionIds?.[0] ?? "memory",
21014
21293
  timestamp: memory.createdAt,
21015
21294
  type: "decision",
21016
21295
  title: memory.title,
@@ -21030,7 +21309,7 @@ async function main() {
21030
21309
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
21031
21310
  }
21032
21311
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
21033
- bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21312
+ bootLog(`REST API: 125 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21034
21313
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
21035
21314
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
21036
21315
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);