@agentmemory/agentmemory 0.9.22 → 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 (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 +32 -18
  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 +335 -57
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/{src-gpTAJuBy.mjs → src-DvS3bhMe.mjs} +322 -58
  37. package/dist/src-DvS3bhMe.mjs.map +1 -0
  38. package/dist/{standalone-C4i7ktpn.mjs → standalone-DHQcPX_g.mjs} +92 -11
  39. package/dist/standalone-DHQcPX_g.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-BPfyI4Kc.mjs +6 -0
  45. package/dist/version-BPfyI4Kc.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.23";
6684
6797
 
6685
6798
  //#endregion
6686
6799
  //#region src/functions/export-import.ts
@@ -6820,7 +6933,8 @@ 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"
6824
6938
  ]).has(importData.version)) return {
6825
6939
  success: false,
6826
6940
  error: `Unsupported export version: ${importData.version}`
@@ -6943,6 +7057,7 @@ function registerExportImportFunction(sdk, kv) {
6943
7057
  continue;
6944
7058
  }
6945
7059
  }
7060
+ if (!Array.isArray(memory.sessionIds)) memory.sessionIds = [];
6946
7061
  await kv.set(KV.memories, memory.id, memory);
6947
7062
  stats.memories++;
6948
7063
  }
@@ -7146,6 +7261,7 @@ function escapeXml(s) {
7146
7261
  }
7147
7262
  function registerEnrichFunction(sdk, kv) {
7148
7263
  sdk.registerFunction("mem::enrich", async (data) => {
7264
+ const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
7149
7265
  const parts = [];
7150
7266
  const fileContextPromise = sdk.trigger({
7151
7267
  function_id: "mem::file-context",
@@ -7159,10 +7275,11 @@ function registerEnrichFunction(sdk, kv) {
7159
7275
  function_id: "mem::search",
7160
7276
  payload: {
7161
7277
  query: searchQueries.join(" "),
7162
- limit: 5
7278
+ limit: 5,
7279
+ ...project !== void 0 && { project }
7163
7280
  }
7164
7281
  }).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(() => []);
7282
+ 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
7283
  const [fileContext, searchResult, bugMemories] = await Promise.all([
7167
7284
  fileContextPromise,
7168
7285
  searchPromise,
@@ -7185,6 +7302,7 @@ function registerEnrichFunction(sdk, kv) {
7185
7302
  }
7186
7303
  logger.info("Enrichment completed", {
7187
7304
  sessionId: data.sessionId,
7305
+ project,
7188
7306
  fileCount: data.files.length,
7189
7307
  contextLength: context.length,
7190
7308
  truncated
@@ -7342,16 +7460,24 @@ function buildGraphExtractionPrompt(observations) {
7342
7460
 
7343
7461
  //#endregion
7344
7462
  //#region src/functions/graph.ts
7463
+ function parseAttrs(raw) {
7464
+ const attrs = {};
7465
+ const attrRegex = /([A-Za-z_][\w:-]*)="([^"]*)"/g;
7466
+ let m;
7467
+ while ((m = attrRegex.exec(raw)) !== null) attrs[m[1]] = m[2];
7468
+ return attrs;
7469
+ }
7345
7470
  function parseGraphXml(xml, observationIds) {
7346
7471
  const nodes = [];
7347
7472
  const edges = [];
7348
7473
  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] ?? "";
7474
+ const entitySelfClose = /<entity\b([^>]*?)\/>/g;
7475
+ const entityWithBody = /<entity\b([^>]*[^/])>([\s\S]*?)<\/entity>/g;
7476
+ const addEntity = (rawAttrs, propsBlock = "") => {
7477
+ const attrs = parseAttrs(rawAttrs);
7478
+ const type = attrs["type"];
7479
+ const name = attrs["name"];
7480
+ if (!type || !name) return;
7355
7481
  const properties = {};
7356
7482
  const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
7357
7483
  let propMatch;
@@ -7364,17 +7490,23 @@ function parseGraphXml(xml, observationIds) {
7364
7490
  sourceObservationIds: observationIds,
7365
7491
  createdAt: now
7366
7492
  });
7367
- }
7368
- const relRegex = /<relationship\s+type="([^"]+)"\s+source="([^"]+)"\s+target="([^"]+)"\s+weight="([^"]+)"\s*\/>/g;
7493
+ };
7494
+ let match;
7495
+ while ((match = entitySelfClose.exec(xml)) !== null) addEntity(match[1]);
7496
+ while ((match = entityWithBody.exec(xml)) !== null) addEntity(match[1], match[2]);
7497
+ const relRegex = /<relationship\b([^>]*?)\/>/g;
7369
7498
  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;
7499
+ const attrs = parseAttrs(match[1]);
7500
+ const type = attrs["type"];
7501
+ const sourceName = attrs["source"];
7502
+ const targetName = attrs["target"];
7503
+ if (!type || !sourceName || !targetName) continue;
7504
+ const parsedWeight = parseFloat(attrs["weight"] ?? "");
7505
+ const weight = Number.isFinite(parsedWeight) ? parsedWeight : .5;
7375
7506
  const sourceNode = nodes.find((n) => n.name === sourceName);
7376
7507
  const targetNode = nodes.find((n) => n.name === targetName);
7377
- if (sourceNode && targetNode) edges.push({
7508
+ if (!sourceNode || !targetNode) continue;
7509
+ edges.push({
7378
7510
  id: generateId("ge"),
7379
7511
  type,
7380
7512
  sourceNodeId: sourceNode.id,
@@ -7588,7 +7720,7 @@ function registerConsolidationPipelineFunction(sdk, kv, provider) {
7588
7720
  if (!data?.force && !isConsolidationEnabled()) return {
7589
7721
  success: false,
7590
7722
  skipped: true,
7591
- reason: "CONSOLIDATION_ENABLED is not set to true"
7723
+ 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
7724
  };
7593
7725
  const tier = data?.tier || "all";
7594
7726
  const decayDays = getConsolidationDecayDays();
@@ -10758,11 +10890,34 @@ function registerDiagnosticsFunction(sdk, kv) {
10758
10890
  });
10759
10891
  memoryIssues++;
10760
10892
  }
10893
+ const latestMemories = memories.filter((m) => m.isLatest);
10894
+ const unscopedCount = latestMemories.filter((m) => !m.project).length;
10895
+ if (unscopedCount === 0) checks.push({
10896
+ name: "memory-project-coverage",
10897
+ category: "memories",
10898
+ status: "pass",
10899
+ message: `All ${latestMemories.length} latest memories have a project scope`,
10900
+ fixable: false
10901
+ });
10902
+ else if (unscopedCount <= 10) checks.push({
10903
+ name: "memory-project-coverage",
10904
+ category: "memories",
10905
+ status: "warn",
10906
+ message: `${unscopedCount} of ${latestMemories.length} latest memories have no project scope — run POST /agentmemory/migrate {"step":"infer-memory-projects"} to backfill`,
10907
+ fixable: true
10908
+ });
10909
+ else checks.push({
10910
+ name: "memory-project-coverage",
10911
+ category: "memories",
10912
+ status: "fail",
10913
+ message: `${unscopedCount} of ${latestMemories.length} latest memories have no project scope — run POST /agentmemory/migrate {"step":"infer-memory-projects"} to backfill`,
10914
+ fixable: true
10915
+ });
10761
10916
  if (memoryIssues === 0) checks.push({
10762
10917
  name: "memories-ok",
10763
10918
  category: "memories",
10764
10919
  status: "pass",
10765
- message: `All ${memories.length} memories are consistent`,
10920
+ message: `All ${memories.length} memories are structurally consistent`,
10766
10921
  fixable: false
10767
10922
  });
10768
10923
  }
@@ -14535,6 +14690,22 @@ function consolidationDisabledResponse() {
14535
14690
  docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
14536
14691
  });
14537
14692
  }
14693
+ function slotsDisabledResponse() {
14694
+ return flagDisabledResponse({
14695
+ error: "Memory slots not enabled",
14696
+ flag: "AGENTMEMORY_SLOTS",
14697
+ enableHow: "Set AGENTMEMORY_SLOTS=true (in ~/.agentmemory/.env or the shell) and restart.",
14698
+ docsHref: "https://github.com/rohitg00/agentmemory#memory-slots"
14699
+ });
14700
+ }
14701
+ function reflectDisabledResponse() {
14702
+ return flagDisabledResponse({
14703
+ error: "Slot reflection not enabled",
14704
+ flag: "AGENTMEMORY_REFLECT",
14705
+ enableHow: "Set AGENTMEMORY_REFLECT=true (in ~/.agentmemory/.env or the shell) and restart. Requires AGENTMEMORY_SLOTS=true.",
14706
+ docsHref: "https://github.com/rohitg00/agentmemory#memory-slots"
14707
+ });
14708
+ }
14538
14709
  function asNonEmptyString$1(value) {
14539
14710
  if (typeof value !== "string") return null;
14540
14711
  const trimmed = value.trim();
@@ -14979,6 +15150,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14979
15150
  path: "status",
14980
15151
  value: "completed"
14981
15152
  }]);
15153
+ try {
15154
+ sdk.triggerVoid("event::session::stopped", { sessionId });
15155
+ } catch (err) {
15156
+ logger.warn("event::session::stopped triggerVoid failed", {
15157
+ sessionId,
15158
+ error: err instanceof Error ? err.message : String(err)
15159
+ });
15160
+ }
14982
15161
  return {
14983
15162
  status_code: 200,
14984
15163
  body: { success: true }
@@ -15197,11 +15376,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15197
15376
  status_code: 400,
15198
15377
  body: { error: "terms must be an array of strings" }
15199
15378
  };
15379
+ if (req.body.project !== void 0 && (typeof req.body.project !== "string" || !req.body.project.trim())) return {
15380
+ status_code: 400,
15381
+ body: { error: "project must be a non-empty string" }
15382
+ };
15200
15383
  return {
15201
15384
  status_code: 200,
15202
15385
  body: await sdk.trigger({
15203
15386
  function_id: "mem::enrich",
15204
- payload: req.body
15387
+ payload: {
15388
+ sessionId: req.body.sessionId,
15389
+ files: req.body.files,
15390
+ ...req.body.terms !== void 0 && { terms: req.body.terms },
15391
+ ...req.body.toolName !== void 0 && { toolName: req.body.toolName },
15392
+ ...req.body.project !== void 0 && { project: req.body.project }
15393
+ }
15205
15394
  })
15206
15395
  };
15207
15396
  });
@@ -15220,11 +15409,23 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15220
15409
  status_code: 400,
15221
15410
  body: { error: "content is required" }
15222
15411
  };
15412
+ if (req.body.project !== void 0 && (typeof req.body.project !== "string" || !req.body.project.trim())) return {
15413
+ status_code: 400,
15414
+ body: { error: "project must be a non-empty string" }
15415
+ };
15223
15416
  return {
15224
15417
  status_code: 201,
15225
15418
  body: await sdk.trigger({
15226
15419
  function_id: "mem::remember",
15227
- payload: req.body
15420
+ payload: {
15421
+ content: req.body.content,
15422
+ ...req.body.type !== void 0 && { type: req.body.type },
15423
+ ...req.body.concepts !== void 0 && { concepts: req.body.concepts },
15424
+ ...req.body.files !== void 0 && { files: req.body.files },
15425
+ ...req.body.ttlDays !== void 0 && { ttlDays: req.body.ttlDays },
15426
+ ...req.body.sourceObservationIds !== void 0 && { sourceObservationIds: req.body.sourceObservationIds },
15427
+ ...req.body.project !== void 0 && { project: req.body.project }
15428
+ }
15228
15429
  })
15229
15430
  };
15230
15431
  });
@@ -15319,15 +15520,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15319
15520
  sdk.registerFunction("api::migrate", async (req) => {
15320
15521
  const authErr = checkAuth(req, secret);
15321
15522
  if (authErr) return authErr;
15322
- if (!req.body?.dbPath || typeof req.body.dbPath !== "string") return {
15523
+ const hasStep = typeof req.body?.step === "string" && req.body.step.trim().length > 0;
15524
+ const hasDbPath = typeof req.body?.dbPath === "string" && req.body.dbPath.trim().length > 0;
15525
+ if (!hasStep && !hasDbPath) return {
15323
15526
  status_code: 400,
15324
- body: { error: "dbPath is required" }
15527
+ body: { error: "Either step (string) or dbPath (string) is required" }
15325
15528
  };
15326
15529
  return {
15327
15530
  status_code: 200,
15328
15531
  body: await sdk.trigger({
15329
15532
  function_id: "mem::migrate",
15330
- payload: req.body
15533
+ payload: {
15534
+ ...req.body.step !== void 0 && { step: req.body.step },
15535
+ ...req.body.dbPath !== void 0 && { dbPath: req.body.dbPath },
15536
+ ...req.body.dryRun !== void 0 && { dryRun: req.body.dryRun }
15537
+ }
15331
15538
  })
15332
15539
  };
15333
15540
  });
@@ -15673,6 +15880,63 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15673
15880
  http_method: "POST"
15674
15881
  }
15675
15882
  });
15883
+ sdk.registerFunction("api::graph-build", async (req) => {
15884
+ const authErr = checkAuth(req, secret);
15885
+ if (authErr) return authErr;
15886
+ const batchSize = Math.max(1, Math.min(100, Number(req.body?.batchSize) || 25));
15887
+ try {
15888
+ const sessions = await kv.list(KV.sessions);
15889
+ let totalNodes = 0;
15890
+ let totalEdges = 0;
15891
+ let batchesRun = 0;
15892
+ for (const session of sessions) {
15893
+ const sid = session?.id;
15894
+ if (typeof sid !== "string" || sid.length === 0) continue;
15895
+ const compressed = (await kv.list(KV.observations(sid))).filter((o) => o && typeof o.title === "string" && o.title.length > 0);
15896
+ if (compressed.length === 0) continue;
15897
+ for (let i = 0; i < compressed.length; i += batchSize) {
15898
+ const batch = compressed.slice(i, i + batchSize);
15899
+ try {
15900
+ const result = await sdk.trigger({
15901
+ function_id: "mem::graph-extract",
15902
+ payload: { observations: batch }
15903
+ });
15904
+ if (result?.success) {
15905
+ totalNodes += Number(result.nodesAdded) || 0;
15906
+ totalEdges += Number(result.edgesAdded) || 0;
15907
+ }
15908
+ batchesRun++;
15909
+ } catch (err) {
15910
+ logger.warn("graph-build batch failed", {
15911
+ sessionId: sid,
15912
+ batchIndex: Math.floor(i / batchSize),
15913
+ error: err instanceof Error ? err.message : String(err)
15914
+ });
15915
+ }
15916
+ }
15917
+ }
15918
+ return {
15919
+ status_code: 200,
15920
+ body: {
15921
+ success: true,
15922
+ sessions: sessions.length,
15923
+ batches: batchesRun,
15924
+ nodes: totalNodes,
15925
+ edges: totalEdges
15926
+ }
15927
+ };
15928
+ } catch {
15929
+ return graphDisabledResponse();
15930
+ }
15931
+ });
15932
+ sdk.registerTrigger({
15933
+ type: "http",
15934
+ function_id: "api::graph-build",
15935
+ config: {
15936
+ api_path: "/agentmemory/graph/build",
15937
+ http_method: "POST"
15938
+ }
15939
+ });
15676
15940
  sdk.registerFunction("api::consolidate-pipeline", async (req) => {
15677
15941
  const authErr = checkAuth(req, secret);
15678
15942
  if (authErr) return authErr;
@@ -16130,6 +16394,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16130
16394
  sdk.registerFunction("api::slot-list", async (req) => {
16131
16395
  const authErr = checkAuth(req, secret);
16132
16396
  if (authErr) return authErr;
16397
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16133
16398
  return {
16134
16399
  status_code: 200,
16135
16400
  body: await sdk.trigger({
@@ -16149,6 +16414,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16149
16414
  sdk.registerFunction("api::slot-get", async (req) => {
16150
16415
  const authErr = checkAuth(req, secret);
16151
16416
  if (authErr) return authErr;
16417
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16152
16418
  const label = asNonEmptyString$1(req.query_params?.["label"]);
16153
16419
  if (!label) return {
16154
16420
  status_code: 400,
@@ -16179,6 +16445,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16179
16445
  sdk.registerFunction("api::slot-create", async (req) => {
16180
16446
  const authErr = checkAuth(req, secret);
16181
16447
  if (authErr) return authErr;
16448
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16182
16449
  const body = req.body ?? {};
16183
16450
  const label = asNonEmptyString$1(body["label"]);
16184
16451
  if (!label) return {
@@ -16241,6 +16508,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16241
16508
  sdk.registerFunction("api::slot-append", async (req) => {
16242
16509
  const authErr = checkAuth(req, secret);
16243
16510
  if (authErr) return authErr;
16511
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16244
16512
  const body = req.body ?? {};
16245
16513
  const label = asNonEmptyString$1(body["label"]);
16246
16514
  const text = typeof body["text"] === "string" ? body["text"] : null;
@@ -16280,6 +16548,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16280
16548
  sdk.registerFunction("api::slot-replace", async (req) => {
16281
16549
  const authErr = checkAuth(req, secret);
16282
16550
  if (authErr) return authErr;
16551
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16283
16552
  const body = req.body ?? {};
16284
16553
  const label = asNonEmptyString$1(body["label"]);
16285
16554
  const content = body["content"];
@@ -16319,6 +16588,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16319
16588
  sdk.registerFunction("api::slot-delete", async (req) => {
16320
16589
  const authErr = checkAuth(req, secret);
16321
16590
  if (authErr) return authErr;
16591
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16322
16592
  const label = asNonEmptyString$1(req.query_params?.["label"]);
16323
16593
  if (!label) return {
16324
16594
  status_code: 400,
@@ -16349,6 +16619,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
16349
16619
  sdk.registerFunction("api::slot-reflect", async (req) => {
16350
16620
  const authErr = checkAuth(req, secret);
16351
16621
  if (authErr) return authErr;
16622
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16623
+ if (!isReflectEnabled()) return reflectDisabledResponse();
16352
16624
  const body = req.body ?? {};
16353
16625
  const sessionId = asNonEmptyString$1(body["sessionId"]);
16354
16626
  if (!sessionId) return {
@@ -18001,6 +18273,10 @@ const CORE_TOOLS = [
18001
18273
  files: {
18002
18274
  type: "string",
18003
18275
  description: "Comma-separated relevant file paths"
18276
+ },
18277
+ project: {
18278
+ type: "string",
18279
+ 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
18280
  }
18005
18281
  },
18006
18282
  required: ["content"]
@@ -19157,13 +19433,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
19157
19433
  const type = args.type || "fact";
19158
19434
  const concepts = typeof args.concepts === "string" ? args.concepts.split(",").map((c) => c.trim()).filter(Boolean) : [];
19159
19435
  const files = typeof args.files === "string" ? args.files.split(",").map((f) => f.trim()).filter(Boolean) : [];
19436
+ const project = typeof args.project === "string" && args.project.trim().length > 0 ? args.project.trim() : void 0;
19160
19437
  const result = await sdk.trigger({
19161
19438
  function_id: "mem::remember",
19162
19439
  payload: {
19163
19440
  content: args.content,
19164
19441
  type,
19165
19442
  concepts,
19166
- files
19443
+ files,
19444
+ ...project !== void 0 && { project }
19167
19445
  }
19168
19446
  });
19169
19447
  return {
@@ -21010,7 +21288,7 @@ async function main() {
21010
21288
  if (bm25Index.has(memory.id)) continue;
21011
21289
  bm25Index.add({
21012
21290
  id: memory.id,
21013
- sessionId: memory.sessionIds[0] ?? "memory",
21291
+ sessionId: memory.sessionIds?.[0] ?? "memory",
21014
21292
  timestamp: memory.createdAt,
21015
21293
  type: "decision",
21016
21294
  title: memory.title,
@@ -21030,7 +21308,7 @@ async function main() {
21030
21308
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
21031
21309
  }
21032
21310
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
21033
- bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21311
+ bootLog(`REST API: 125 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21034
21312
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
21035
21313
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
21036
21314
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);