@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
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { TriggerAction, registerWorker } from "iii-sdk";
4
- import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import Anthropic from "@anthropic-ai/sdk";
@@ -40,6 +40,7 @@ function safeParseInt(value, fallback) {
40
40
  }
41
41
  const DATA_DIR = join(homedir(), ".agentmemory");
42
42
  const ENV_FILE = join(DATA_DIR, ".env");
43
+ let warnPremiumModelShown = false;
43
44
  function loadEnvFile() {
44
45
  if (!existsSync(ENV_FILE)) return {};
45
46
  const content = readFileSync(ENV_FILE, "utf-8");
@@ -93,11 +94,18 @@ function detectProvider(env) {
93
94
  maxTokens
94
95
  };
95
96
  }
96
- if (hasRealValue(env["OPENROUTER_API_KEY"])) return {
97
- provider: "openrouter",
98
- model: env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-4-20250514",
99
- maxTokens
100
- };
97
+ if (hasRealValue(env["OPENROUTER_API_KEY"])) {
98
+ const model = env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-4-20250514";
99
+ if (!warnPremiumModelShown && /sonnet|opus|gpt-4o(?!.*mini)|gpt-4-turbo/i.test(model) && env["AGENTMEMORY_SUPPRESS_COST_WARNING"] !== "1" && env["AGENTMEMORY_SUPPRESS_COST_WARNING"] !== "true") {
100
+ warnPremiumModelShown = true;
101
+ process.stderr.write(`[agentmemory] OPENROUTER_MODEL=${model} is in the premium tier. Background compression on this model can cost $5+/day under active use. Cheaper alternatives with comparable quality for memory compression: deepseek/deepseek-v4-pro, deepseek/deepseek-chat, qwen/qwen3-coder. See README "Cost-aware model selection" for the full table. Set AGENTMEMORY_SUPPRESS_COST_WARNING=1 to silence.\n`);
102
+ }
103
+ return {
104
+ provider: "openrouter",
105
+ model,
106
+ maxTokens
107
+ };
108
+ }
101
109
  if (!(env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true")) {
102
110
  process.stderr.write("[agentmemory] No LLM provider key found (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY, OPENAI_API_KEY). LLM-backed compression and summarization are DISABLED — using no-op provider. This is the safe default: the agent-sdk fallback used to spawn Claude Agent SDK child sessions which inherit Claude Code's plugin hooks and cause infinite Stop-hook recursion (#149 follow-up). To opt in to the agent-sdk fallback anyway, set both AGENTMEMORY_AUTO_COMPRESS=true AND AGENTMEMORY_ALLOW_AGENT_SDK=true — but be aware it will burn your Claude Pro allocation and may still recurse if you use it from inside Claude Code itself.\n");
103
111
  return {
@@ -175,8 +183,8 @@ function loadClaudeBridgeConfig() {
175
183
  const lineBudget = safeParseInt(env["CLAUDE_MEMORY_LINE_BUDGET"], 200);
176
184
  let memoryFilePath = "";
177
185
  if (enabled && projectPath) {
178
- const safePath = projectPath.replace(/[/\\]/g, "-").replace(/^-/, "");
179
- memoryFilePath = join(homedir(), ".claude", "projects", safePath, "memory", "MEMORY.md");
186
+ const safePath = projectPath.replace(/[/\\]/g, "-");
187
+ memoryFilePath = join(homedir(), ".claude", "projects", safePath, "MEMORY.md");
180
188
  }
181
189
  return {
182
190
  enabled,
@@ -196,6 +204,23 @@ function loadTeamConfig() {
196
204
  mode: env["TEAM_MODE"] === "shared" ? "shared" : "private"
197
205
  };
198
206
  }
207
+ function loadAgentScope() {
208
+ const env = getMergedEnv();
209
+ const raw = env["AGENT_ID"];
210
+ if (!raw) return null;
211
+ const agentId = raw.trim().slice(0, 128);
212
+ if (!agentId) return null;
213
+ return {
214
+ agentId,
215
+ mode: env["AGENTMEMORY_AGENT_SCOPE"] === "isolated" ? "isolated" : "shared"
216
+ };
217
+ }
218
+ function getAgentId() {
219
+ return loadAgentScope()?.agentId;
220
+ }
221
+ function isAgentScopeIsolated() {
222
+ return loadAgentScope()?.mode === "isolated";
223
+ }
199
224
  function loadSnapshotConfig() {
200
225
  const env = getMergedEnv();
201
226
  return {
@@ -208,7 +233,17 @@ function isGraphExtractionEnabled() {
208
233
  return getMergedEnv()["GRAPH_EXTRACTION_ENABLED"] === "true";
209
234
  }
210
235
  function isConsolidationEnabled() {
211
- 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");
212
247
  }
213
248
  function isAutoCompressEnabled() {
214
249
  return getMergedEnv()["AGENTMEMORY_AUTO_COMPRESS"] === "true";
@@ -453,13 +488,25 @@ function v1AzureUrl(baseUrl, path) {
453
488
  url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
454
489
  return url.toString();
455
490
  }
491
+ function appendOpenAIRoute(baseUrl, route) {
492
+ const trimmedBase = baseUrl.replace(/\/+$/, "");
493
+ const cleanRoute = route.startsWith("/") ? route : `/${route}`;
494
+ let pathname;
495
+ try {
496
+ pathname = new URL(trimmedBase).pathname.replace(/\/+$/, "");
497
+ } catch {
498
+ return `${trimmedBase}/v1${cleanRoute}`;
499
+ }
500
+ if (pathname === "" || pathname === "/") return `${trimmedBase}/v1${cleanRoute}`;
501
+ return `${trimmedBase}${cleanRoute}`;
502
+ }
456
503
  function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
457
504
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
458
- return `${baseUrl}/v1/chat/completions`;
505
+ return appendOpenAIRoute(baseUrl, "/chat/completions");
459
506
  }
460
507
  function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
461
508
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
462
- return `${baseUrl}/v1/embeddings`;
509
+ return appendOpenAIRoute(baseUrl, "/embeddings");
463
510
  }
464
511
  function buildAuthHeaders(apiKey, isAzure) {
465
512
  if (isAzure) return {
@@ -541,6 +588,7 @@ var OpenAIProvider = class {
541
588
  const body = {
542
589
  model: this.model,
543
590
  max_tokens: this.maxTokens,
591
+ stream: false,
544
592
  messages: [{
545
593
  role: "system",
546
594
  content: systemPrompt
@@ -569,7 +617,7 @@ var OpenAIProvider = class {
569
617
  const message = data.choices?.[0]?.message;
570
618
  const content = message?.content;
571
619
  if (content) return content;
572
- const reasoning = message?.reasoning;
620
+ const reasoning = message?.reasoning ?? message?.reasoning_content;
573
621
  if (reasoning) return reasoning;
574
622
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
575
623
  }
@@ -848,16 +896,30 @@ function resolveDimensions(model, override) {
848
896
  * `api-key` header instead of `Authorization: Bearer`.
849
897
  *
850
898
  * Required env vars:
851
- * OPENAI_API_KEY — API key
899
+ * OPENAI_API_KEY — API key (fallback for OPENAI_EMBEDDING_API_KEY)
852
900
  *
853
901
  * Optional:
854
- * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com).
855
- * Azure: https://<resource>.openai.azure.com/openai/deployments/<deployment>
856
- * OPENAI_API_VERSION Azure api-version query param (default: 2024-08-01-preview)
857
- * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
858
- * OPENAI_EMBEDDING_DIMENSIONS override reported dimensions (required for
859
- * custom / self-hosted models not in the
860
- * 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)
861
923
  */
862
924
  var OpenAIEmbeddingProvider = class {
863
925
  name = "openai";
@@ -868,9 +930,9 @@ var OpenAIEmbeddingProvider = class {
868
930
  isAzure;
869
931
  azureApiVersion;
870
932
  constructor(apiKey) {
871
- this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
872
- if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
873
- 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"));
874
936
  this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
875
937
  this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
876
938
  this.isAzure = detectAzure(this.baseUrl);
@@ -1317,10 +1379,11 @@ function jaccardSimilarity(a, b) {
1317
1379
  //#endregion
1318
1380
  //#region src/state/vector-index.ts
1319
1381
  function float32ToBase64(arr) {
1320
- return Buffer.from(arr.buffer).toString("base64");
1382
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString("base64");
1321
1383
  }
1322
1384
  function base64ToFloat32(b64) {
1323
- 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);
1324
1387
  }
1325
1388
  function cosineSimilarity(a, b) {
1326
1389
  if (a.length !== b.length) return 0;
@@ -1441,7 +1504,7 @@ var VectorIndex = class VectorIndex {
1441
1504
  function memoryToObservation(memory) {
1442
1505
  return {
1443
1506
  id: memory.id,
1444
- sessionId: memory.sessionIds[0] ?? "memory",
1507
+ sessionId: memory.sessionIds?.[0] ?? "memory",
1445
1508
  timestamp: memory.createdAt,
1446
1509
  type: "decision",
1447
1510
  title: memory.title,
@@ -2514,6 +2577,24 @@ var SearchIndex = class SearchIndex {
2514
2577
  has(id) {
2515
2578
  return this.entries.has(id);
2516
2579
  }
2580
+ remove(id) {
2581
+ const entry = this.entries.get(id);
2582
+ if (!entry) return;
2583
+ const termFreq = this.docTermCounts.get(id);
2584
+ if (termFreq) {
2585
+ for (const term of termFreq.keys()) {
2586
+ const postingList = this.invertedIndex.get(term);
2587
+ if (postingList) {
2588
+ postingList.delete(id);
2589
+ if (postingList.size === 0) this.invertedIndex.delete(term);
2590
+ }
2591
+ }
2592
+ this.docTermCounts.delete(id);
2593
+ }
2594
+ this.totalDocLength = Math.max(0, this.totalDocLength - entry.termCount);
2595
+ this.entries.delete(id);
2596
+ this.sortedTerms = null;
2597
+ }
2517
2598
  search(query, limit = 20) {
2518
2599
  const rawTerms = this.tokenize(query.toLowerCase());
2519
2600
  if (rawTerms.length === 0) return [];
@@ -2866,6 +2947,7 @@ function buildSyntheticCompression(raw) {
2866
2947
  };
2867
2948
  if (raw.modality) result.modality = raw.modality;
2868
2949
  if (raw.imageData) result.imageData = raw.imageData;
2950
+ if (raw.agentId) result.agentId = raw.agentId;
2869
2951
  return result;
2870
2952
  }
2871
2953
 
@@ -2955,6 +3037,16 @@ function setVectorIndex(idx) {
2955
3037
  function setEmbeddingProvider(provider) {
2956
3038
  currentEmbeddingProvider = provider;
2957
3039
  }
3040
+ function vectorIndexRemove(id) {
3041
+ vectorIndex?.remove(id);
3042
+ }
3043
+ let indexPersistence = null;
3044
+ function setIndexPersistence(p) {
3045
+ indexPersistence = p;
3046
+ }
3047
+ async function flushIndexSave() {
3048
+ await indexPersistence?.save();
3049
+ }
2958
3050
  const EMBED_MAX_CHARS = 16e3;
2959
3051
  function clipEmbedInput(text) {
2960
3052
  if (text.length <= EMBED_MAX_CHARS) return text;
@@ -3084,7 +3176,7 @@ async function rebuildIndex(kv) {
3084
3176
  idx.add(memoryToObservation(memory));
3085
3177
  await enqueue({
3086
3178
  id: memory.id,
3087
- sessionId: memory.sessionIds[0] ?? "memory",
3179
+ sessionId: memory.sessionIds?.[0] ?? "memory",
3088
3180
  text: memory.title + " " + memory.content,
3089
3181
  context: {
3090
3182
  kind: "memory",
@@ -3143,8 +3235,8 @@ function registerSearchFunction(sdk, kv) {
3143
3235
  if (!Number.isInteger(data.limit) || data.limit < 1) throw new Error("mem::search: limit must be a positive integer");
3144
3236
  effectiveLimit = Math.min(data.limit, MAX_LIMIT);
3145
3237
  }
3146
- const projectFilter = typeof data.project === "string" && data.project.length > 0 ? data.project : void 0;
3147
- 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;
3148
3240
  const format = typeof data.format === "string" ? data.format : "full";
3149
3241
  if (![
3150
3242
  "full",
@@ -3170,14 +3262,25 @@ function registerSearchFunction(sdk, kv) {
3170
3262
  sessionCache.set(sessionId, s ?? null);
3171
3263
  return s ?? null;
3172
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
+ };
3173
3272
  const candidates = [];
3174
3273
  for (const r of results) {
3175
3274
  if (candidates.length >= effectiveLimit) break;
3176
3275
  if (filtering) {
3177
3276
  const s = await loadSession(r.sessionId);
3178
- if (!s) continue;
3179
- if (projectFilter && s.project !== projectFilter) continue;
3180
- 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
+ }
3181
3284
  }
3182
3285
  candidates.push(r);
3183
3286
  }
@@ -3349,6 +3452,9 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3349
3452
  error: `Session observation limit reached (${maxObservationsPerSession})`
3350
3453
  };
3351
3454
  }
3455
+ const existingSession = await kv.get(KV.sessions, payload.sessionId);
3456
+ const inheritedAgentId = existingSession ? existingSession.agentId : getAgentId();
3457
+ if (inheritedAgentId) raw.agentId = inheritedAgentId;
3352
3458
  if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
3353
3459
  const { saveImageToDisk } = await Promise.resolve().then(() => image_store_exports);
3354
3460
  const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
@@ -3400,7 +3506,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3400
3506
  },
3401
3507
  action: TriggerAction.Void()
3402
3508
  });
3403
- const session = await kv.get(KV.sessions, payload.sessionId);
3509
+ const session = existingSession;
3404
3510
  if (session) {
3405
3511
  const updates = [{
3406
3512
  type: "set",
@@ -3420,6 +3526,20 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3420
3526
  });
3421
3527
  }
3422
3528
  await kv.update(KV.sessions, payload.sessionId, updates);
3529
+ } else if (typeof payload.project === "string" && payload.project.trim().length > 0 && typeof payload.cwd === "string" && payload.cwd.trim().length > 0) {
3530
+ const trimmedPrompt = typeof raw.userPrompt === "string" ? raw.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
3531
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3532
+ await kv.set(KV.sessions, payload.sessionId, {
3533
+ id: payload.sessionId,
3534
+ project: payload.project,
3535
+ cwd: payload.cwd,
3536
+ startedAt: payload.timestamp ?? ts,
3537
+ updatedAt: ts,
3538
+ status: "active",
3539
+ observationCount: 1,
3540
+ ...inheritedAgentId ? { agentId: inheritedAgentId } : {},
3541
+ ...trimmedPrompt && trimmedPrompt.length > 0 ? { firstPrompt: trimmedPrompt } : {}
3542
+ });
3423
3543
  }
3424
3544
  if (isAutoCompressEnabled()) await sdk.trigger({
3425
3545
  function_id: "mem::compress",
@@ -3912,10 +4032,10 @@ const DEFAULT_SLOTS = [
3912
4032
  }
3913
4033
  ];
3914
4034
  function isSlotsEnabled() {
3915
- return process.env["AGENTMEMORY_SLOTS"] === "true";
4035
+ return getEnvVar("AGENTMEMORY_SLOTS") === "true";
3916
4036
  }
3917
4037
  function isReflectEnabled() {
3918
- return process.env["AGENTMEMORY_REFLECT"] === "true";
4038
+ return getEnvVar("AGENTMEMORY_REFLECT") === "true";
3919
4039
  }
3920
4040
  function scopeKv(scope) {
3921
4041
  return scope === "global" ? KV.globalSlots : KV.slots;
@@ -4679,7 +4799,8 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
4679
4799
  confidence: qualityScore / 100,
4680
4800
  ...hasImage ? { modality: data.raw.modality } : {},
4681
4801
  ...imageDescription ? { imageDescription } : {},
4682
- ...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
4802
+ ...data.raw.imageData ? { imageRef: data.raw.imageData } : {},
4803
+ ...data.raw.agentId ? { agentId: data.raw.agentId } : {}
4683
4804
  };
4684
4805
  await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
4685
4806
  try {
@@ -5179,8 +5300,78 @@ function isAllowedPath(dbPath) {
5179
5300
  const resolved = resolve(dbPath);
5180
5301
  return ALLOWED_DIRS.some((dir) => resolved.startsWith(dir + "/"));
5181
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
+ }
5182
5360
  function registerMigrateFunction(sdk, kv) {
5183
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
+ };
5184
5375
  logger.info("Migration started", { dbPath: data.dbPath });
5185
5376
  if (!isAllowedPath(data.dbPath)) return {
5186
5377
  success: false,
@@ -5456,9 +5647,10 @@ function registerConsolidateFunction(sdk, kv, provider) {
5456
5647
  llmCallCount++;
5457
5648
  const parsed = parseMemoryXml(response, sessionIds);
5458
5649
  if (!parsed) continue;
5459
- const existingMatch = existingMemories.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase());
5460
5650
  const now = (/* @__PURE__ */ new Date()).toISOString();
5461
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));
5462
5654
  if (existingMatch) {
5463
5655
  existingMatch.isLatest = false;
5464
5656
  await kv.set(KV.memories, existingMatch.id, existingMatch);
@@ -5475,7 +5667,8 @@ function registerConsolidateFunction(sdk, kv, provider) {
5475
5667
  parentId: existingMatch.id,
5476
5668
  supersedes: [existingMatch.id, ...existingMatch.supersedes || []],
5477
5669
  sourceObservationIds: obsIds,
5478
- isLatest: true
5670
+ isLatest: true,
5671
+ ...scopedProject !== void 0 && { project: scopedProject }
5479
5672
  };
5480
5673
  await kv.set(KV.memories, evolved.id, evolved);
5481
5674
  await recordAudit(kv, "evolve", "mem::consolidate", [evolved.id], {
@@ -5494,7 +5687,8 @@ function registerConsolidateFunction(sdk, kv, provider) {
5494
5687
  ...parsed,
5495
5688
  sourceObservationIds: obsIds,
5496
5689
  version: 1,
5497
- isLatest: true
5690
+ isLatest: true,
5691
+ ...scopedProject !== void 0 && { project: scopedProject }
5498
5692
  };
5499
5693
  await kv.set(KV.memories, memory.id, memory);
5500
5694
  await recordAudit(kv, "remember", "mem::consolidate", [memory.id], {
@@ -5635,6 +5829,7 @@ function registerRememberFunction(sdk, kv) {
5635
5829
  "fact"
5636
5830
  ]).has(data.type || "") ? data.type : "fact";
5637
5831
  const now = (/* @__PURE__ */ new Date()).toISOString();
5832
+ const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
5638
5833
  return withKeyedLock("mem:remember", async () => {
5639
5834
  const existingMemories = await kv.list(KV.memories);
5640
5835
  let supersededId;
@@ -5643,6 +5838,7 @@ function registerRememberFunction(sdk, kv) {
5643
5838
  const lowerContent = data.content.toLowerCase();
5644
5839
  for (const existing of existingMemories) {
5645
5840
  if (existing.isLatest === false) continue;
5841
+ if (project && existing.project && existing.project !== project) continue;
5646
5842
  if (jaccardSimilarity(lowerContent, existing.content.toLowerCase()) > .7) {
5647
5843
  supersededId = existing.id;
5648
5844
  supersededVersion = existing.version ?? 1;
@@ -5650,6 +5846,7 @@ function registerRememberFunction(sdk, kv) {
5650
5846
  break;
5651
5847
  }
5652
5848
  }
5849
+ const callAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim().slice(0, 128) : getAgentId();
5653
5850
  const memory = {
5654
5851
  id: generateId("mem"),
5655
5852
  createdAt: now,
@@ -5665,7 +5862,9 @@ function registerRememberFunction(sdk, kv) {
5665
5862
  parentId: supersededId,
5666
5863
  supersedes: supersededId ? [supersededId] : [],
5667
5864
  sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
5668
- isLatest: true
5865
+ isLatest: true,
5866
+ ...callAgentId ? { agentId: callAgentId } : {},
5867
+ ...project !== void 0 && { project }
5669
5868
  };
5670
5869
  if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
5671
5870
  if (supersededMemory) {
@@ -5681,7 +5880,7 @@ function registerRememberFunction(sdk, kv) {
5681
5880
  error: err instanceof Error ? err.message : String(err)
5682
5881
  });
5683
5882
  }
5684
- 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, {
5685
5884
  kind: "memory",
5686
5885
  logId: memory.id
5687
5886
  });
@@ -5692,7 +5891,8 @@ function registerRememberFunction(sdk, kv) {
5692
5891
  });
5693
5892
  logger.info("Memory saved", {
5694
5893
  memId: memory.id,
5695
- type: memory.type
5894
+ type: memory.type,
5895
+ project: memory.project
5696
5896
  });
5697
5897
  return {
5698
5898
  success: true,
@@ -5711,6 +5911,8 @@ function registerRememberFunction(sdk, kv) {
5711
5911
  await kv.delete(KV.memories, data.memoryId);
5712
5912
  if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
5713
5913
  await deleteAccessLog(kv, data.memoryId);
5914
+ getSearchIndex().remove(data.memoryId);
5915
+ vectorIndexRemove(data.memoryId);
5714
5916
  deletedMemoryIds.push(data.memoryId);
5715
5917
  deleted++;
5716
5918
  }
@@ -5719,6 +5921,8 @@ function registerRememberFunction(sdk, kv) {
5719
5921
  await kv.delete(KV.observations(data.sessionId), obsId);
5720
5922
  if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5721
5923
  if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5924
+ getSearchIndex().remove(obsId);
5925
+ vectorIndexRemove(obsId);
5722
5926
  deletedObservationIds.push(obsId);
5723
5927
  deleted++;
5724
5928
  }
@@ -5728,6 +5932,8 @@ function registerRememberFunction(sdk, kv) {
5728
5932
  await kv.delete(KV.observations(data.sessionId), obs.id);
5729
5933
  if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5730
5934
  if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5935
+ getSearchIndex().remove(obs.id);
5936
+ vectorIndexRemove(obs.id);
5731
5937
  deletedObservationIds.push(obs.id);
5732
5938
  deleted++;
5733
5939
  }
@@ -5736,14 +5942,17 @@ function registerRememberFunction(sdk, kv) {
5736
5942
  deletedSession = true;
5737
5943
  deleted += 2;
5738
5944
  }
5739
- if (deleted > 0) await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5740
- sessionId: data.sessionId,
5741
- deleted,
5742
- memoriesDeleted: deletedMemoryIds.length,
5743
- observationsDeleted: deletedObservationIds.length,
5744
- sessionDeleted: deletedSession,
5745
- reason: "user-initiated forget"
5746
- });
5945
+ if (deleted > 0) {
5946
+ await flushIndexSave();
5947
+ await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5948
+ sessionId: data.sessionId,
5949
+ deleted,
5950
+ memoriesDeleted: deletedMemoryIds.length,
5951
+ observationsDeleted: deletedObservationIds.length,
5952
+ sessionDeleted: deletedSession,
5953
+ reason: "user-initiated forget"
5954
+ });
5955
+ }
5747
5956
  logger.info("Memory forgotten", { deleted });
5748
5957
  return {
5749
5958
  success: true,
@@ -6258,6 +6467,9 @@ async function findByKeyword(kv, keyword, project) {
6258
6467
  const LESSON_CONTENT_PREVIEW_CHARS = 240;
6259
6468
  function registerSmartSearchFunction(sdk, kv, searchFn) {
6260
6469
  sdk.registerFunction("mem::smart-search", async (data) => {
6470
+ const isolated = isAgentScopeIsolated();
6471
+ const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
6472
+ const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
6261
6473
  if (data.expandIds && data.expandIds.length > 0) {
6262
6474
  const raw = data.expandIds.slice(0, 20);
6263
6475
  const items = raw.map((entry) => {
@@ -6278,17 +6490,19 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6278
6490
  observation: obs
6279
6491
  } : null)));
6280
6492
  for (const r of results) if (r) expanded.push(r);
6281
- recordAccessBatch(kv, expanded.map((e) => e.observation.id));
6493
+ const scoped = filterAgentId ? expanded.filter((e) => e.observation.agentId === filterAgentId) : expanded;
6494
+ recordAccessBatch(kv, scoped.map((e) => e.observation.id));
6282
6495
  const truncated = data.expandIds.length > raw.length;
6283
6496
  logger.info("Smart search expanded", {
6284
6497
  requested: data.expandIds.length,
6285
6498
  attempted: raw.length,
6286
- returned: expanded.length,
6499
+ returned: scoped.length,
6500
+ filteredOutOfScope: expanded.length - scoped.length,
6287
6501
  truncated
6288
6502
  });
6289
6503
  return {
6290
6504
  mode: "expanded",
6291
- results: expanded,
6505
+ results: scoped,
6292
6506
  truncated
6293
6507
  };
6294
6508
  }
@@ -6300,8 +6514,9 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6300
6514
  const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
6301
6515
  const lessonLimit = Math.min(limit, 10);
6302
6516
  const includeLessons = data.includeLessons !== false;
6303
- const [hybridResults, lessons] = await Promise.all([searchFn(data.query, limit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
6304
- const compact = hybridResults.map((r) => ({
6517
+ const overFetchLimit = filterAgentId ? Math.min(limit * 3, 300) : limit;
6518
+ const [hybridResults, lessons] = await Promise.all([searchFn(data.query, overFetchLimit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
6519
+ const compact = (filterAgentId ? hybridResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : hybridResults.slice(0, limit)).map((r) => ({
6305
6520
  obsId: r.observation.id,
6306
6521
  sessionId: r.sessionId,
6307
6522
  title: r.observation.title,
@@ -6480,6 +6695,8 @@ function registerAutoForgetFunction(sdk, kv) {
6480
6695
  timestamp: mem.forgetAfter
6481
6696
  });
6482
6697
  await deleteAccessLog(kv, mem.id);
6698
+ getSearchIndex().remove(mem.id);
6699
+ vectorIndexRemove(mem.id);
6483
6700
  }
6484
6701
  }
6485
6702
  }
@@ -6557,10 +6774,13 @@ function registerAutoForgetFunction(sdk, kv) {
6557
6774
  sessionId: sessions[i].id,
6558
6775
  timestamp: obs.timestamp
6559
6776
  });
6777
+ getSearchIndex().remove(obs.id);
6778
+ vectorIndexRemove(obs.id);
6560
6779
  }
6561
6780
  }
6562
6781
  }
6563
6782
  }
6783
+ if (!dryRun && (result.ttlExpired.length > 0 || result.lowValueObs.length > 0)) await flushIndexSave();
6564
6784
  logger.info("Auto-forget complete", {
6565
6785
  ttlExpired: result.ttlExpired.length,
6566
6786
  contradictions: result.contradictions.length,
@@ -6573,7 +6793,7 @@ function registerAutoForgetFunction(sdk, kv) {
6573
6793
 
6574
6794
  //#endregion
6575
6795
  //#region src/version.ts
6576
- const VERSION = "0.9.21";
6796
+ const VERSION = "0.9.23";
6577
6797
 
6578
6798
  //#endregion
6579
6799
  //#region src/functions/export-import.ts
@@ -6712,7 +6932,9 @@ function registerExportImportFunction(sdk, kv) {
6712
6932
  "0.9.18",
6713
6933
  "0.9.19",
6714
6934
  "0.9.20",
6715
- "0.9.21"
6935
+ "0.9.21",
6936
+ "0.9.22",
6937
+ "0.9.23"
6716
6938
  ]).has(importData.version)) return {
6717
6939
  success: false,
6718
6940
  error: `Unsupported export version: ${importData.version}`
@@ -6835,6 +7057,7 @@ function registerExportImportFunction(sdk, kv) {
6835
7057
  continue;
6836
7058
  }
6837
7059
  }
7060
+ if (!Array.isArray(memory.sessionIds)) memory.sessionIds = [];
6838
7061
  await kv.set(KV.memories, memory.id, memory);
6839
7062
  stats.memories++;
6840
7063
  }
@@ -7038,6 +7261,7 @@ function escapeXml(s) {
7038
7261
  }
7039
7262
  function registerEnrichFunction(sdk, kv) {
7040
7263
  sdk.registerFunction("mem::enrich", async (data) => {
7264
+ const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
7041
7265
  const parts = [];
7042
7266
  const fileContextPromise = sdk.trigger({
7043
7267
  function_id: "mem::file-context",
@@ -7051,10 +7275,11 @@ function registerEnrichFunction(sdk, kv) {
7051
7275
  function_id: "mem::search",
7052
7276
  payload: {
7053
7277
  query: searchQueries.join(" "),
7054
- limit: 5
7278
+ limit: 5,
7279
+ ...project !== void 0 && { project }
7055
7280
  }
7056
7281
  }).catch(() => ({ results: [] })) : Promise.resolve({ results: [] });
7057
- 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(() => []);
7058
7283
  const [fileContext, searchResult, bugMemories] = await Promise.all([
7059
7284
  fileContextPromise,
7060
7285
  searchPromise,
@@ -7077,6 +7302,7 @@ function registerEnrichFunction(sdk, kv) {
7077
7302
  }
7078
7303
  logger.info("Enrichment completed", {
7079
7304
  sessionId: data.sessionId,
7305
+ project,
7080
7306
  fileCount: data.files.length,
7081
7307
  contextLength: context.length,
7082
7308
  truncated
@@ -7234,16 +7460,24 @@ function buildGraphExtractionPrompt(observations) {
7234
7460
 
7235
7461
  //#endregion
7236
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
+ }
7237
7470
  function parseGraphXml(xml, observationIds) {
7238
7471
  const nodes = [];
7239
7472
  const edges = [];
7240
7473
  const now = (/* @__PURE__ */ new Date()).toISOString();
7241
- const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/entity>/g;
7242
- let match;
7243
- while ((match = entityRegex.exec(xml)) !== null) {
7244
- const type = match[1];
7245
- const name = match[2];
7246
- 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;
7247
7481
  const properties = {};
7248
7482
  const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
7249
7483
  let propMatch;
@@ -7256,17 +7490,23 @@ function parseGraphXml(xml, observationIds) {
7256
7490
  sourceObservationIds: observationIds,
7257
7491
  createdAt: now
7258
7492
  });
7259
- }
7260
- 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;
7261
7498
  while ((match = relRegex.exec(xml)) !== null) {
7262
- const type = match[1];
7263
- const sourceName = match[2];
7264
- const targetName = match[3];
7265
- const parsedWeight = parseFloat(match[4]);
7266
- 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;
7267
7506
  const sourceNode = nodes.find((n) => n.name === sourceName);
7268
7507
  const targetNode = nodes.find((n) => n.name === targetName);
7269
- if (sourceNode && targetNode) edges.push({
7508
+ if (!sourceNode || !targetNode) continue;
7509
+ edges.push({
7270
7510
  id: generateId("ge"),
7271
7511
  type,
7272
7512
  sourceNodeId: sourceNode.id,
@@ -7480,7 +7720,7 @@ function registerConsolidationPipelineFunction(sdk, kv, provider) {
7480
7720
  if (!data?.force && !isConsolidationEnabled()) return {
7481
7721
  success: false,
7482
7722
  skipped: true,
7483
- 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)"
7484
7724
  };
7485
7725
  const tier = data?.tier || "all";
7486
7726
  const decayDays = getConsolidationDecayDays();
@@ -7771,8 +8011,11 @@ function registerGovernanceFunction(sdk, kv) {
7771
8011
  for (const id of data.memoryIds) if (await kv.get(KV.memories, id)) {
7772
8012
  await kv.delete(KV.memories, id);
7773
8013
  await deleteAccessLog(kv, id);
8014
+ getSearchIndex().remove(id);
8015
+ vectorIndexRemove(id);
7774
8016
  deleted++;
7775
8017
  }
8018
+ if (deleted > 0) await flushIndexSave();
7776
8019
  await recordAudit(kv, "delete", "mem::governance-delete", data.memoryIds, {
7777
8020
  reason: data.reason || "manual deletion",
7778
8021
  deleted
@@ -7825,6 +8068,8 @@ function registerGovernanceFunction(sdk, kv) {
7825
8068
  (await Promise.allSettled(batch.map(async (mem) => {
7826
8069
  await kv.delete(KV.memories, mem.id);
7827
8070
  await deleteAccessLog(kv, mem.id);
8071
+ getSearchIndex().remove(mem.id);
8072
+ vectorIndexRemove(mem.id);
7828
8073
  }))).forEach((result, j) => {
7829
8074
  const mem = batch[j];
7830
8075
  if (result.status === "fulfilled") successfulIds.push(mem.id);
@@ -7840,6 +8085,7 @@ function registerGovernanceFunction(sdk, kv) {
7840
8085
  }
7841
8086
  });
7842
8087
  }
8088
+ if (successfulIds.length > 0) await flushIndexSave();
7843
8089
  await safeAudit(kv, "delete", "mem::governance-bulk", successfulIds, {
7844
8090
  filter: data,
7845
8091
  deleted: successfulIds.length,
@@ -10644,11 +10890,34 @@ function registerDiagnosticsFunction(sdk, kv) {
10644
10890
  });
10645
10891
  memoryIssues++;
10646
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
+ });
10647
10916
  if (memoryIssues === 0) checks.push({
10648
10917
  name: "memories-ok",
10649
10918
  category: "memories",
10650
10919
  status: "pass",
10651
- message: `All ${memories.length} memories are consistent`,
10920
+ message: `All ${memories.length} memories are structurally consistent`,
10652
10921
  fixable: false
10653
10922
  });
10654
10923
  }
@@ -13239,6 +13508,8 @@ function registerRetentionFunctions(sdk, kv) {
13239
13508
  await kv.delete(scope, candidate.memoryId);
13240
13509
  await kv.delete(KV.retentionScores, candidate.memoryId);
13241
13510
  await deleteAccessLog(kv, candidate.memoryId);
13511
+ getSearchIndex().remove(candidate.memoryId);
13512
+ vectorIndexRemove(candidate.memoryId);
13242
13513
  evicted++;
13243
13514
  evictedIds.push(candidate.memoryId);
13244
13515
  if (resolvedSource === "semantic") evictedSemantic++;
@@ -13246,13 +13517,16 @@ function registerRetentionFunctions(sdk, kv) {
13246
13517
  } catch {
13247
13518
  continue;
13248
13519
  }
13249
- if (evicted > 0) await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
13250
- threshold,
13251
- evicted,
13252
- evictedEpisodic,
13253
- evictedSemantic,
13254
- reason: "retention score below threshold"
13255
- });
13520
+ if (evicted > 0) {
13521
+ await flushIndexSave();
13522
+ await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
13523
+ threshold,
13524
+ evicted,
13525
+ evictedEpisodic,
13526
+ evictedSemantic,
13527
+ reason: "retention score below threshold"
13528
+ });
13529
+ }
13256
13530
  logger.info("Retention-based eviction complete", {
13257
13531
  evicted,
13258
13532
  evictedEpisodic,
@@ -14157,107 +14431,340 @@ function renderViewerDocument() {
14157
14431
  }
14158
14432
 
14159
14433
  //#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
- };
14434
+ //#region src/viewer/server.ts
14435
+ function loadViewerFavicon() {
14436
+ const base = dirname(fileURLToPath(import.meta.url));
14437
+ const candidates = [
14438
+ join(base, "..", "src", "viewer", "favicon.svg"),
14439
+ join(base, "..", "viewer", "favicon.svg"),
14440
+ join(base, "viewer", "favicon.svg")
14441
+ ];
14442
+ for (const path of candidates) try {
14443
+ return readFileSync(path);
14444
+ } catch {}
14173
14445
  return null;
14174
14446
  }
14175
- function requireConfiguredSecret(secret, feature) {
14176
- if (secret) return null;
14177
- return {
14178
- status_code: 503,
14179
- body: { error: `${feature} requires AGENTMEMORY_SECRET` }
14180
- };
14447
+ 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());
14448
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
14449
+ function buildAllowedHosts(origins, listenPort) {
14450
+ const hosts = /* @__PURE__ */ new Set();
14451
+ for (const o of origins) try {
14452
+ const parsed = new URL(o);
14453
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
14454
+ } catch {}
14455
+ hosts.add(`localhost:${listenPort}`);
14456
+ hosts.add(`127.0.0.1:${listenPort}`);
14457
+ hosts.add(`[::1]:${listenPort}`);
14458
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
14459
+ return hosts;
14181
14460
  }
14182
- function flagDisabledResponse(opts) {
14461
+ function isHostAllowed(headerHost, allowed) {
14462
+ if (typeof headerHost !== "string") return false;
14463
+ const lower = headerHost.toLowerCase().trim();
14464
+ if (!lower) return false;
14465
+ return allowed.has(lower);
14466
+ }
14467
+ function corsHeaders(req) {
14468
+ const origin = req.headers.origin || "";
14469
+ const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
14183
14470
  return {
14184
- status_code: 503,
14185
- body: opts
14471
+ "Access-Control-Allow-Origin": allowed,
14472
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
14473
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
14474
+ Vary: "Origin"
14186
14475
  };
14187
14476
  }
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"
14477
+ function json(res, status, data, req) {
14478
+ const body = JSON.stringify(data);
14479
+ const cors = req ? corsHeaders(req) : {
14480
+ "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
14481
+ Vary: "Origin"
14482
+ };
14483
+ res.writeHead(status, {
14484
+ ...cors,
14485
+ "Content-Type": "application/json"
14194
14486
  });
14487
+ res.end(body);
14195
14488
  }
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"
14489
+ function readBody(req) {
14490
+ return new Promise((resolve, reject) => {
14491
+ let data = "";
14492
+ let size = 0;
14493
+ req.on("data", (chunk) => {
14494
+ size += chunk.length;
14495
+ if (size > 1e6) {
14496
+ req.destroy();
14497
+ reject(/* @__PURE__ */ new Error("too large"));
14498
+ return;
14499
+ }
14500
+ data += chunk.toString();
14501
+ });
14502
+ req.on("end", () => resolve(data));
14503
+ req.on("error", reject);
14202
14504
  });
14203
14505
  }
14204
- function asNonEmptyString$1(value) {
14205
- if (typeof value !== "string") return null;
14206
- const trimmed = value.trim();
14207
- return trimmed ? trimmed : null;
14208
- }
14209
- function parseOptionalFiniteNumber(value) {
14210
- if (value === void 0 || value === null) return void 0;
14211
- if (typeof value === "number") return Number.isFinite(value) ? value : null;
14212
- if (typeof value === "string") {
14213
- const trimmed = value.trim();
14214
- if (!trimmed) return void 0;
14215
- const parsed = Number(trimmed);
14216
- return Number.isFinite(parsed) ? parsed : null;
14217
- }
14218
- return null;
14506
+ const MAX_VIEWER_PORT_RETRIES = 10;
14507
+ let boundViewerPort = null;
14508
+ let viewerSkipped = false;
14509
+ function getBoundViewerPort() {
14510
+ return boundViewerPort;
14219
14511
  }
14220
- function parseOptionalPositiveInt(value) {
14221
- const parsed = parseOptionalFiniteNumber(value);
14222
- if (parsed === void 0 || parsed === null) return parsed;
14223
- if (!Number.isInteger(parsed) || parsed < 1) return null;
14224
- return parsed;
14512
+ function getViewerSkipped() {
14513
+ return viewerSkipped;
14225
14514
  }
14226
- function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14227
- sdk.registerFunction("middleware::api-auth", async (input) => {
14228
- if (!secret) return { action: "continue" };
14229
- const headers = input?.request?.headers || {};
14230
- const auth = headers["authorization"] || headers["Authorization"];
14231
- if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
14232
- action: "respond",
14233
- response: {
14234
- status_code: 401,
14235
- body: { error: "unauthorized" }
14236
- }
14237
- };
14238
- return { action: "continue" };
14239
- });
14240
- sdk.registerFunction("api::liveness", async () => ({
14241
- status_code: 200,
14242
- body: {
14243
- status: "ok",
14244
- service: "agentmemory"
14515
+ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14516
+ boundViewerPort = null;
14517
+ viewerSkipped = false;
14518
+ const resolvedRestPort = restPort ?? port - 2;
14519
+ const requestedPort = port;
14520
+ let allowedHosts = null;
14521
+ const server = createServer(async (req, res) => {
14522
+ if (!allowedHosts) {
14523
+ const addr = server.address();
14524
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
14245
14525
  }
14246
- }));
14247
- sdk.registerTrigger({
14248
- type: "http",
14249
- function_id: "api::liveness",
14250
- config: {
14251
- api_path: "/agentmemory/livez",
14252
- http_method: "GET"
14526
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
14527
+ res.writeHead(403, { "Content-Type": "text/plain" });
14528
+ res.end("forbidden host");
14529
+ return;
14253
14530
  }
14254
- });
14255
- sdk.registerFunction("api::config-flags", async (req) => {
14256
- const authErr = checkAuth(req, secret);
14257
- if (authErr) return authErr;
14258
- return {
14259
- status_code: 200,
14260
- body: {
14531
+ const raw = req.url || "/";
14532
+ const qIdx = raw.indexOf("?");
14533
+ const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
14534
+ const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
14535
+ const method = req.method || "GET";
14536
+ if (method === "OPTIONS") {
14537
+ res.writeHead(204, {
14538
+ ...corsHeaders(req),
14539
+ "Access-Control-Max-Age": "86400"
14540
+ });
14541
+ res.end();
14542
+ return;
14543
+ }
14544
+ if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
14545
+ const rendered = renderViewerDocument();
14546
+ if (rendered.found) {
14547
+ res.writeHead(200, {
14548
+ "Content-Type": "text/html; charset=utf-8",
14549
+ "Content-Security-Policy": rendered.csp,
14550
+ "Cache-Control": "no-cache"
14551
+ });
14552
+ res.end(rendered.html);
14553
+ return;
14554
+ }
14555
+ res.writeHead(404, { "Content-Type": "text/plain" });
14556
+ res.end("viewer not found");
14557
+ return;
14558
+ }
14559
+ if (method === "GET" && pathname === "/favicon.svg") {
14560
+ const favicon = loadViewerFavicon();
14561
+ if (favicon) {
14562
+ res.writeHead(200, {
14563
+ "Content-Type": "image/svg+xml",
14564
+ "Cache-Control": "public, max-age=3600"
14565
+ });
14566
+ res.end(favicon);
14567
+ return;
14568
+ }
14569
+ res.writeHead(404, { "Content-Type": "text/plain" });
14570
+ res.end("favicon not found");
14571
+ return;
14572
+ }
14573
+ try {
14574
+ await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
14575
+ } catch (err) {
14576
+ console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
14577
+ json(res, 502, { error: "upstream error" }, req);
14578
+ }
14579
+ });
14580
+ let attempt = 0;
14581
+ let currentPort = requestedPort;
14582
+ const tryListen = () => {
14583
+ server.listen(currentPort, "127.0.0.1");
14584
+ };
14585
+ server.on("listening", () => {
14586
+ const addr = server.address();
14587
+ boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
14588
+ viewerSkipped = false;
14589
+ if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
14590
+ else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
14591
+ });
14592
+ server.on("error", (err) => {
14593
+ if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
14594
+ attempt++;
14595
+ currentPort = requestedPort + attempt;
14596
+ setImmediate(tryListen);
14597
+ return;
14598
+ }
14599
+ if (err.code === "EADDRINUSE") {
14600
+ boundViewerPort = null;
14601
+ viewerSkipped = true;
14602
+ console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
14603
+ } else {
14604
+ boundViewerPort = null;
14605
+ viewerSkipped = true;
14606
+ console.error(`[agentmemory] Viewer error:`, err.message);
14607
+ }
14608
+ });
14609
+ tryListen();
14610
+ return server;
14611
+ }
14612
+ async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
14613
+ const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
14614
+ const headers = {};
14615
+ if (secret) headers["Authorization"] = `Bearer ${secret}`;
14616
+ const ct = req.headers["content-type"];
14617
+ if (ct) headers["Content-Type"] = ct;
14618
+ let body;
14619
+ if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
14620
+ const controller = new AbortController();
14621
+ const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
14622
+ let upstream;
14623
+ try {
14624
+ upstream = await fetch(upstreamUrl, {
14625
+ method,
14626
+ headers,
14627
+ body: body || void 0,
14628
+ signal: controller.signal
14629
+ });
14630
+ clearTimeout(fetchTimeout);
14631
+ } catch (err) {
14632
+ clearTimeout(fetchTimeout);
14633
+ if (err instanceof Error && err.name === "AbortError") {
14634
+ json(res, 504, { error: "upstream timeout" }, req);
14635
+ return;
14636
+ }
14637
+ throw err;
14638
+ }
14639
+ const cors = corsHeaders(req);
14640
+ const responseBody = await upstream.text();
14641
+ const responseHeaders = { ...cors };
14642
+ const upstreamCt = upstream.headers.get("content-type");
14643
+ if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
14644
+ res.writeHead(upstream.status, responseHeaders);
14645
+ res.end(responseBody);
14646
+ }
14647
+
14648
+ //#endregion
14649
+ //#region src/triggers/api.ts
14650
+ function parseOptionalInt(raw) {
14651
+ if (raw === void 0 || raw === null || raw === "") return void 0;
14652
+ const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
14653
+ return Number.isFinite(n) ? n : void 0;
14654
+ }
14655
+ function checkAuth(req, secret) {
14656
+ if (!secret) return null;
14657
+ const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
14658
+ if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
14659
+ status_code: 401,
14660
+ body: { error: "unauthorized" }
14661
+ };
14662
+ return null;
14663
+ }
14664
+ function requireConfiguredSecret(secret, feature) {
14665
+ if (secret) return null;
14666
+ return {
14667
+ status_code: 503,
14668
+ body: { error: `${feature} requires AGENTMEMORY_SECRET` }
14669
+ };
14670
+ }
14671
+ function flagDisabledResponse(opts) {
14672
+ return {
14673
+ status_code: 503,
14674
+ body: opts
14675
+ };
14676
+ }
14677
+ function graphDisabledResponse() {
14678
+ return flagDisabledResponse({
14679
+ error: "Knowledge graph not enabled",
14680
+ flag: "GRAPH_EXTRACTION_ENABLED",
14681
+ enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and restart. Requires an LLM provider key.",
14682
+ docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
14683
+ });
14684
+ }
14685
+ function consolidationDisabledResponse() {
14686
+ return flagDisabledResponse({
14687
+ error: "Consolidation pipeline not enabled",
14688
+ flag: "CONSOLIDATION_ENABLED",
14689
+ enableHow: "Set CONSOLIDATION_ENABLED=true and restart. Requires an LLM provider key.",
14690
+ docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
14691
+ });
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
+ }
14709
+ function asNonEmptyString$1(value) {
14710
+ if (typeof value !== "string") return null;
14711
+ const trimmed = value.trim();
14712
+ return trimmed ? trimmed : null;
14713
+ }
14714
+ function parseOptionalFiniteNumber(value) {
14715
+ if (value === void 0 || value === null) return void 0;
14716
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
14717
+ if (typeof value === "string") {
14718
+ const trimmed = value.trim();
14719
+ if (!trimmed) return void 0;
14720
+ const parsed = Number(trimmed);
14721
+ return Number.isFinite(parsed) ? parsed : null;
14722
+ }
14723
+ return null;
14724
+ }
14725
+ function parseOptionalPositiveInt(value) {
14726
+ const parsed = parseOptionalFiniteNumber(value);
14727
+ if (parsed === void 0 || parsed === null) return parsed;
14728
+ if (!Number.isInteger(parsed) || parsed < 1) return null;
14729
+ return parsed;
14730
+ }
14731
+ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14732
+ sdk.registerFunction("middleware::api-auth", async (input) => {
14733
+ if (!secret) return { action: "continue" };
14734
+ const headers = input?.request?.headers || {};
14735
+ const auth = headers["authorization"] || headers["Authorization"];
14736
+ if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
14737
+ action: "respond",
14738
+ response: {
14739
+ status_code: 401,
14740
+ body: { error: "unauthorized" }
14741
+ }
14742
+ };
14743
+ return { action: "continue" };
14744
+ });
14745
+ sdk.registerFunction("api::liveness", async () => ({
14746
+ status_code: 200,
14747
+ body: {
14748
+ status: "ok",
14749
+ service: "agentmemory",
14750
+ viewerPort: getBoundViewerPort(),
14751
+ viewerSkipped: getViewerSkipped()
14752
+ }
14753
+ }));
14754
+ sdk.registerTrigger({
14755
+ type: "http",
14756
+ function_id: "api::liveness",
14757
+ config: {
14758
+ api_path: "/agentmemory/livez",
14759
+ http_method: "GET"
14760
+ }
14761
+ });
14762
+ sdk.registerFunction("api::config-flags", async (req) => {
14763
+ const authErr = checkAuth(req, secret);
14764
+ if (authErr) return authErr;
14765
+ return {
14766
+ status_code: 200,
14767
+ body: {
14261
14768
  version: VERSION,
14262
14769
  provider: detectLlmProviderKind(),
14263
14770
  embeddingProvider: detectEmbeddingProvider() ? "embeddings" : "none",
@@ -14336,7 +14843,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14336
14843
  version: VERSION,
14337
14844
  health: health || null,
14338
14845
  functionMetrics,
14339
- circuitBreaker
14846
+ circuitBreaker,
14847
+ viewerPort: getBoundViewerPort(),
14848
+ viewerSkipped: getViewerSkipped()
14340
14849
  }
14341
14850
  };
14342
14851
  });
@@ -14590,6 +15099,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14590
15099
  body: { error: "sessionId, project, and cwd are required non-empty strings" }
14591
15100
  };
14592
15101
  const title = typeof body.title === "string" ? body.title.trim() : void 0;
15102
+ const agentId = (typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim().slice(0, 128) : void 0) ?? getAgentId();
14593
15103
  const session = {
14594
15104
  id: sessionId,
14595
15105
  project,
@@ -14598,7 +15108,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14598
15108
  status: "active",
14599
15109
  observationCount: 0,
14600
15110
  ...title ? { summary: title.slice(0, 200) } : {},
14601
- ...title ? { firstPrompt: title.slice(0, 200) } : {}
15111
+ ...title ? { firstPrompt: title.slice(0, 200) } : {},
15112
+ ...agentId ? { agentId } : {}
14602
15113
  };
14603
15114
  await kv.set(KV.sessions, sessionId, session);
14604
15115
  return {
@@ -14639,6 +15150,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14639
15150
  path: "status",
14640
15151
  value: "completed"
14641
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
+ }
14642
15161
  return {
14643
15162
  status_code: 200,
14644
15163
  body: { success: true }
@@ -14785,9 +15304,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14785
15304
  sdk.registerFunction("api::sessions", async (req) => {
14786
15305
  const authErr = checkAuth(req, secret);
14787
15306
  if (authErr) return authErr;
15307
+ const sessions = await kv.list(KV.sessions);
15308
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
15309
+ const wildcardAgent = normalizedAgentId === "*";
15310
+ const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
14788
15311
  return {
14789
15312
  status_code: 200,
14790
- body: { sessions: await kv.list(KV.sessions) }
15313
+ body: { sessions: filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions }
14791
15314
  };
14792
15315
  });
14793
15316
  sdk.registerTrigger({
@@ -14806,9 +15329,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14806
15329
  status_code: 400,
14807
15330
  body: { error: "sessionId required" }
14808
15331
  };
15332
+ const observations = await kv.list(KV.observations(sessionId));
15333
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
15334
+ const wildcardAgent = normalizedAgentId === "*";
15335
+ const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
14809
15336
  return {
14810
15337
  status_code: 200,
14811
- body: { observations: await kv.list(KV.observations(sessionId)) }
15338
+ body: { observations: filterAgentId ? observations.filter((o) => o.agentId === filterAgentId) : observations }
14812
15339
  };
14813
15340
  });
14814
15341
  sdk.registerTrigger({
@@ -14849,11 +15376,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14849
15376
  status_code: 400,
14850
15377
  body: { error: "terms must be an array of strings" }
14851
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
+ };
14852
15383
  return {
14853
15384
  status_code: 200,
14854
15385
  body: await sdk.trigger({
14855
15386
  function_id: "mem::enrich",
14856
- 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
+ }
14857
15394
  })
14858
15395
  };
14859
15396
  });
@@ -14872,11 +15409,23 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14872
15409
  status_code: 400,
14873
15410
  body: { error: "content is required" }
14874
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
+ };
14875
15416
  return {
14876
15417
  status_code: 201,
14877
15418
  body: await sdk.trigger({
14878
15419
  function_id: "mem::remember",
14879
- 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
+ }
14880
15429
  })
14881
15430
  };
14882
15431
  });
@@ -14971,15 +15520,21 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14971
15520
  sdk.registerFunction("api::migrate", async (req) => {
14972
15521
  const authErr = checkAuth(req, secret);
14973
15522
  if (authErr) return authErr;
14974
- 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 {
14975
15526
  status_code: 400,
14976
- body: { error: "dbPath is required" }
15527
+ body: { error: "Either step (string) or dbPath (string) is required" }
14977
15528
  };
14978
15529
  return {
14979
15530
  status_code: 200,
14980
15531
  body: await sdk.trigger({
14981
15532
  function_id: "mem::migrate",
14982
- 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
+ }
14983
15538
  })
14984
15539
  };
14985
15540
  });
@@ -15084,11 +15639,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15084
15639
  sdk.registerFunction("api::export", async (req) => {
15085
15640
  const authErr = checkAuth(req, secret);
15086
15641
  if (authErr) return authErr;
15642
+ const rawMax = req.query_params?.["maxSessions"];
15643
+ const rawOffset = req.query_params?.["offset"];
15644
+ const payload = {};
15645
+ if (typeof rawMax === "string") {
15646
+ const n = Number(rawMax);
15647
+ if (Number.isInteger(n) && n > 0) payload.maxSessions = n;
15648
+ }
15649
+ if (typeof rawOffset === "string") {
15650
+ const n = Number(rawOffset);
15651
+ if (Number.isInteger(n) && n >= 0) payload.offset = n;
15652
+ }
15087
15653
  return {
15088
15654
  status_code: 200,
15089
15655
  body: await sdk.trigger({
15090
15656
  function_id: "mem::export",
15091
- payload: {}
15657
+ payload
15092
15658
  })
15093
15659
  };
15094
15660
  });
@@ -15314,6 +15880,63 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15314
15880
  http_method: "POST"
15315
15881
  }
15316
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
+ });
15317
15940
  sdk.registerFunction("api::consolidate-pipeline", async (req) => {
15318
15941
  const authErr = checkAuth(req, secret);
15319
15942
  if (authErr) return authErr;
@@ -15574,9 +16197,35 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15574
16197
  const authErr = checkAuth(req, secret);
15575
16198
  if (authErr) return authErr;
15576
16199
  const memories = await kv.list(KV.memories);
16200
+ const latest = req.query_params?.["latest"] === "true";
16201
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
16202
+ const wildcardAgent = normalizedAgentId === "*";
16203
+ const explicitAgentId = normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0;
16204
+ const includeOrphans = req.query_params?.["includeOrphans"] === "true";
16205
+ const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
16206
+ let filtered = latest ? memories.filter((m) => m.isLatest) : memories;
16207
+ if (filterAgentId) filtered = filtered.filter((m) => m.agentId === filterAgentId || includeOrphans && m.agentId === void 0);
16208
+ if (req.query_params?.["count"] === "true") return {
16209
+ status_code: 200,
16210
+ body: {
16211
+ total: filtered.length,
16212
+ latestCount: filtered.filter((m) => m.isLatest).length
16213
+ }
16214
+ };
16215
+ const rawLimit = req.query_params?.["limit"];
16216
+ const rawOffset = req.query_params?.["offset"];
16217
+ const parsedLimit = typeof rawLimit === "string" ? Number(rawLimit) : NaN;
16218
+ const parsedOffset = typeof rawOffset === "string" ? Number(rawOffset) : NaN;
16219
+ const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 5e3) : void 0;
16220
+ const offset = Number.isInteger(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
15577
16221
  return {
15578
16222
  status_code: 200,
15579
- body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
16223
+ body: {
16224
+ memories: limit !== void 0 ? filtered.slice(offset, offset + limit) : filtered,
16225
+ total: filtered.length,
16226
+ offset,
16227
+ limit: limit ?? null
16228
+ }
15580
16229
  };
15581
16230
  });
15582
16231
  sdk.registerTrigger({
@@ -15745,6 +16394,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15745
16394
  sdk.registerFunction("api::slot-list", async (req) => {
15746
16395
  const authErr = checkAuth(req, secret);
15747
16396
  if (authErr) return authErr;
16397
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15748
16398
  return {
15749
16399
  status_code: 200,
15750
16400
  body: await sdk.trigger({
@@ -15764,6 +16414,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15764
16414
  sdk.registerFunction("api::slot-get", async (req) => {
15765
16415
  const authErr = checkAuth(req, secret);
15766
16416
  if (authErr) return authErr;
16417
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15767
16418
  const label = asNonEmptyString$1(req.query_params?.["label"]);
15768
16419
  if (!label) return {
15769
16420
  status_code: 400,
@@ -15794,6 +16445,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15794
16445
  sdk.registerFunction("api::slot-create", async (req) => {
15795
16446
  const authErr = checkAuth(req, secret);
15796
16447
  if (authErr) return authErr;
16448
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15797
16449
  const body = req.body ?? {};
15798
16450
  const label = asNonEmptyString$1(body["label"]);
15799
16451
  if (!label) return {
@@ -15856,6 +16508,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15856
16508
  sdk.registerFunction("api::slot-append", async (req) => {
15857
16509
  const authErr = checkAuth(req, secret);
15858
16510
  if (authErr) return authErr;
16511
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15859
16512
  const body = req.body ?? {};
15860
16513
  const label = asNonEmptyString$1(body["label"]);
15861
16514
  const text = typeof body["text"] === "string" ? body["text"] : null;
@@ -15895,6 +16548,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15895
16548
  sdk.registerFunction("api::slot-replace", async (req) => {
15896
16549
  const authErr = checkAuth(req, secret);
15897
16550
  if (authErr) return authErr;
16551
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15898
16552
  const body = req.body ?? {};
15899
16553
  const label = asNonEmptyString$1(body["label"]);
15900
16554
  const content = body["content"];
@@ -15934,6 +16588,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15934
16588
  sdk.registerFunction("api::slot-delete", async (req) => {
15935
16589
  const authErr = checkAuth(req, secret);
15936
16590
  if (authErr) return authErr;
16591
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
15937
16592
  const label = asNonEmptyString$1(req.query_params?.["label"]);
15938
16593
  if (!label) return {
15939
16594
  status_code: 400,
@@ -15964,6 +16619,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15964
16619
  sdk.registerFunction("api::slot-reflect", async (req) => {
15965
16620
  const authErr = checkAuth(req, secret);
15966
16621
  if (authErr) return authErr;
16622
+ if (!isSlotsEnabled()) return slotsDisabledResponse();
16623
+ if (!isReflectEnabled()) return reflectDisabledResponse();
15967
16624
  const body = req.body ?? {};
15968
16625
  const sessionId = asNonEmptyString$1(body["sessionId"]);
15969
16626
  if (!sessionId) return {
@@ -17616,6 +18273,10 @@ const CORE_TOOLS = [
17616
18273
  files: {
17617
18274
  type: "string",
17618
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."
17619
18280
  }
17620
18281
  },
17621
18282
  required: ["content"]
@@ -18656,8 +19317,8 @@ function getAllTools() {
18656
19317
  ];
18657
19318
  }
18658
19319
  function getVisibleTools() {
18659
- if ((process.env["AGENTMEMORY_TOOLS"] || "core") === "all") return getAllTools();
18660
- return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
19320
+ if ((process.env["AGENTMEMORY_TOOLS"] || "all") === "core") return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
19321
+ return getAllTools();
18661
19322
  }
18662
19323
 
18663
19324
  //#endregion
@@ -18772,13 +19433,15 @@ function registerMcpEndpoints(sdk, kv, secret) {
18772
19433
  const type = args.type || "fact";
18773
19434
  const concepts = typeof args.concepts === "string" ? args.concepts.split(",").map((c) => c.trim()).filter(Boolean) : [];
18774
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;
18775
19437
  const result = await sdk.trigger({
18776
19438
  function_id: "mem::remember",
18777
19439
  payload: {
18778
19440
  content: args.content,
18779
19441
  type,
18780
19442
  concepts,
18781
- files
19443
+ files,
19444
+ ...project !== void 0 && { project }
18782
19445
  }
18783
19446
  });
18784
19447
  return {
@@ -20295,201 +20958,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
20295
20958
  });
20296
20959
  }
20297
20960
 
20298
- //#endregion
20299
- //#region src/viewer/server.ts
20300
- function loadViewerFavicon() {
20301
- const base = dirname(fileURLToPath(import.meta.url));
20302
- const candidates = [
20303
- join(base, "..", "src", "viewer", "favicon.svg"),
20304
- join(base, "..", "viewer", "favicon.svg"),
20305
- join(base, "viewer", "favicon.svg")
20306
- ];
20307
- for (const path of candidates) try {
20308
- return readFileSync(path);
20309
- } catch {}
20310
- return null;
20311
- }
20312
- 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());
20313
- const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
20314
- function buildAllowedHosts(origins, listenPort) {
20315
- const hosts = /* @__PURE__ */ new Set();
20316
- for (const o of origins) try {
20317
- const parsed = new URL(o);
20318
- if (parsed.host) hosts.add(parsed.host.toLowerCase());
20319
- } catch {}
20320
- hosts.add(`localhost:${listenPort}`);
20321
- hosts.add(`127.0.0.1:${listenPort}`);
20322
- hosts.add(`[::1]:${listenPort}`);
20323
- for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
20324
- return hosts;
20325
- }
20326
- function isHostAllowed(headerHost, allowed) {
20327
- if (typeof headerHost !== "string") return false;
20328
- const lower = headerHost.toLowerCase().trim();
20329
- if (!lower) return false;
20330
- return allowed.has(lower);
20331
- }
20332
- function corsHeaders(req) {
20333
- const origin = req.headers.origin || "";
20334
- const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
20335
- return {
20336
- "Access-Control-Allow-Origin": allowed,
20337
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
20338
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
20339
- Vary: "Origin"
20340
- };
20341
- }
20342
- function json(res, status, data, req) {
20343
- const body = JSON.stringify(data);
20344
- const cors = req ? corsHeaders(req) : {
20345
- "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
20346
- Vary: "Origin"
20347
- };
20348
- res.writeHead(status, {
20349
- ...cors,
20350
- "Content-Type": "application/json"
20351
- });
20352
- res.end(body);
20353
- }
20354
- function readBody(req) {
20355
- return new Promise((resolve, reject) => {
20356
- let data = "";
20357
- let size = 0;
20358
- req.on("data", (chunk) => {
20359
- size += chunk.length;
20360
- if (size > 1e6) {
20361
- req.destroy();
20362
- reject(/* @__PURE__ */ new Error("too large"));
20363
- return;
20364
- }
20365
- data += chunk.toString();
20366
- });
20367
- req.on("end", () => resolve(data));
20368
- req.on("error", reject);
20369
- });
20370
- }
20371
- const MAX_VIEWER_PORT_RETRIES = 10;
20372
- function startViewerServer(port, _kv, _sdk, secret, restPort) {
20373
- const resolvedRestPort = restPort ?? port - 2;
20374
- const requestedPort = port;
20375
- let allowedHosts = null;
20376
- const server = createServer(async (req, res) => {
20377
- if (!allowedHosts) {
20378
- const addr = server.address();
20379
- allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
20380
- }
20381
- if (!isHostAllowed(req.headers.host, allowedHosts)) {
20382
- res.writeHead(403, { "Content-Type": "text/plain" });
20383
- res.end("forbidden host");
20384
- return;
20385
- }
20386
- const raw = req.url || "/";
20387
- const qIdx = raw.indexOf("?");
20388
- const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
20389
- const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
20390
- const method = req.method || "GET";
20391
- if (method === "OPTIONS") {
20392
- res.writeHead(204, {
20393
- ...corsHeaders(req),
20394
- "Access-Control-Max-Age": "86400"
20395
- });
20396
- res.end();
20397
- return;
20398
- }
20399
- if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
20400
- const rendered = renderViewerDocument();
20401
- if (rendered.found) {
20402
- res.writeHead(200, {
20403
- "Content-Type": "text/html; charset=utf-8",
20404
- "Content-Security-Policy": rendered.csp,
20405
- "Cache-Control": "no-cache"
20406
- });
20407
- res.end(rendered.html);
20408
- return;
20409
- }
20410
- res.writeHead(404, { "Content-Type": "text/plain" });
20411
- res.end("viewer not found");
20412
- return;
20413
- }
20414
- if (method === "GET" && pathname === "/favicon.svg") {
20415
- const favicon = loadViewerFavicon();
20416
- if (favicon) {
20417
- res.writeHead(200, {
20418
- "Content-Type": "image/svg+xml",
20419
- "Cache-Control": "public, max-age=3600"
20420
- });
20421
- res.end(favicon);
20422
- return;
20423
- }
20424
- res.writeHead(404, { "Content-Type": "text/plain" });
20425
- res.end("favicon not found");
20426
- return;
20427
- }
20428
- try {
20429
- await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
20430
- } catch (err) {
20431
- console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
20432
- json(res, 502, { error: "upstream error" }, req);
20433
- }
20434
- });
20435
- let attempt = 0;
20436
- let currentPort = requestedPort;
20437
- const tryListen = () => {
20438
- server.listen(currentPort, "127.0.0.1");
20439
- };
20440
- server.on("listening", () => {
20441
- if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
20442
- else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
20443
- });
20444
- server.on("error", (err) => {
20445
- if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
20446
- attempt++;
20447
- currentPort = requestedPort + attempt;
20448
- setImmediate(tryListen);
20449
- return;
20450
- }
20451
- if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
20452
- else console.error(`[agentmemory] Viewer error:`, err.message);
20453
- });
20454
- tryListen();
20455
- return server;
20456
- }
20457
- async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
20458
- const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
20459
- const headers = {};
20460
- if (secret) headers["Authorization"] = `Bearer ${secret}`;
20461
- const ct = req.headers["content-type"];
20462
- if (ct) headers["Content-Type"] = ct;
20463
- let body;
20464
- if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
20465
- const controller = new AbortController();
20466
- const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
20467
- let upstream;
20468
- try {
20469
- upstream = await fetch(upstreamUrl, {
20470
- method,
20471
- headers,
20472
- body: body || void 0,
20473
- signal: controller.signal
20474
- });
20475
- clearTimeout(fetchTimeout);
20476
- } catch (err) {
20477
- clearTimeout(fetchTimeout);
20478
- if (err instanceof Error && err.name === "AbortError") {
20479
- json(res, 504, { error: "upstream timeout" }, req);
20480
- return;
20481
- }
20482
- throw err;
20483
- }
20484
- const cors = corsHeaders(req);
20485
- const responseBody = await upstream.text();
20486
- const responseHeaders = { ...cors };
20487
- const upstreamCt = upstream.headers.get("content-type");
20488
- if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
20489
- res.writeHead(upstream.status, responseHeaders);
20490
- res.end(responseBody);
20491
- }
20492
-
20493
20961
  //#endregion
20494
20962
  //#region src/eval/metrics-store.ts
20495
20963
  var MetricsStore = class {
@@ -20627,6 +21095,21 @@ function initMetrics(getMeter) {
20627
21095
 
20628
21096
  //#endregion
20629
21097
  //#region src/index.ts
21098
+ function workerPidfilePath() {
21099
+ return join(homedir(), ".agentmemory", "worker.pid");
21100
+ }
21101
+ function writeWorkerPidfile() {
21102
+ try {
21103
+ const p = workerPidfilePath();
21104
+ mkdirSync(dirname(p), { recursive: true });
21105
+ writeFileSync(p, `${process.pid}\n`, { encoding: "utf-8" });
21106
+ } catch {}
21107
+ }
21108
+ function clearWorkerPidfile() {
21109
+ try {
21110
+ unlinkSync(workerPidfilePath());
21111
+ } catch {}
21112
+ }
20630
21113
  function hasGetMeter(sdk) {
20631
21114
  return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
20632
21115
  }
@@ -20667,6 +21150,7 @@ async function main() {
20667
21150
  framework: "iii-sdk"
20668
21151
  }
20669
21152
  });
21153
+ writeWorkerPidfile();
20670
21154
  const kv = new StateKV(sdk);
20671
21155
  const secret = getEnvVar("AGENTMEMORY_SECRET");
20672
21156
  const metricsStore = new MetricsStore(kv);
@@ -20762,6 +21246,7 @@ async function main() {
20762
21246
  registerMcpEndpoints(sdk, kv, secret);
20763
21247
  const healthMonitor = registerHealthMonitor(sdk, kv);
20764
21248
  const indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
21249
+ setIndexPersistence(indexPersistence);
20765
21250
  const loaded = await indexPersistence.load().catch((err) => {
20766
21251
  console.warn(`[agentmemory] Failed to load persisted index:`, err);
20767
21252
  return null;
@@ -20803,7 +21288,7 @@ async function main() {
20803
21288
  if (bm25Index.has(memory.id)) continue;
20804
21289
  bm25Index.add({
20805
21290
  id: memory.id,
20806
- sessionId: memory.sessionIds[0] ?? "memory",
21291
+ sessionId: memory.sessionIds?.[0] ?? "memory",
20807
21292
  timestamp: memory.createdAt,
20808
21293
  type: "decision",
20809
21294
  title: memory.title,
@@ -20823,7 +21308,7 @@ async function main() {
20823
21308
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
20824
21309
  }
20825
21310
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
20826
- bootLog(`REST API: 124 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21311
+ bootLog(`REST API: 125 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
20827
21312
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
20828
21313
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
20829
21314
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
@@ -20879,6 +21364,7 @@ async function main() {
20879
21364
  console.warn(`[agentmemory] Failed to save index on shutdown:`, err);
20880
21365
  });
20881
21366
  await sdk.shutdown();
21367
+ clearWorkerPidfile();
20882
21368
  process.exit(0);
20883
21369
  };
20884
21370
  process.on("SIGINT", shutdown);