@agentmemory/agentmemory 0.9.24 → 0.9.25

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 (88) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/cli.d.mts.map +1 -1
  4. package/dist/cli.mjs +125 -65
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{connect-Cf9bmBqO.mjs → connect-bmZ5eqYN.mjs} +17 -56
  7. package/dist/{connect-Cf9bmBqO.mjs.map → connect-bmZ5eqYN.mjs.map} +1 -1
  8. package/dist/hooks/notification.mjs +2 -4
  9. package/dist/hooks/notification.mjs.map +1 -1
  10. package/dist/hooks/post-commit.mjs +2 -3
  11. package/dist/hooks/post-commit.mjs.map +1 -1
  12. package/dist/hooks/post-tool-failure.mjs +2 -4
  13. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  14. package/dist/hooks/post-tool-use.mjs +2 -4
  15. package/dist/hooks/post-tool-use.mjs.map +1 -1
  16. package/dist/hooks/pre-compact.mjs +2 -4
  17. package/dist/hooks/pre-compact.mjs.map +1 -1
  18. package/dist/hooks/pre-tool-use.mjs +2 -2
  19. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  20. package/dist/hooks/prompt-submit.mjs +2 -4
  21. package/dist/hooks/prompt-submit.mjs.map +1 -1
  22. package/dist/hooks/session-end.mjs +2 -2
  23. package/dist/hooks/session-start.mjs +2 -4
  24. package/dist/hooks/session-start.mjs.map +1 -1
  25. package/dist/hooks/stop.mjs +2 -2
  26. package/dist/hooks/subagent-start.mjs +2 -4
  27. package/dist/hooks/subagent-start.mjs.map +1 -1
  28. package/dist/hooks/subagent-stop.mjs +2 -4
  29. package/dist/hooks/subagent-stop.mjs.map +1 -1
  30. package/dist/hooks/task-completed.mjs +2 -4
  31. package/dist/hooks/task-completed.mjs.map +1 -1
  32. package/dist/image-refs-C7h9L5wx.mjs +52 -0
  33. package/dist/image-refs-C7h9L5wx.mjs.map +1 -0
  34. package/dist/{image-refs-CJS5B9Gq.mjs → image-store-Gpo2mgM9.mjs} +11 -42
  35. package/dist/image-store-Gpo2mgM9.mjs.map +1 -0
  36. package/dist/index.mjs +941 -493
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{logger-xlVlvCWX.mjs → logger-yHTcEBAI.mjs} +2 -2
  39. package/dist/{logger-xlVlvCWX.mjs.map → logger-yHTcEBAI.mjs.map} +1 -1
  40. package/dist/rolldown-runtime-twds-ZHy.mjs +14 -0
  41. package/dist/{schema-BkALl7Z_.mjs → schema-Dsr_V2Wp.mjs} +4 -4
  42. package/dist/schema-Dsr_V2Wp.mjs.map +1 -0
  43. package/dist/{src-B8J9Exum.mjs → src-fQOMXeCp.mjs} +936 -483
  44. package/dist/src-fQOMXeCp.mjs.map +1 -0
  45. package/dist/{standalone-CPfsVTBA.mjs → standalone-BzfA1zu8.mjs} +6 -10
  46. package/dist/{standalone-CPfsVTBA.mjs.map → standalone-BzfA1zu8.mjs.map} +1 -1
  47. package/dist/standalone.mjs +3 -14
  48. package/dist/standalone.mjs.map +1 -1
  49. package/dist/{tools-registry-DJizX9Az.mjs → tools-registry-Dzxv9iUu.mjs} +7 -5
  50. package/dist/tools-registry-Dzxv9iUu.mjs.map +1 -0
  51. package/dist/version-C3hZKw8n.mjs +6 -0
  52. package/dist/version-C3hZKw8n.mjs.map +1 -0
  53. package/dist/viewer/index.html +155 -9
  54. package/package.json +9 -4
  55. package/plugin/.claude-plugin/plugin.json +1 -1
  56. package/plugin/.codex-plugin/plugin.json +1 -1
  57. package/plugin/plugin.json +1 -1
  58. package/plugin/scripts/notification.mjs +2 -4
  59. package/plugin/scripts/notification.mjs.map +1 -1
  60. package/plugin/scripts/post-commit.mjs +2 -3
  61. package/plugin/scripts/post-commit.mjs.map +1 -1
  62. package/plugin/scripts/post-tool-failure.mjs +2 -4
  63. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  64. package/plugin/scripts/post-tool-use.mjs +2 -4
  65. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  66. package/plugin/scripts/pre-compact.mjs +2 -4
  67. package/plugin/scripts/pre-compact.mjs.map +1 -1
  68. package/plugin/scripts/pre-tool-use.mjs +2 -2
  69. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  70. package/plugin/scripts/prompt-submit.mjs +2 -4
  71. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  72. package/plugin/scripts/session-end.mjs +2 -2
  73. package/plugin/scripts/session-start.mjs +2 -4
  74. package/plugin/scripts/session-start.mjs.map +1 -1
  75. package/plugin/scripts/stop.mjs +2 -2
  76. package/plugin/scripts/subagent-start.mjs +2 -4
  77. package/plugin/scripts/subagent-start.mjs.map +1 -1
  78. package/plugin/scripts/subagent-stop.mjs +2 -4
  79. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  80. package/plugin/scripts/task-completed.mjs +2 -4
  81. package/plugin/scripts/task-completed.mjs.map +1 -1
  82. package/dist/image-refs-CJS5B9Gq.mjs.map +0 -1
  83. package/dist/image-store-CdE0amb1.mjs +0 -3
  84. package/dist/schema-BkALl7Z_.mjs.map +0 -1
  85. package/dist/src-B8J9Exum.mjs.map +0 -1
  86. package/dist/tools-registry-DJizX9Az.mjs.map +0 -1
  87. package/dist/version-BWEBnKAp.mjs +0 -6
  88. package/dist/version-BWEBnKAp.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -4,6 +4,7 @@ import { TriggerAction, registerWorker } from "iii-sdk";
4
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
+ import { AsyncLocalStorage } from "node:async_hooks";
7
8
  import Anthropic from "@anthropic-ai/sdk";
8
9
  import { lstat, mkdir, open, readFile, readdir, stat, unlink, utimes, writeFile } from "node:fs/promises";
9
10
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
@@ -14,23 +15,17 @@ import { lookup } from "node:dns/promises";
14
15
  import { isIP } from "node:net";
15
16
  import { fileURLToPath } from "node:url";
16
17
  import { createServer } from "node:http";
17
-
18
18
  //#region \0rolldown/runtime.js
19
19
  var __defProp = Object.defineProperty;
20
20
  var __exportAll = (all, no_symbols) => {
21
21
  let target = {};
22
- for (var name in all) {
23
- __defProp(target, name, {
24
- get: all[name],
25
- enumerable: true
26
- });
27
- }
28
- if (!no_symbols) {
29
- __defProp(target, Symbol.toStringTag, { value: "Module" });
30
- }
22
+ for (var name in all) __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true
25
+ });
26
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
31
27
  return target;
32
28
  };
33
-
34
29
  //#endregion
35
30
  //#region src/config.ts
36
31
  function safeParseInt(value, fallback) {
@@ -232,6 +227,10 @@ function loadSnapshotConfig() {
232
227
  function isGraphExtractionEnabled() {
233
228
  return getMergedEnv()["GRAPH_EXTRACTION_ENABLED"] === "true";
234
229
  }
230
+ const FOLLOWUP_WINDOW_DEFAULT_SECONDS = 30;
231
+ function getFollowupWindowSeconds() {
232
+ return safeParseInt(getMergedEnv()["AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS"], FOLLOWUP_WINDOW_DEFAULT_SECONDS);
233
+ }
235
234
  function isConsolidationEnabled() {
236
235
  const env = getMergedEnv();
237
236
  const explicit = env["CONSOLIDATION_ENABLED"];
@@ -274,11 +273,18 @@ function loadFallbackConfig() {
274
273
  return true;
275
274
  }) };
276
275
  }
277
-
278
276
  //#endregion
279
277
  //#region src/providers/agent-sdk.ts
278
+ const sdkChildContext = new AsyncLocalStorage();
279
+ let sdkActiveCount = 0;
280
+ let sdkOriginalEnv;
280
281
  var AgentSDKProvider = class {
281
282
  name = "agent-sdk";
283
+ sdkPromise = null;
284
+ loadSdk() {
285
+ if (!this.sdkPromise) this.sdkPromise = import("@anthropic-ai/claude-agent-sdk");
286
+ return this.sdkPromise;
287
+ }
282
288
  async compress(systemPrompt, userPrompt) {
283
289
  return this.query(systemPrompt, userPrompt);
284
290
  }
@@ -286,29 +292,37 @@ var AgentSDKProvider = class {
286
292
  return this.query(systemPrompt, userPrompt);
287
293
  }
288
294
  async query(systemPrompt, userPrompt) {
289
- if (process.env.AGENTMEMORY_SDK_CHILD === "1") return "";
290
- const prev = process.env.AGENTMEMORY_SDK_CHILD;
291
- process.env.AGENTMEMORY_SDK_CHILD = "1";
292
- try {
293
- const { query } = await import("@anthropic-ai/claude-agent-sdk");
294
- const messages = query({
295
- prompt: userPrompt,
296
- options: {
297
- systemPrompt,
298
- maxTurns: 1,
299
- allowedTools: []
295
+ if (sdkChildContext.getStore()) return "";
296
+ return sdkChildContext.run(true, async () => {
297
+ if (sdkActiveCount === 0) {
298
+ sdkOriginalEnv = process.env.AGENTMEMORY_SDK_CHILD;
299
+ process.env.AGENTMEMORY_SDK_CHILD = "1";
300
+ }
301
+ sdkActiveCount++;
302
+ try {
303
+ const { query } = await this.loadSdk();
304
+ const messages = query({
305
+ prompt: userPrompt,
306
+ options: {
307
+ systemPrompt,
308
+ maxTurns: 1,
309
+ allowedTools: []
310
+ }
311
+ });
312
+ let result = "";
313
+ for await (const msg of messages) if (msg.type === "result") result = msg.result ?? "";
314
+ return result;
315
+ } finally {
316
+ sdkActiveCount--;
317
+ if (sdkActiveCount === 0) {
318
+ if (sdkOriginalEnv === void 0) delete process.env.AGENTMEMORY_SDK_CHILD;
319
+ else process.env.AGENTMEMORY_SDK_CHILD = sdkOriginalEnv;
320
+ sdkOriginalEnv = void 0;
300
321
  }
301
- });
302
- let result = "";
303
- for await (const msg of messages) if (msg.type === "result") result = msg.result ?? "";
304
- return result;
305
- } finally {
306
- if (prev === void 0) delete process.env.AGENTMEMORY_SDK_CHILD;
307
- else process.env.AGENTMEMORY_SDK_CHILD = prev;
308
- }
322
+ }
323
+ });
309
324
  }
310
325
  };
311
-
312
326
  //#endregion
313
327
  //#region src/providers/anthropic.ts
314
328
  var AnthropicProvider = class {
@@ -362,7 +376,6 @@ var AnthropicProvider = class {
362
376
  })).content.find((b) => b.type === "text")?.text ?? "";
363
377
  }
364
378
  };
365
-
366
379
  //#endregion
367
380
  //#region src/providers/_fetch.ts
368
381
  function fetchWithTimeout(url, init, timeoutMs) {
@@ -376,7 +389,6 @@ function fetchWithTimeout(url, init, timeoutMs) {
376
389
  signal
377
390
  }).finally(() => clearTimeout(t));
378
391
  }
379
-
380
392
  //#endregion
381
393
  //#region src/providers/minimax.ts
382
394
  /**
@@ -436,7 +448,6 @@ var MinimaxProvider = class {
436
448
  return ((await response.json()).content?.find((b) => b.type === "text"))?.text ?? "";
437
449
  }
438
450
  };
439
-
440
451
  //#endregion
441
452
  //#region src/providers/noop.ts
442
453
  /**
@@ -455,11 +466,6 @@ var NoopProvider = class {
455
466
  return "";
456
467
  }
457
468
  };
458
-
459
- //#endregion
460
- //#region src/providers/_openai-shared.ts
461
- const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com";
462
- const DEFAULT_AZURE_API_VERSION = "2024-08-01-preview";
463
469
  function detectAzure(baseUrl) {
464
470
  try {
465
471
  return new URL(baseUrl).hostname.endsWith(".openai.azure.com");
@@ -519,9 +525,8 @@ function buildAuthHeaders(apiKey, isAzure) {
519
525
  };
520
526
  }
521
527
  function normalizeBaseUrl(raw) {
522
- return (raw || DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, "");
528
+ return (raw || "https://api.openai.com").replace(/\/+$/, "");
523
529
  }
524
-
525
530
  //#endregion
526
531
  //#region src/providers/openai.ts
527
532
  const DEFAULT_TIMEOUT_MS = 6e4;
@@ -574,7 +579,7 @@ var OpenAIProvider = class {
574
579
  this.baseUrl = normalizeBaseUrl(baseURL || getEnvVar("OPENAI_BASE_URL"));
575
580
  this.reasoningEffort = getEnvVar("OPENAI_REASONING_EFFORT") || void 0;
576
581
  this.timeoutMs = resolveTimeout();
577
- this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
582
+ this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || "2024-08-01-preview";
578
583
  this.isAzure = detectAzure(this.baseUrl);
579
584
  }
580
585
  async compress(systemPrompt, userPrompt) {
@@ -636,7 +641,6 @@ function parsePositiveInt(raw) {
636
641
  const n = Number(trimmed);
637
642
  return Number.isFinite(n) && n > 0 ? n : void 0;
638
643
  }
639
-
640
644
  //#endregion
641
645
  //#region src/providers/openrouter.ts
642
646
  var OpenRouterProvider = class {
@@ -688,7 +692,6 @@ var OpenRouterProvider = class {
688
692
  return content;
689
693
  }
690
694
  };
691
-
692
695
  //#endregion
693
696
  //#region src/providers/circuit-breaker.ts
694
697
  function positiveFinite(val, fallback) {
@@ -750,7 +753,6 @@ var CircuitBreaker = class {
750
753
  };
751
754
  }
752
755
  };
753
-
754
756
  //#endregion
755
757
  //#region src/providers/resilient.ts
756
758
  var ResilientProvider = class {
@@ -781,7 +783,6 @@ var ResilientProvider = class {
781
783
  return this.breaker.getState();
782
784
  }
783
785
  };
784
-
785
786
  //#endregion
786
787
  //#region src/providers/fallback-chain.ts
787
788
  var FallbackChainProvider = class {
@@ -806,7 +807,6 @@ var FallbackChainProvider = class {
806
807
  throw lastError || /* @__PURE__ */ new Error("No providers available");
807
808
  }
808
809
  };
809
-
810
810
  //#endregion
811
811
  //#region src/providers/embedding/gemini.ts
812
812
  const BATCH_LIMIT = 100;
@@ -862,7 +862,6 @@ function l2Normalize(vec) {
862
862
  for (let i = 0; i < vec.length; i++) vec[i] = vec[i] / norm;
863
863
  return vec;
864
864
  }
865
-
866
865
  //#endregion
867
866
  //#region src/providers/embedding/openai.ts
868
867
  const DEFAULT_MODEL$1 = "text-embedding-3-small";
@@ -936,7 +935,7 @@ var OpenAIEmbeddingProvider = class {
936
935
  this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
937
936
  this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
938
937
  this.isAzure = detectAzure(this.baseUrl);
939
- this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || DEFAULT_AZURE_API_VERSION;
938
+ this.azureApiVersion = getEnvVar("OPENAI_API_VERSION") || "2024-08-01-preview";
940
939
  }
941
940
  async embed(text) {
942
941
  const [result] = await this.embedBatch([text]);
@@ -958,7 +957,6 @@ var OpenAIEmbeddingProvider = class {
958
957
  return (await response.json()).data.map((d) => new Float32Array(d.embedding));
959
958
  }
960
959
  };
961
-
962
960
  //#endregion
963
961
  //#region src/providers/embedding/voyage.ts
964
962
  const API_URL$2 = "https://api.voyageai.com/v1/embeddings";
@@ -994,7 +992,6 @@ var VoyageEmbeddingProvider = class {
994
992
  return (await response.json()).data.map((d) => new Float32Array(d.embedding));
995
993
  }
996
994
  };
997
-
998
995
  //#endregion
999
996
  //#region src/providers/embedding/cohere.ts
1000
997
  const API_URL$1 = "https://api.cohere.ai/v1/embed";
@@ -1030,7 +1027,6 @@ var CohereEmbeddingProvider = class {
1030
1027
  return (await response.json()).embeddings.map((e) => new Float32Array(e));
1031
1028
  }
1032
1029
  };
1033
-
1034
1030
  //#endregion
1035
1031
  //#region src/providers/embedding/openrouter.ts
1036
1032
  const API_URL = "https://openrouter.ai/api/v1/embeddings";
@@ -1067,7 +1063,6 @@ var OpenRouterEmbeddingProvider = class {
1067
1063
  return (await response.json()).data.map((d) => new Float32Array(d.embedding));
1068
1064
  }
1069
1065
  };
1070
-
1071
1066
  //#endregion
1072
1067
  //#region src/providers/embedding/local.ts
1073
1068
  var LocalEmbeddingProvider = class {
@@ -1096,7 +1091,6 @@ var LocalEmbeddingProvider = class {
1096
1091
  return this.extractor;
1097
1092
  }
1098
1093
  };
1099
-
1100
1094
  //#endregion
1101
1095
  //#region src/providers/embedding/clip.ts
1102
1096
  const DEFAULT_MODEL = "Xenova/clip-vit-base-patch32";
@@ -1137,12 +1131,14 @@ var ClipEmbeddingProvider = class {
1137
1131
  }
1138
1132
  async getTextExtractor() {
1139
1133
  if (this.textExtractor) return this.textExtractor;
1140
- this.textExtractor = await (await this.getTransformers()).pipeline("feature-extraction", this.modelId);
1134
+ const t = await this.getTransformers();
1135
+ this.textExtractor = await t.pipeline("feature-extraction", this.modelId);
1141
1136
  return this.textExtractor;
1142
1137
  }
1143
1138
  async getImageExtractor() {
1144
1139
  if (this.imageExtractor) return this.imageExtractor;
1145
- this.imageExtractor = await (await this.getTransformers()).pipeline("image-feature-extraction", this.modelId);
1140
+ const t = await this.getTransformers();
1141
+ this.imageExtractor = await t.pipeline("image-feature-extraction", this.modelId);
1146
1142
  return this.imageExtractor;
1147
1143
  }
1148
1144
  };
@@ -1167,7 +1163,6 @@ function normalize(vec) {
1167
1163
  for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;
1168
1164
  return out;
1169
1165
  }
1170
-
1171
1166
  //#endregion
1172
1167
  //#region src/providers/embedding/index.ts
1173
1168
  let imageEmbeddingProvider = null;
@@ -1206,7 +1201,6 @@ function withDimensionGuard(provider) {
1206
1201
  if (provider.embedImage) wrapped.embedImage = async (s) => check(await provider.embedImage(s), "embedImage");
1207
1202
  return wrapped;
1208
1203
  }
1209
-
1210
1204
  //#endregion
1211
1205
  //#region src/providers/index.ts
1212
1206
  function requireEnvVar(key) {
@@ -1214,6 +1208,17 @@ function requireEnvVar(key) {
1214
1208
  if (!value) throw new Error(`Missing required environment variable: ${key}. Set it in ~/.agentmemory/.env or as an environment variable.`);
1215
1209
  return value;
1216
1210
  }
1211
+ function defaultModelFor(providerType) {
1212
+ switch (providerType) {
1213
+ case "openai": return getEnvVar("OPENAI_MODEL") || "gpt-4o-mini";
1214
+ case "anthropic": return getEnvVar("ANTHROPIC_MODEL") || "claude-sonnet-4-20250514";
1215
+ case "gemini": return getEnvVar("GEMINI_MODEL") || "gemini-2.5-flash";
1216
+ case "openrouter": return getEnvVar("OPENROUTER_MODEL") || "anthropic/claude-sonnet-4-20250514";
1217
+ case "minimax": return getEnvVar("MINIMAX_MODEL") || "MiniMax-M2.7";
1218
+ case "agent-sdk": return "claude-sonnet-4-20250514";
1219
+ default: return "noop";
1220
+ }
1221
+ }
1217
1222
  function createProvider(config) {
1218
1223
  return new ResilientProvider(createBaseProvider(config));
1219
1224
  }
@@ -1225,7 +1230,7 @@ function createFallbackProvider(config, fallbackConfig) {
1225
1230
  try {
1226
1231
  const fbConfig = {
1227
1232
  provider: providerType,
1228
- model: config.model,
1233
+ model: defaultModelFor(providerType),
1229
1234
  maxTokens: config.maxTokens
1230
1235
  };
1231
1236
  providers.push(createBaseProvider(fbConfig));
@@ -1253,7 +1258,6 @@ function createBaseProvider(config) {
1253
1258
  default: return new AgentSDKProvider();
1254
1259
  }
1255
1260
  }
1256
-
1257
1261
  //#endregion
1258
1262
  //#region src/state/kv.ts
1259
1263
  var StateKV = class {
@@ -1305,7 +1309,6 @@ var StateKV = class {
1305
1309
  });
1306
1310
  }
1307
1311
  };
1308
-
1309
1312
  //#endregion
1310
1313
  //#region src/state/schema.ts
1311
1314
  const KV = {
@@ -1353,7 +1356,8 @@ const KV = {
1353
1356
  slots: "mem:slots",
1354
1357
  globalSlots: "mem:slots:global",
1355
1358
  state: "mem:state",
1356
- commits: "mem:commits"
1359
+ commits: "mem:commits",
1360
+ recentSearches: "mem:recent-searches"
1357
1361
  };
1358
1362
  const STREAM = {
1359
1363
  name: "mem-live",
@@ -1375,7 +1379,6 @@ function jaccardSimilarity(a, b) {
1375
1379
  for (const word of setA) if (setB.has(word)) intersection++;
1376
1380
  return intersection / (setA.size + setB.size - intersection);
1377
1381
  }
1378
-
1379
1382
  //#endregion
1380
1383
  //#region src/state/vector-index.ts
1381
1384
  function float32ToBase64(arr) {
@@ -1498,7 +1501,6 @@ var VectorIndex = class VectorIndex {
1498
1501
  return idx;
1499
1502
  }
1500
1503
  };
1501
-
1502
1504
  //#endregion
1503
1505
  //#region src/state/memory-utils.ts
1504
1506
  function memoryToObservation(memory) {
@@ -1515,7 +1517,6 @@ function memoryToObservation(memory) {
1515
1517
  importance: memory.strength
1516
1518
  };
1517
1519
  }
1518
-
1519
1520
  //#endregion
1520
1521
  //#region src/functions/graph-retrieval.ts
1521
1522
  function buildGraphContext(path) {
@@ -1760,7 +1761,6 @@ var MinHeap = class {
1760
1761
  }
1761
1762
  }
1762
1763
  };
1763
-
1764
1764
  //#endregion
1765
1765
  //#region src/logger.ts
1766
1766
  function fmt(level, msg, fields) {
@@ -1798,7 +1798,6 @@ function bootLog(msg) {
1798
1798
  }
1799
1799
  if (bootBuffer.length < 500) bootBuffer.push(msg);
1800
1800
  }
1801
-
1802
1801
  //#endregion
1803
1802
  //#region src/functions/query-expansion.ts
1804
1803
  const QUERY_EXPANSION_SYSTEM = `You are a query expansion engine for a memory retrieval system. Given a user query, generate diverse reformulations to maximize recall.
@@ -1955,7 +1954,6 @@ function extractEntitiesFromQuery(query) {
1955
1954
  }
1956
1955
  return [...new Set(entities)];
1957
1956
  }
1958
-
1959
1957
  //#endregion
1960
1958
  //#region src/state/reranker.ts
1961
1959
  let pipeline = null;
@@ -2009,7 +2007,6 @@ async function rerank(query, results, topK = 20) {
2009
2007
  rerankPosition: i + 1
2010
2008
  }));
2011
2009
  }
2012
-
2013
2010
  //#endregion
2014
2011
  //#region src/state/hybrid-search.ts
2015
2012
  const RRF_K = 60;
@@ -2180,7 +2177,6 @@ var HybridSearch = class {
2180
2177
  return enriched;
2181
2178
  }
2182
2179
  };
2183
-
2184
2180
  //#endregion
2185
2181
  //#region src/state/stemmer.ts
2186
2182
  const step2map = {
@@ -2276,7 +2272,6 @@ function stem(word) {
2276
2272
  if (endsDoubleConsonant(w) && w.endsWith("l") && measure(w.slice(0, -1)) > 1) w = w.slice(0, -1);
2277
2273
  return w;
2278
2274
  }
2279
-
2280
2275
  //#endregion
2281
2276
  //#region src/state/synonyms.ts
2282
2277
  const SYNONYM_GROUPS = [
@@ -2438,7 +2433,6 @@ function getSynonyms(stemmedTerm) {
2438
2433
  const syns = synonymMap.get(stemmedTerm);
2439
2434
  return syns ? [...syns] : [];
2440
2435
  }
2441
-
2442
2436
  //#endregion
2443
2437
  //#region src/state/cjk-segmenter.ts
2444
2438
  const cjkRequire = createRequire(import.meta.url);
@@ -2542,7 +2536,6 @@ function segmentCjk(text) {
2542
2536
  }
2543
2537
  return out;
2544
2538
  }
2545
-
2546
2539
  //#endregion
2547
2540
  //#region src/state/search-index.ts
2548
2541
  var SearchIndex = class SearchIndex {
@@ -2742,18 +2735,92 @@ var SearchIndex = class SearchIndex {
2742
2735
  return lo;
2743
2736
  }
2744
2737
  };
2745
-
2738
+ //#endregion
2739
+ //#region src/functions/audit.ts
2740
+ async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
2741
+ const entry = {
2742
+ id: generateId("aud"),
2743
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2744
+ operation,
2745
+ userId,
2746
+ functionId,
2747
+ targetIds,
2748
+ details,
2749
+ qualityScore
2750
+ };
2751
+ await kv.set(KV.audit, entry.id, entry);
2752
+ return entry;
2753
+ }
2754
+ async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
2755
+ try {
2756
+ await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
2757
+ } catch (err) {
2758
+ try {
2759
+ logger.warn("audit write failed", {
2760
+ functionId,
2761
+ operation,
2762
+ targetIds,
2763
+ error: err instanceof Error ? err.message : String(err)
2764
+ });
2765
+ } catch {}
2766
+ }
2767
+ }
2768
+ async function queryAudit(kv, filter) {
2769
+ let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
2770
+ if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
2771
+ if (filter?.dateFrom) {
2772
+ const from = new Date(filter.dateFrom).getTime();
2773
+ if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
2774
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
2775
+ }
2776
+ if (filter?.dateTo) {
2777
+ const to = new Date(filter.dateTo).getTime();
2778
+ if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
2779
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
2780
+ }
2781
+ return entries.slice(0, filter?.limit || 100);
2782
+ }
2746
2783
  //#endregion
2747
2784
  //#region src/state/index-persistence.ts
2748
2785
  const DEBOUNCE_MS = 5e3;
2749
2786
  const FAILURE_LOG_THROTTLE_MS = 6e4;
2787
+ const INDEX_PERSISTENCE_FUNCTION_ID = "mem::index-persistence";
2788
+ const BM25_KEY = "data";
2789
+ const BM25_MANIFEST_KEY = "data:manifest";
2790
+ const BM25_SHARD_SCOPE_PREFIX = `${KV.bm25Index}:bm25:`;
2791
+ const VECTOR_KEY = "vectors";
2792
+ const VECTOR_MANIFEST_KEY = "vectors:manifest";
2793
+ const VECTOR_SHARD_SCOPE_PREFIX = `${KV.bm25Index}:vectors:`;
2794
+ const INDEX_SHARD_KEY = "data";
2795
+ const DEFAULT_INDEX_SHARD_CHARS = 2e6;
2796
+ function shardChars(options) {
2797
+ const configured = options.shardChars;
2798
+ if (typeof configured !== "number" || !Number.isFinite(configured)) return DEFAULT_INDEX_SHARD_CHARS;
2799
+ const wholeChars = Math.floor(configured);
2800
+ return wholeChars >= 1 ? wholeChars : DEFAULT_INDEX_SHARD_CHARS;
2801
+ }
2802
+ function createIndexGeneration() {
2803
+ return generateId("idx");
2804
+ }
2805
+ function statePath(scope, key) {
2806
+ return `${scope}/${key}`;
2807
+ }
2808
+ function errorMessage(err) {
2809
+ return err instanceof Error ? err.message : String(err);
2810
+ }
2811
+ function isValidShardDescriptor(shard) {
2812
+ if (!shard || typeof shard !== "object") return false;
2813
+ const candidate = shard;
2814
+ return typeof candidate.scope === "string" && candidate.scope.length > 0 && typeof candidate.key === "string" && candidate.key.length > 0 && Number.isInteger(candidate.chars) && candidate.chars >= 0;
2815
+ }
2750
2816
  var IndexPersistence = class {
2751
2817
  timer = null;
2752
2818
  lastFailureLogAt = 0;
2753
- constructor(kv, bm25, vector) {
2819
+ constructor(kv, bm25, vector, options = {}) {
2754
2820
  this.kv = kv;
2755
2821
  this.bm25 = bm25;
2756
2822
  this.vector = vector;
2823
+ this.options = options;
2757
2824
  }
2758
2825
  scheduleSave() {
2759
2826
  if (this.timer) clearTimeout(this.timer);
@@ -2767,8 +2834,8 @@ var IndexPersistence = class {
2767
2834
  this.timer = null;
2768
2835
  }
2769
2836
  try {
2770
- await this.kv.set(KV.bm25Index, "data", this.bm25.serialize());
2771
- if (this.vector && this.vector.size > 0) await this.kv.set(KV.bm25Index, "vectors", this.vector.serialize());
2837
+ await this.saveBm25Index(this.bm25.serialize());
2838
+ if (this.vector) await this.saveVectorIndex(this.vector.serialize());
2772
2839
  } catch (err) {
2773
2840
  this.logFailure(err);
2774
2841
  }
@@ -2776,9 +2843,9 @@ var IndexPersistence = class {
2776
2843
  async load() {
2777
2844
  let bm25 = null;
2778
2845
  let vector = null;
2779
- const bm25Data = await this.kv.get(KV.bm25Index, "data").catch(() => null);
2846
+ const bm25Data = await this.loadBm25Data();
2780
2847
  if (bm25Data && typeof bm25Data === "string") bm25 = SearchIndex.deserialize(bm25Data);
2781
- const vecData = await this.kv.get(KV.bm25Index, "vectors").catch(() => null);
2848
+ const vecData = await this.loadVectorData();
2782
2849
  if (vecData && typeof vecData === "string") vector = VectorIndex.deserialize(vecData);
2783
2850
  return {
2784
2851
  bm25,
@@ -2803,8 +2870,190 @@ var IndexPersistence = class {
2803
2870
  hint: code === "TIMEOUT" ? "iii-engine state::set timed out; recent index updates remain in memory and will retry on the next debounce flush" : void 0
2804
2871
  });
2805
2872
  }
2873
+ async saveBm25Index(serialized) {
2874
+ await this.saveShardedIndex(serialized, BM25_MANIFEST_KEY, BM25_KEY, BM25_SHARD_SCOPE_PREFIX);
2875
+ }
2876
+ async saveVectorIndex(serialized) {
2877
+ await this.saveShardedIndex(serialized, VECTOR_MANIFEST_KEY, VECTOR_KEY, VECTOR_SHARD_SCOPE_PREFIX);
2878
+ }
2879
+ async saveShardedIndex(serialized, manifestKey, legacyKey, scopePrefix) {
2880
+ const previous = await this.kv.get(KV.bm25Index, manifestKey).catch(() => null);
2881
+ const generation = this.options.createGeneration?.() ?? createIndexGeneration();
2882
+ const chunkChars = shardChars(this.options);
2883
+ const shards = [];
2884
+ const chunks = [];
2885
+ for (let offset = 0; offset < serialized.length; offset += chunkChars) {
2886
+ const shardIndex = shards.length;
2887
+ const scope = `${scopePrefix}${generation}:${String(shardIndex).padStart(5, "0")}`;
2888
+ const chunk = serialized.slice(offset, offset + chunkChars);
2889
+ shards.push({
2890
+ scope,
2891
+ key: INDEX_SHARD_KEY,
2892
+ chars: chunk.length
2893
+ });
2894
+ chunks.push(chunk);
2895
+ }
2896
+ const failedWrite = (await Promise.allSettled(shards.map(async (shard, index) => {
2897
+ const chunk = chunks[index] ?? "";
2898
+ await this.kv.set(shard.scope, shard.key, chunk);
2899
+ await this.auditIndexPersistence("shard_write", [statePath(shard.scope, shard.key)], {
2900
+ scope: shard.scope,
2901
+ key: shard.key,
2902
+ manifestKey,
2903
+ generation,
2904
+ chars: chunk.length
2905
+ });
2906
+ }))).find((result) => result.status === "rejected");
2907
+ if (failedWrite) {
2908
+ await this.deleteShards(shards, "shard_write_rollback");
2909
+ throw failedWrite.reason;
2910
+ }
2911
+ const nextManifest = {
2912
+ v: 1,
2913
+ generation,
2914
+ shards,
2915
+ chars: serialized.length
2916
+ };
2917
+ try {
2918
+ await this.kv.set(KV.bm25Index, manifestKey, nextManifest);
2919
+ await this.auditIndexPersistence("manifest_publish", [statePath(KV.bm25Index, manifestKey)], {
2920
+ manifestKey,
2921
+ generation,
2922
+ chars: serialized.length,
2923
+ shards: shards.length,
2924
+ result: "committed"
2925
+ });
2926
+ } catch (err) {
2927
+ if (await this.isManifestPublished(manifestKey, nextManifest)) await this.auditIndexPersistence("manifest_publish", [statePath(KV.bm25Index, manifestKey)], {
2928
+ manifestKey,
2929
+ generation,
2930
+ chars: serialized.length,
2931
+ shards: shards.length,
2932
+ result: "committed_after_error",
2933
+ error: errorMessage(err)
2934
+ });
2935
+ else await this.deleteShards(shards, "manifest_publish_rollback");
2936
+ throw err;
2937
+ }
2938
+ await this.deleteKey(KV.bm25Index, legacyKey, "legacy_cleanup");
2939
+ if (previous?.v === 1 && Array.isArray(previous.shards)) {
2940
+ const currentShardIds = new Set(shards.map((shard) => `${shard.scope}\0${shard.key}`));
2941
+ for (const shard of previous.shards) {
2942
+ if (currentShardIds.has(`${shard.scope}\0${shard.key}`)) continue;
2943
+ await this.deleteShards([shard], "previous_generation_cleanup");
2944
+ }
2945
+ }
2946
+ }
2947
+ async auditIndexPersistence(action, targetIds, details) {
2948
+ await safeAudit(this.kv, "index_persist", INDEX_PERSISTENCE_FUNCTION_ID, targetIds, {
2949
+ action,
2950
+ ...details
2951
+ });
2952
+ }
2953
+ async deleteKey(scope, key, reason) {
2954
+ let result = "deleted";
2955
+ let error;
2956
+ try {
2957
+ await this.kv.delete(scope, key);
2958
+ } catch (err) {
2959
+ result = "failed";
2960
+ error = errorMessage(err);
2961
+ }
2962
+ await this.auditIndexPersistence("delete", [statePath(scope, key)], {
2963
+ scope,
2964
+ key,
2965
+ reason,
2966
+ result,
2967
+ error
2968
+ });
2969
+ }
2970
+ async deleteShards(shards, reason) {
2971
+ for (const shard of shards) await this.deleteKey(shard.scope, shard.key, reason);
2972
+ }
2973
+ async isManifestPublished(manifestKey, expected) {
2974
+ const published = await this.kv.get(KV.bm25Index, manifestKey).catch(() => null);
2975
+ if (published?.v !== 1 || published.generation !== expected.generation || published.chars !== expected.chars || !Array.isArray(published.shards) || published.shards.length !== expected.shards.length) return false;
2976
+ return published.shards.every((shard, index) => {
2977
+ const expectedShard = expected.shards[index];
2978
+ if (!expectedShard) return false;
2979
+ return shard.scope === expectedShard.scope && shard.key === expectedShard.key && shard.chars === expectedShard.chars;
2980
+ });
2981
+ }
2982
+ async loadBm25Data() {
2983
+ return this.loadShardedData(BM25_KEY, BM25_MANIFEST_KEY, "BM25");
2984
+ }
2985
+ async loadVectorData() {
2986
+ return this.loadShardedData(VECTOR_KEY, VECTOR_MANIFEST_KEY, "vector");
2987
+ }
2988
+ async loadShardedData(legacyKey, manifestKey, label) {
2989
+ const manifest = await this.readIndexValue(KV.bm25Index, manifestKey, label, "manifest");
2990
+ if (!manifest.ok) return null;
2991
+ if (manifest.value !== null) return this.loadManifestData(manifest.value, label);
2992
+ const legacy = await this.readIndexValue(KV.bm25Index, legacyKey, label, "legacy");
2993
+ if (!legacy.ok) return null;
2994
+ if (legacy.value && typeof legacy.value === "string") return legacy.value;
2995
+ return null;
2996
+ }
2997
+ async readIndexValue(scope, key, label, source) {
2998
+ try {
2999
+ return {
3000
+ ok: true,
3001
+ value: await this.kv.get(scope, key)
3002
+ };
3003
+ } catch (err) {
3004
+ logger.warn(`index persistence: ${label} ${source} read failed`, {
3005
+ scope,
3006
+ key,
3007
+ message: errorMessage(err)
3008
+ });
3009
+ return { ok: false };
3010
+ }
3011
+ }
3012
+ async loadManifestData(manifest, label) {
3013
+ if (manifest.v !== 1 || !Array.isArray(manifest.shards) || manifest.shards.length === 0 || !Number.isInteger(manifest.chars) || manifest.chars < 0) {
3014
+ logger.warn(`index persistence: ${label} shard manifest invalid`);
3015
+ return null;
3016
+ }
3017
+ for (const shard of manifest.shards) if (!isValidShardDescriptor(shard)) {
3018
+ logger.warn(`index persistence: ${label} shard manifest invalid`);
3019
+ return null;
3020
+ }
3021
+ const loadedShards = await Promise.all(manifest.shards.map(async (shard) => ({
3022
+ shard,
3023
+ chunk: await this.kv.get(shard.scope, shard.key).catch(() => null)
3024
+ })));
3025
+ const chunks = [];
3026
+ let chars = 0;
3027
+ for (const { shard, chunk } of loadedShards) {
3028
+ if (typeof chunk !== "string") {
3029
+ logger.warn(`index persistence: ${label} shard missing`, {
3030
+ scope: shard.scope,
3031
+ key: shard.key
3032
+ });
3033
+ return null;
3034
+ }
3035
+ if (chunk.length !== shard.chars) {
3036
+ logger.warn(`index persistence: ${label} shard length mismatch`, {
3037
+ scope: shard.scope,
3038
+ key: shard.key,
3039
+ expected: shard.chars,
3040
+ actual: chunk.length
3041
+ });
3042
+ return null;
3043
+ }
3044
+ chunks.push(chunk);
3045
+ chars += chunk.length;
3046
+ }
3047
+ if (chars !== manifest.chars) {
3048
+ logger.warn(`index persistence: ${label} total length mismatch`, {
3049
+ expected: manifest.chars,
3050
+ actual: chars
3051
+ });
3052
+ return null;
3053
+ }
3054
+ return chunks.join("");
3055
+ }
2806
3056
  };
2807
-
2808
3057
  //#endregion
2809
3058
  //#region src/functions/privacy.ts
2810
3059
  const PRIVATE_TAG_RE = /<private>[\s\S]*?<\/private>/gi;
@@ -2841,7 +3090,6 @@ function registerPrivacyFunction(sdk) {
2841
3090
  return { output: stripPrivateData(data.input) };
2842
3091
  });
2843
3092
  }
2844
-
2845
3093
  //#endregion
2846
3094
  //#region src/state/keyed-mutex.ts
2847
3095
  const locks = /* @__PURE__ */ new Map();
@@ -2854,7 +3102,6 @@ function withKeyedLock(key, fn) {
2854
3102
  });
2855
3103
  return next;
2856
3104
  }
2857
-
2858
3105
  //#endregion
2859
3106
  //#region src/functions/compress-synthetic.ts
2860
3107
  function inferType(toolName, hookType) {
@@ -2950,7 +3197,6 @@ function buildSyntheticCompression(raw) {
2950
3197
  if (raw.agentId) result.agentId = raw.agentId;
2951
3198
  return result;
2952
3199
  }
2953
-
2954
3200
  //#endregion
2955
3201
  //#region src/functions/access-tracker.ts
2956
3202
  const RECENT_CAP = 20;
@@ -3021,7 +3267,6 @@ async function deleteAccessLog(kv, memoryId) {
3021
3267
  });
3022
3268
  } catch {}
3023
3269
  }
3024
-
3025
3270
  //#endregion
3026
3271
  //#region src/functions/search.ts
3027
3272
  let index = null;
@@ -3377,7 +3622,6 @@ function registerSearchFunction(sdk, kv) {
3377
3622
  };
3378
3623
  });
3379
3624
  }
3380
-
3381
3625
  //#endregion
3382
3626
  //#region src/functions/observe.ts
3383
3627
  function extractImage(d) {
@@ -3461,11 +3705,19 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3461
3705
  raw.imageData = filePath;
3462
3706
  const { incrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
3463
3707
  await incrementImageRef(kv, filePath);
3464
- sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: bytesWritten });
3465
- if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.triggerVoid("mem::vision-embed", {
3466
- imageRef: filePath,
3467
- sessionId: payload.sessionId,
3468
- observationId: obsId
3708
+ sdk.trigger({
3709
+ function_id: "mem::disk-size-delta",
3710
+ payload: { deltaBytes: bytesWritten },
3711
+ action: TriggerAction.Void()
3712
+ });
3713
+ if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.trigger({
3714
+ function_id: "mem::vision-embed",
3715
+ payload: {
3716
+ imageRef: filePath,
3717
+ sessionId: payload.sessionId,
3718
+ observationId: obsId
3719
+ },
3720
+ action: TriggerAction.Void()
3469
3721
  });
3470
3722
  }
3471
3723
  try {
@@ -3474,7 +3726,11 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3474
3726
  if (raw.imageData) {
3475
3727
  const { deleteImage } = await Promise.resolve().then(() => image_store_exports);
3476
3728
  const { deletedBytes } = await deleteImage(raw.imageData);
3477
- if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
3729
+ if (deletedBytes > 0) sdk.trigger({
3730
+ function_id: "mem::disk-size-delta",
3731
+ payload: { deltaBytes: -deletedBytes },
3732
+ action: TriggerAction.Void()
3733
+ });
3478
3734
  }
3479
3735
  throw error;
3480
3736
  }
@@ -3594,7 +3850,6 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3594
3850
  });
3595
3851
  });
3596
3852
  }
3597
-
3598
3853
  //#endregion
3599
3854
  //#region src/utils/image-store.ts
3600
3855
  var image_store_exports = /* @__PURE__ */ __exportAll({
@@ -3671,7 +3926,6 @@ async function touchImage(filePath) {
3671
3926
  }
3672
3927
  } catch (err) {}
3673
3928
  }
3674
-
3675
3929
  //#endregion
3676
3930
  //#region src/functions/image-refs.ts
3677
3931
  var image_refs_exports = /* @__PURE__ */ __exportAll({
@@ -3697,11 +3951,14 @@ async function decrementImageRef(kv, sdk, filePath) {
3697
3951
  await kv.delete(KV.imageEmbeddings, filePath);
3698
3952
  await kv.delete(KV.imageRefs, filePath);
3699
3953
  const { deletedBytes } = await deleteImage(filePath);
3700
- if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
3954
+ if (deletedBytes > 0) sdk.trigger({
3955
+ function_id: "mem::disk-size-delta",
3956
+ payload: { deltaBytes: -deletedBytes },
3957
+ action: TriggerAction.Void()
3958
+ });
3701
3959
  } else await kv.set(KV.imageRefs, filePath, current - 1);
3702
3960
  });
3703
3961
  }
3704
-
3705
3962
  //#endregion
3706
3963
  //#region src/functions/image-quota-cleanup.ts
3707
3964
  const GRACE_PERIOD_MS = 3e4;
@@ -3761,7 +4018,11 @@ function registerImageQuotaCleanup(sdk, kv) {
3761
4018
  if (refCount > 0) return;
3762
4019
  const { deletedBytes } = await deleteImage(f.filePath);
3763
4020
  if (deletedBytes > 0) {
3764
- sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
4021
+ sdk.trigger({
4022
+ function_id: "mem::disk-size-delta",
4023
+ payload: { deltaBytes: -deletedBytes },
4024
+ action: TriggerAction.Void()
4025
+ });
3765
4026
  totalToFree -= deletedBytes;
3766
4027
  freedBytes += deletedBytes;
3767
4028
  evicted++;
@@ -3783,53 +4044,6 @@ function registerImageQuotaCleanup(sdk, kv) {
3783
4044
  });
3784
4045
  });
3785
4046
  }
3786
-
3787
- //#endregion
3788
- //#region src/functions/audit.ts
3789
- async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
3790
- const entry = {
3791
- id: generateId("aud"),
3792
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3793
- operation,
3794
- userId,
3795
- functionId,
3796
- targetIds,
3797
- details,
3798
- qualityScore
3799
- };
3800
- await kv.set(KV.audit, entry.id, entry);
3801
- return entry;
3802
- }
3803
- async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
3804
- try {
3805
- await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
3806
- } catch (err) {
3807
- try {
3808
- logger.warn("audit write failed", {
3809
- functionId,
3810
- operation,
3811
- targetIds,
3812
- error: err instanceof Error ? err.message : String(err)
3813
- });
3814
- } catch {}
3815
- }
3816
- }
3817
- async function queryAudit(kv, filter) {
3818
- let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
3819
- if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
3820
- if (filter?.dateFrom) {
3821
- const from = new Date(filter.dateFrom).getTime();
3822
- if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
3823
- entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
3824
- }
3825
- if (filter?.dateTo) {
3826
- const to = new Date(filter.dateTo).getTime();
3827
- if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
3828
- entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
3829
- }
3830
- return entries.slice(0, filter?.limit || 100);
3831
- }
3832
-
3833
4047
  //#endregion
3834
4048
  //#region src/functions/vision-search.ts
3835
4049
  function registerVisionSearchFunctions(sdk, kv, imageProvider) {
@@ -3953,7 +4167,6 @@ function cosine(a, b) {
3953
4167
  const denom = Math.sqrt(normA) * Math.sqrt(normB);
3954
4168
  return denom === 0 ? 0 : dot / denom;
3955
4169
  }
3956
-
3957
4170
  //#endregion
3958
4171
  //#region src/functions/slots.ts
3959
4172
  const DEFAULT_SIZE_LIMIT = 2e3;
@@ -4392,7 +4605,6 @@ function registerSlotsFunctions(sdk, kv) {
4392
4605
  };
4393
4606
  });
4394
4607
  }
4395
-
4396
4608
  //#endregion
4397
4609
  //#region src/functions/disk-size-manager.ts
4398
4610
  const DISK_SIZE_KEY = "system:currentDiskSize";
@@ -4407,7 +4619,11 @@ function registerDiskSizeManager(sdk, kv) {
4407
4619
  if (newTotal < 0) newTotal = 0;
4408
4620
  await kv.set(KV.state, DISK_SIZE_KEY, newTotal);
4409
4621
  if (data.deltaBytes > 0 && newTotal > getMaxBytes()) {
4410
- sdk.triggerVoid("mem::image-quota-cleanup", {});
4622
+ sdk.trigger({
4623
+ function_id: "mem::image-quota-cleanup",
4624
+ payload: {},
4625
+ action: TriggerAction.Void()
4626
+ });
4411
4627
  logger.info("Disk quota exceeded, cleanup triggered", {
4412
4628
  currentBytes: newTotal,
4413
4629
  maxBytes: getMaxBytes()
@@ -4420,7 +4636,6 @@ function registerDiskSizeManager(sdk, kv) {
4420
4636
  });
4421
4637
  });
4422
4638
  }
4423
-
4424
4639
  //#endregion
4425
4640
  //#region src/prompts/compression.ts
4426
4641
  const COMPRESSION_SYSTEM = `You are a memory compression engine for an AI coding agent. Your job is to extract the essential information from a tool usage observation and compress it into structured data.
@@ -4468,7 +4683,6 @@ function buildCompressionPrompt(observation) {
4468
4683
  function truncate$1(s, max) {
4469
4684
  return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
4470
4685
  }
4471
-
4472
4686
  //#endregion
4473
4687
  //#region src/prompts/vision.ts
4474
4688
  const VISION_DESCRIPTION_PROMPT = `Describe what this image shows in the context of software development. Extract:
@@ -4479,7 +4693,6 @@ const VISION_DESCRIPTION_PROMPT = `Describe what this image shows in the context
4479
4693
  - Text content visible in the image
4480
4694
 
4481
4695
  Be concise but preserve all technically relevant details. Output plain text, no XML.`;
4482
-
4483
4696
  //#endregion
4484
4697
  //#region src/prompts/xml.ts
4485
4698
  const VALID_TAG = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
@@ -4498,7 +4711,6 @@ function getXmlChildren(xml, parentTag, childTag) {
4498
4711
  while ((m = re.exec(parentMatch[1])) !== null) items.push(m[1].trim());
4499
4712
  return items;
4500
4713
  }
4501
-
4502
4714
  //#endregion
4503
4715
  //#region src/eval/schemas.ts
4504
4716
  const HookTypeEnum = z.enum([
@@ -4531,7 +4743,7 @@ const ObservationTypeEnum = z.enum([
4531
4743
  "task",
4532
4744
  "other"
4533
4745
  ]);
4534
- const ObserveInputSchema = z.object({
4746
+ z.object({
4535
4747
  hookType: HookTypeEnum,
4536
4748
  sessionId: z.string().min(1),
4537
4749
  project: z.string().min(1),
@@ -4556,16 +4768,16 @@ const SummaryOutputSchema = z.object({
4556
4768
  filesModified: z.array(z.string()),
4557
4769
  concepts: z.array(z.string())
4558
4770
  });
4559
- const SearchInputSchema = z.object({
4771
+ z.object({
4560
4772
  query: z.string().min(1),
4561
4773
  limit: z.number().int().positive().optional()
4562
4774
  });
4563
- const ContextInputSchema = z.object({
4775
+ z.object({
4564
4776
  sessionId: z.string().min(1),
4565
4777
  project: z.string().min(1),
4566
4778
  budget: z.number().positive().optional()
4567
4779
  });
4568
- const RememberInputSchema = z.object({
4780
+ z.object({
4569
4781
  content: z.string().min(1),
4570
4782
  type: z.enum([
4571
4783
  "pattern",
@@ -4578,22 +4790,22 @@ const RememberInputSchema = z.object({
4578
4790
  concepts: z.array(z.string()).optional(),
4579
4791
  files: z.array(z.string()).optional()
4580
4792
  });
4581
- const SmartSearchInputSchema = z.object({
4793
+ z.object({
4582
4794
  query: z.string().optional(),
4583
4795
  expandIds: z.array(z.string()).optional(),
4584
4796
  limit: z.number().int().positive().optional()
4585
4797
  });
4586
- const TimelineInputSchema = z.object({
4798
+ z.object({
4587
4799
  anchor: z.string().min(1),
4588
4800
  project: z.string().optional(),
4589
4801
  before: z.number().int().nonnegative().optional(),
4590
4802
  after: z.number().int().nonnegative().optional()
4591
4803
  });
4592
- const ProfileInputSchema = z.object({
4804
+ z.object({
4593
4805
  project: z.string().min(1),
4594
4806
  refresh: z.boolean().optional()
4595
4807
  });
4596
- const RelateInputSchema = z.object({
4808
+ z.object({
4597
4809
  sourceId: z.string().min(1),
4598
4810
  targetId: z.string().min(1),
4599
4811
  type: z.enum([
@@ -4604,12 +4816,12 @@ const RelateInputSchema = z.object({
4604
4816
  "related"
4605
4817
  ])
4606
4818
  });
4607
- const EvolveInputSchema = z.object({
4819
+ z.object({
4608
4820
  memoryId: z.string().min(1),
4609
4821
  newContent: z.string().min(1),
4610
4822
  newTitle: z.string().optional()
4611
4823
  });
4612
- const ExportImportInputSchema = z.object({
4824
+ z.object({
4613
4825
  exportData: z.object({
4614
4826
  version: z.union([z.literal("0.3.0"), z.literal("0.4.0")]),
4615
4827
  exportedAt: z.string(),
@@ -4625,7 +4837,6 @@ const ExportImportInputSchema = z.object({
4625
4837
  "skip"
4626
4838
  ]).optional()
4627
4839
  });
4628
-
4629
4840
  //#endregion
4630
4841
  //#region src/eval/validator.ts
4631
4842
  function validateInput(schema, data, functionId) {
@@ -4648,7 +4859,6 @@ function validateInput(schema, data, functionId) {
4648
4859
  function validateOutput(schema, data, functionId) {
4649
4860
  return validateInput(schema, data, functionId);
4650
4861
  }
4651
-
4652
4862
  //#endregion
4653
4863
  //#region src/eval/quality.ts
4654
4864
  function scoreCompression(obs) {
@@ -4672,7 +4882,6 @@ function scoreSummary(summary) {
4672
4882
  if (summary.concepts && summary.concepts.length > 0) score += 15;
4673
4883
  return Math.min(100, score);
4674
4884
  }
4675
-
4676
4885
  //#endregion
4677
4886
  //#region src/eval/self-correct.ts
4678
4887
  const STRICTER_SUFFIX = `
@@ -4696,7 +4905,6 @@ async function compressWithRetry(provider, systemPrompt, userPrompt, validator,
4696
4905
  retried: true
4697
4906
  };
4698
4907
  }
4699
-
4700
4908
  //#endregion
4701
4909
  //#region src/functions/compress.ts
4702
4910
  const VALID_TYPES$1 = new Set([
@@ -4877,7 +5085,6 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
4877
5085
  }
4878
5086
  });
4879
5087
  }
4880
-
4881
5088
  //#endregion
4882
5089
  //#region src/functions/context.ts
4883
5090
  function estimateTokens$1(text) {
@@ -5002,7 +5209,6 @@ function registerContextFunction(sdk, kv, tokenBudget) {
5002
5209
  };
5003
5210
  });
5004
5211
  }
5005
-
5006
5212
  //#endregion
5007
5213
  //#region src/prompts/summary.ts
5008
5214
  const SUMMARY_SYSTEM = `You are a session summarizer for an AI coding agent's memory system. Given all compressed observations from a coding session, produce a concise session summary.
@@ -5074,7 +5280,6 @@ Concepts: ${concepts}`;
5074
5280
  });
5075
5281
  return `Partial summaries (${partials.length} chunks of one session, chronological):\n\n${sections.join("\n\n---\n\n")}`;
5076
5282
  }
5077
-
5078
5283
  //#endregion
5079
5284
  //#region src/functions/summarize.ts
5080
5285
  const CHUNK_SIZE_DEFAULT = 400;
@@ -5163,18 +5368,29 @@ async function produceSummaryXml(provider, compressed, sessionId, project) {
5163
5368
  skipped
5164
5369
  };
5165
5370
  }
5371
+ function stripXmlWrappers(raw) {
5372
+ if (!raw) return "";
5373
+ let cleaned = raw.trim();
5374
+ cleaned = cleaned.replace(/```\s*xml\s*\n?/gi, "");
5375
+ cleaned = cleaned.replace(/```/g, "");
5376
+ cleaned = cleaned.trim();
5377
+ const rootMatch = cleaned.match(/(<[a-zA-Z_][a-zA-Z0-9_-]*>[\s\S]*<\/[a-zA-Z_][a-zA-Z0-9_-]*>)/);
5378
+ if (rootMatch && rootMatch[1]) return rootMatch[1].trim();
5379
+ return cleaned;
5380
+ }
5166
5381
  function parseSummaryXml(xml, sessionId, project, obsCount) {
5167
- const title = getXmlTag(xml, "title");
5382
+ const cleaned = stripXmlWrappers(xml);
5383
+ const title = getXmlTag(cleaned, "title");
5168
5384
  if (!title) return null;
5169
5385
  return {
5170
5386
  sessionId,
5171
5387
  project,
5172
5388
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5173
5389
  title,
5174
- narrative: getXmlTag(xml, "narrative"),
5175
- keyDecisions: getXmlChildren(xml, "decisions", "decision"),
5176
- filesModified: getXmlChildren(xml, "files", "file"),
5177
- concepts: getXmlChildren(xml, "concepts", "concept"),
5390
+ narrative: getXmlTag(cleaned, "narrative"),
5391
+ keyDecisions: getXmlChildren(cleaned, "decisions", "decision"),
5392
+ filesModified: getXmlChildren(cleaned, "files", "file"),
5393
+ concepts: getXmlChildren(cleaned, "concepts", "concept"),
5178
5394
  observationCount: obsCount
5179
5395
  };
5180
5396
  }
@@ -5211,27 +5427,44 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
5211
5427
  };
5212
5428
  }
5213
5429
  try {
5214
- const { response, mode, chunks } = await produceSummaryXml(provider, compressed, sessionId, session.project);
5430
+ let summary = null;
5431
+ let response = "";
5432
+ let mode = "single";
5433
+ let chunks = 1;
5434
+ for (let attempt = 1; attempt <= 2; attempt++) {
5435
+ const produced = await produceSummaryXml(provider, compressed, sessionId, session.project);
5436
+ response = produced.response;
5437
+ mode = produced.mode;
5438
+ chunks = produced.chunks;
5439
+ if (!response || !response.trim()) {
5440
+ logger.warn("Empty provider response on summarize", {
5441
+ sessionId,
5442
+ provider: provider.name,
5443
+ mode,
5444
+ chunks,
5445
+ observationCount: compressed.length,
5446
+ attempt
5447
+ });
5448
+ continue;
5449
+ }
5450
+ summary = parseSummaryXml(response, sessionId, session.project, compressed.length);
5451
+ if (summary) break;
5452
+ logger.warn("Failed to parse summary XML", {
5453
+ sessionId,
5454
+ attempt
5455
+ });
5456
+ }
5215
5457
  if (!response || !response.trim()) {
5216
5458
  const latencyMs = Date.now() - startMs;
5217
5459
  if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
5218
- logger.warn("Empty provider response on summarize", {
5219
- sessionId,
5220
- provider: provider.name,
5221
- mode,
5222
- chunks,
5223
- observationCount: compressed.length
5224
- });
5225
5460
  return {
5226
5461
  success: false,
5227
5462
  error: "empty_provider_response"
5228
5463
  };
5229
5464
  }
5230
- const summary = parseSummaryXml(response, sessionId, session.project, compressed.length);
5231
5465
  if (!summary) {
5232
5466
  const latencyMs = Date.now() - startMs;
5233
5467
  if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
5234
- logger.warn("Failed to parse summary XML", { sessionId });
5235
5468
  return {
5236
5469
  success: false,
5237
5470
  error: "parse_failed"
@@ -5292,7 +5525,6 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
5292
5525
  }
5293
5526
  });
5294
5527
  }
5295
-
5296
5528
  //#endregion
5297
5529
  //#region src/functions/migrate.ts
5298
5530
  const ALLOWED_DIRS = [resolve(homedir(), ".agentmemory")];
@@ -5493,7 +5725,6 @@ function safeJsonParse(value, fallback) {
5493
5725
  }
5494
5726
  return fallback;
5495
5727
  }
5496
-
5497
5728
  //#endregion
5498
5729
  //#region src/functions/file-index.ts
5499
5730
  function registerFileIndexFunction(sdk, kv) {
@@ -5563,7 +5794,6 @@ function registerFileIndexFunction(sdk, kv) {
5563
5794
  return { context };
5564
5795
  });
5565
5796
  }
5566
-
5567
5797
  //#endregion
5568
5798
  //#region src/functions/consolidate.ts
5569
5799
  const CONSOLIDATION_SYSTEM = `You are a memory consolidation engine. Given a set of related observations from coding sessions, synthesize them into a single long-term memory.
@@ -5715,7 +5945,6 @@ function registerConsolidateFunction(sdk, kv, provider) {
5715
5945
  };
5716
5946
  });
5717
5947
  }
5718
-
5719
5948
  //#endregion
5720
5949
  //#region src/functions/patterns.ts
5721
5950
  function registerPatternsFunction(sdk, kv) {
@@ -5799,7 +6028,6 @@ function registerPatternsFunction(sdk, kv) {
5799
6028
  return { rules };
5800
6029
  });
5801
6030
  }
5802
-
5803
6031
  //#endregion
5804
6032
  //#region src/functions/remember.ts
5805
6033
  function registerRememberFunction(sdk, kv) {
@@ -5960,7 +6188,6 @@ function registerRememberFunction(sdk, kv) {
5960
6188
  };
5961
6189
  });
5962
6190
  }
5963
-
5964
6191
  //#endregion
5965
6192
  //#region src/functions/evict.ts
5966
6193
  const MS_PER_DAY$1 = 1440 * 60 * 1e3;
@@ -6191,7 +6418,6 @@ function registerEvictFunction(sdk, kv) {
6191
6418
  return stats;
6192
6419
  });
6193
6420
  }
6194
-
6195
6421
  //#endregion
6196
6422
  //#region src/functions/relations.ts
6197
6423
  function computeConfidence(source, target, relationType) {
@@ -6379,7 +6605,6 @@ function registerRelationsFunction(sdk, kv) {
6379
6605
  return { results: result };
6380
6606
  });
6381
6607
  }
6382
-
6383
6608
  //#endregion
6384
6609
  //#region src/functions/timeline.ts
6385
6610
  function registerTimelineFunction(sdk, kv) {
@@ -6461,9 +6686,78 @@ async function findByKeyword(kv, keyword, project) {
6461
6686
  }
6462
6687
  return matches.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
6463
6688
  }
6464
-
6689
+ //#endregion
6690
+ //#region src/version.ts
6691
+ const VERSION = "0.9.25";
6692
+ //#endregion
6693
+ //#region src/telemetry/setup.ts
6694
+ const OTEL_CONFIG = {
6695
+ serviceName: "agentmemory",
6696
+ serviceVersion: VERSION,
6697
+ metricsExportIntervalMs: 3e4
6698
+ };
6699
+ let counters = null;
6700
+ let histograms = null;
6701
+ const NOOP_COUNTER = { add: () => {} };
6702
+ const NOOP_HISTOGRAM = { record: () => {} };
6703
+ const COUNTER_NAMES = [
6704
+ ["observationsTotal", "observations.total"],
6705
+ ["compressionSuccess", "compression.success"],
6706
+ ["compressionFailure", "compression.failure"],
6707
+ ["searchTotal", "search.total"],
6708
+ ["dedupSkipped", "dedup.skipped"],
6709
+ ["evictionTotal", "eviction.total"],
6710
+ ["circuitBreakerOpen", "circuit_breaker.open"],
6711
+ ["embeddingSuccess", "embedding.success"],
6712
+ ["embeddingFailure", "embedding.failure"],
6713
+ ["vectorSearchTotal", "vector_search.total"],
6714
+ ["autoForgetTotal", "auto_forget.total"],
6715
+ ["profileGenerated", "profile.generated"],
6716
+ ["claudeBridgeSync", "claude_bridge.sync"],
6717
+ ["graphExtraction", "graph.extraction"],
6718
+ ["consolidationRun", "consolidation.run"],
6719
+ ["teamShare", "team.share"],
6720
+ ["auditLog", "audit.log"],
6721
+ ["snapshotCreate", "snapshot.create"],
6722
+ ["governanceDelete", "governance.delete"],
6723
+ ["smartSearchFollowupWithinWindow", "smart_search.followup_within_window_total"],
6724
+ ["readerFailureWithEvidence", "reader_failure_with_evidence_total"]
6725
+ ];
6726
+ const HISTOGRAM_NAMES = [
6727
+ ["compressionLatency", "compression.latency_ms"],
6728
+ ["searchLatency", "search.latency_ms"],
6729
+ ["contextTokens", "context.tokens"],
6730
+ ["qualityScore", "quality.score"],
6731
+ ["embeddingLatency", "embedding.latency_ms"],
6732
+ ["vectorSearchLatency", "vector_search.latency_ms"]
6733
+ ];
6734
+ function getCounters() {
6735
+ if (counters) return counters;
6736
+ return Object.fromEntries(COUNTER_NAMES.map(([key]) => [key, NOOP_COUNTER]));
6737
+ }
6738
+ function initMetrics(getMeter) {
6739
+ const meter = getMeter?.("agentmemory");
6740
+ counters = Object.fromEntries(COUNTER_NAMES.map(([key, name]) => [key, meter ? meter.createCounter(name) : NOOP_COUNTER]));
6741
+ histograms = Object.fromEntries(HISTOGRAM_NAMES.map(([key, name]) => [key, meter ? meter.createHistogram(name) : NOOP_HISTOGRAM]));
6742
+ return {
6743
+ counters,
6744
+ histograms
6745
+ };
6746
+ }
6465
6747
  //#endregion
6466
6748
  //#region src/functions/smart-search.ts
6749
+ const followupStats = {
6750
+ followupWithinWindow: 0,
6751
+ agentInitiatedSearches: 0
6752
+ };
6753
+ const pendingFollowups = /* @__PURE__ */ new Set();
6754
+ function getFollowupStats() {
6755
+ const total = followupStats.agentInitiatedSearches;
6756
+ return {
6757
+ ...followupStats,
6758
+ rate: total > 0 ? followupStats.followupWithinWindow / total : 0
6759
+ };
6760
+ }
6467
6761
  const LESSON_CONTENT_PREVIEW_CHARS = 240;
6468
6762
  function registerSmartSearchFunction(sdk, kv, searchFn) {
6469
6763
  sdk.registerFunction("mem::smart-search", async (data) => {
@@ -6525,6 +6819,21 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6525
6819
  timestamp: r.observation.timestamp
6526
6820
  }));
6527
6821
  recordAccessBatch(kv, compact.map((r) => r.obsId));
6822
+ if (data.sessionId && typeof data.sessionId === "string" && data.source !== "viewer" && compact.length > 0) {
6823
+ followupStats.agentInitiatedSearches++;
6824
+ const sessionIdForFollowup = data.sessionId;
6825
+ const queryForFollowup = data.query;
6826
+ const compactForFollowup = compact;
6827
+ const detection = withKeyedLock(`recent-searches:${sessionIdForFollowup}`, () => detectFollowup(kv, sessionIdForFollowup, queryForFollowup, compactForFollowup)).catch((err) => {
6828
+ logger.warn("Smart search followup detection failed", {
6829
+ sessionId: sessionIdForFollowup,
6830
+ error: err instanceof Error ? err.message : String(err)
6831
+ });
6832
+ }).finally(() => {
6833
+ pendingFollowups.delete(detection);
6834
+ });
6835
+ pendingFollowups.add(detection);
6836
+ }
6528
6837
  logger.info("Smart search compact", {
6529
6838
  query: data.query,
6530
6839
  results: compact.length,
@@ -6563,6 +6872,35 @@ async function recallLessons(sdk, query, limit, project) {
6563
6872
  return [];
6564
6873
  }
6565
6874
  }
6875
+ async function detectFollowup(kv, sessionId, query, compact) {
6876
+ const now = Date.now();
6877
+ const windowMs = Math.max(1, getFollowupWindowSeconds()) * 1e3;
6878
+ const currentIds = compact.map((r) => r.obsId);
6879
+ const current = {
6880
+ sessionId,
6881
+ query,
6882
+ resultIds: currentIds,
6883
+ at: now
6884
+ };
6885
+ const prior = await kv.get(KV.recentSearches, sessionId).catch(() => null);
6886
+ await kv.set(KV.recentSearches, sessionId, current);
6887
+ if (!prior || typeof prior.at !== "number") return;
6888
+ if (now - prior.at > windowMs) return;
6889
+ if (typeof prior.query === "string" && prior.query === query) return;
6890
+ const priorIds = Array.isArray(prior.resultIds) ? prior.resultIds : [];
6891
+ const priorSet = new Set(priorIds);
6892
+ if (currentIds.some((id) => priorSet.has(id))) return;
6893
+ getCounters().smartSearchFollowupWithinWindow.add(1);
6894
+ followupStats.followupWithinWindow++;
6895
+ logger.info("Smart search followup detected", {
6896
+ sessionId,
6897
+ windowSeconds: Math.round(windowMs / 1e3),
6898
+ priorQuery: prior.query,
6899
+ nextQuery: query,
6900
+ priorResultCount: priorIds.length,
6901
+ nextResultCount: currentIds.length
6902
+ });
6903
+ }
6566
6904
  async function findObservation$1(kv, obsId, sessionIdHint) {
6567
6905
  if (sessionIdHint) {
6568
6906
  const obs = await kv.get(KV.observations(sessionIdHint), obsId).catch(() => null);
@@ -6576,7 +6914,52 @@ async function findObservation$1(kv, obsId, sessionIdHint) {
6576
6914
  }
6577
6915
  return null;
6578
6916
  }
6579
-
6917
+ //#endregion
6918
+ //#region src/functions/recent-searches-sweep.ts
6919
+ const RETENTION_MS = 1440 * 60 * 1e3;
6920
+ function registerRecentSearchesSweepFunction(sdk, kv) {
6921
+ sdk.registerFunction("mem::diagnostic::recent-searches-sweep", async () => {
6922
+ const cutoff = Date.now() - RETENTION_MS;
6923
+ const rows = await kv.list(KV.recentSearches).catch(() => []);
6924
+ let swept = 0;
6925
+ let skipped = 0;
6926
+ for (const row of rows) {
6927
+ if (!row || typeof row.sessionId !== "string" || !row.sessionId) {
6928
+ skipped++;
6929
+ continue;
6930
+ }
6931
+ if ((typeof row.at === "number" ? row.at : 0) >= cutoff) continue;
6932
+ try {
6933
+ await kv.delete(KV.recentSearches, row.sessionId);
6934
+ swept++;
6935
+ } catch (err) {
6936
+ logger.warn("recent-searches sweep delete failed", {
6937
+ sessionId: row.sessionId,
6938
+ error: err instanceof Error ? err.message : String(err)
6939
+ });
6940
+ }
6941
+ }
6942
+ if (swept > 0 || skipped > 0) logger.info("Recent-searches sweep complete", {
6943
+ swept,
6944
+ skipped
6945
+ });
6946
+ return {
6947
+ success: true,
6948
+ swept,
6949
+ skipped
6950
+ };
6951
+ });
6952
+ sdk.registerFunction("mem::diagnostic::followup-stats", async () => {
6953
+ const stats = getFollowupStats();
6954
+ return {
6955
+ success: true,
6956
+ windowSeconds: getFollowupWindowSeconds(),
6957
+ agentInitiatedSearches: stats.agentInitiatedSearches,
6958
+ followupWithinWindow: stats.followupWithinWindow,
6959
+ rate: stats.rate
6960
+ };
6961
+ });
6962
+ }
6580
6963
  //#endregion
6581
6964
  //#region src/functions/profile.ts
6582
6965
  function registerProfileFunction(sdk, kv) {
@@ -6664,7 +7047,6 @@ function extractConventions(concepts, files) {
6664
7047
  for (const { concept, frequency } of concepts.slice(0, 5)) if (frequency >= 3) conventions.push(`Frequently uses: ${concept}`);
6665
7048
  return conventions;
6666
7049
  }
6667
-
6668
7050
  //#endregion
6669
7051
  //#region src/functions/auto-forget.ts
6670
7052
  const MS_PER_DAY = 1440 * 60 * 1e3;
@@ -6790,11 +7172,6 @@ function registerAutoForgetFunction(sdk, kv) {
6790
7172
  return result;
6791
7173
  });
6792
7174
  }
6793
-
6794
- //#endregion
6795
- //#region src/version.ts
6796
- const VERSION = "0.9.24";
6797
-
6798
7175
  //#endregion
6799
7176
  //#region src/functions/export-import.ts
6800
7177
  function registerExportImportFunction(sdk, kv) {
@@ -6935,7 +7312,8 @@ function registerExportImportFunction(sdk, kv) {
6935
7312
  "0.9.21",
6936
7313
  "0.9.22",
6937
7314
  "0.9.23",
6938
- "0.9.24"
7315
+ "0.9.24",
7316
+ "0.9.25"
6939
7317
  ]).has(importData.version)) return {
6940
7318
  success: false,
6941
7319
  error: `Unsupported export version: ${importData.version}`
@@ -7253,7 +7631,6 @@ function registerExportImportFunction(sdk, kv) {
7253
7631
  };
7254
7632
  });
7255
7633
  }
7256
-
7257
7634
  //#endregion
7258
7635
  //#region src/functions/enrich.ts
7259
7636
  const MAX_CONTEXT_LENGTH = 4e3;
@@ -7314,7 +7691,6 @@ function registerEnrichFunction(sdk, kv) {
7314
7691
  };
7315
7692
  });
7316
7693
  }
7317
-
7318
7694
  //#endregion
7319
7695
  //#region src/functions/claude-bridge.ts
7320
7696
  function parseMemoryMd(content) {
@@ -7435,7 +7811,6 @@ function registerClaudeBridgeFunction(sdk, kv, config) {
7435
7811
  }
7436
7812
  });
7437
7813
  }
7438
-
7439
7814
  //#endregion
7440
7815
  //#region src/prompts/graph-extraction.ts
7441
7816
  const GRAPH_EXTRACTION_SYSTEM = `You are a knowledge graph extraction engine. Given a compressed observation from a coding session, extract entities and relationships.
@@ -7458,9 +7833,41 @@ Rules:
7458
7833
  function buildGraphExtractionPrompt(observations) {
7459
7834
  return `Extract entities and relationships from these observations:\n\n${observations.map((o, i) => `[${i + 1}] Type: ${o.type}\nTitle: ${o.title}\nNarrative: ${o.narrative}\nConcepts: ${(o.concepts ?? []).join(", ")}\nFiles: ${(o.files ?? []).join(", ")}`).join("\n\n")}`;
7460
7835
  }
7461
-
7462
7836
  //#endregion
7463
7837
  //#region src/functions/graph.ts
7838
+ const DEFAULT_GRAPH_QUERY_LIMIT = 500;
7839
+ const MAX_GRAPH_QUERY_LIMIT = 5e3;
7840
+ function resolvePagination(rawLimit, rawOffset) {
7841
+ return {
7842
+ limit: Math.max(1, Math.min(typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.floor(rawLimit) : DEFAULT_GRAPH_QUERY_LIMIT, MAX_GRAPH_QUERY_LIMIT)),
7843
+ offset: Math.max(0, typeof rawOffset === "number" && Number.isFinite(rawOffset) ? Math.floor(rawOffset) : 0)
7844
+ };
7845
+ }
7846
+ function rankByDegree(nodes, edges) {
7847
+ const degree = /* @__PURE__ */ new Map();
7848
+ for (const edge of edges) {
7849
+ degree.set(edge.sourceNodeId, (degree.get(edge.sourceNodeId) ?? 0) + 1);
7850
+ degree.set(edge.targetNodeId, (degree.get(edge.targetNodeId) ?? 0) + 1);
7851
+ }
7852
+ return [...nodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0));
7853
+ }
7854
+ function paginate(nodes, allEdges, depth, limit, offset) {
7855
+ const totalNodes = nodes.length;
7856
+ const pageNodes = nodes.slice(offset, offset + limit);
7857
+ const pageNodeIds = new Set(pageNodes.map((n) => n.id));
7858
+ const pageEdges = allEdges.filter((e) => pageNodeIds.has(e.sourceNodeId) && pageNodeIds.has(e.targetNodeId));
7859
+ const universeIds = new Set(nodes.map((n) => n.id));
7860
+ return {
7861
+ nodes: pageNodes,
7862
+ edges: pageEdges,
7863
+ depth,
7864
+ totalNodes,
7865
+ totalEdges: allEdges.reduce((count, e) => universeIds.has(e.sourceNodeId) && universeIds.has(e.targetNodeId) ? count + 1 : count, 0),
7866
+ truncated: totalNodes > pageNodes.length,
7867
+ limit,
7868
+ offset
7869
+ };
7870
+ }
7464
7871
  function parseAttrs(raw) {
7465
7872
  const attrs = {};
7466
7873
  const attrRegex = /([A-Za-z_][\w:-]*)="([^"]*)"/g;
@@ -7597,15 +8004,10 @@ function registerGraphFunction(sdk, kv, provider) {
7597
8004
  const allNodes = (await kv.list(KV.graphNodes)).filter((n) => !n.stale);
7598
8005
  const allEdges = (await kv.list(KV.graphEdges)).filter((e) => !e.stale);
7599
8006
  const maxDepth = Math.min(data.maxDepth || 3, 5);
8007
+ const { limit, offset } = resolvePagination(data.limit, data.offset);
7600
8008
  if (data.query) {
7601
8009
  const lower = data.query.toLowerCase();
7602
- const matchingNodes = allNodes.filter((n) => n.name.toLowerCase().includes(lower) || Object.values(n.properties).some((v) => typeof v === "string" && v.toLowerCase().includes(lower)));
7603
- const nodeIds = new Set(matchingNodes.map((n) => n.id));
7604
- return {
7605
- nodes: matchingNodes,
7606
- edges: allEdges.filter((e) => nodeIds.has(e.sourceNodeId) || nodeIds.has(e.targetNodeId)),
7607
- depth: 0
7608
- };
8010
+ return paginate(allNodes.filter((n) => n.name.toLowerCase().includes(lower) || Object.values(n.properties).some((v) => typeof v === "string" && v.toLowerCase().includes(lower))), allEdges, 0, limit, offset);
7609
8011
  }
7610
8012
  if (data.startNodeId) {
7611
8013
  const visited = /* @__PURE__ */ new Set();
@@ -7637,19 +8039,11 @@ function registerGraphFunction(sdk, kv, provider) {
7637
8039
  });
7638
8040
  }
7639
8041
  }
7640
- return {
7641
- nodes: resultNodes,
7642
- edges: resultEdges,
7643
- depth: maxDepth
7644
- };
8042
+ return paginate(resultNodes, resultEdges, maxDepth, limit, offset);
7645
8043
  }
7646
8044
  let filtered = allNodes;
7647
8045
  if (data.nodeType) filtered = allNodes.filter((n) => n.type === data.nodeType);
7648
- return {
7649
- nodes: filtered,
7650
- edges: allEdges,
7651
- depth: 0
7652
- };
8046
+ return paginate(rankByDegree(filtered, allEdges), allEdges, 0, limit, offset);
7653
8047
  });
7654
8048
  sdk.registerFunction("mem::graph-stats", async () => {
7655
8049
  const nodes = await kv.list(KV.graphNodes);
@@ -7666,7 +8060,6 @@ function registerGraphFunction(sdk, kv, provider) {
7666
8060
  };
7667
8061
  });
7668
8062
  }
7669
-
7670
8063
  //#endregion
7671
8064
  //#region src/prompts/consolidation.ts
7672
8065
  const SEMANTIC_MERGE_SYSTEM = `You are a memory consolidation engine. Given overlapping episodic memories (session summaries), extract stable factual knowledge.
@@ -7701,7 +8094,6 @@ Rules:
7701
8094
  function buildProceduralExtractionPrompt(patterns) {
7702
8095
  return `Extract reusable procedures from these recurring patterns:\n\n${patterns.map((p, i) => `[Pattern ${i + 1}] (seen ${p.frequency}x)\n${p.content}`).join("\n\n")}`;
7703
8096
  }
7704
-
7705
8097
  //#endregion
7706
8098
  //#region src/functions/consolidation-pipeline.ts
7707
8099
  function applyDecay(items, decayDays) {
@@ -7895,7 +8287,6 @@ function registerConsolidationPipelineFunction(sdk, kv, provider) {
7895
8287
  };
7896
8288
  });
7897
8289
  }
7898
-
7899
8290
  //#endregion
7900
8291
  //#region src/functions/team.ts
7901
8292
  const VALID_ITEM_TYPES = new Set([
@@ -7999,7 +8390,6 @@ function registerTeamFunction(sdk, kv, config) {
7999
8390
  return profile;
8000
8391
  });
8001
8392
  }
8002
-
8003
8393
  //#endregion
8004
8394
  //#region src/functions/governance.ts
8005
8395
  function registerGovernanceFunction(sdk, kv) {
@@ -8108,7 +8498,6 @@ function registerGovernanceFunction(sdk, kv) {
8108
8498
  return queryAudit(kv, data);
8109
8499
  });
8110
8500
  }
8111
-
8112
8501
  //#endregion
8113
8502
  //#region src/functions/snapshot.ts
8114
8503
  const COMMIT_HASH_RE = /^[0-9a-f]{7,40}$/i;
@@ -8273,7 +8662,6 @@ function registerSnapshotFunction(sdk, kv, snapshotDir) {
8273
8662
  }
8274
8663
  });
8275
8664
  }
8276
-
8277
8665
  //#endregion
8278
8666
  //#region src/functions/actions.ts
8279
8667
  function registerActionsFunction(sdk, kv) {
@@ -8475,7 +8863,6 @@ async function propagateCompletion(kv, completedActionId) {
8475
8863
  });
8476
8864
  }
8477
8865
  }
8478
-
8479
8866
  //#endregion
8480
8867
  //#region src/functions/frontier.ts
8481
8868
  function registerFrontierFunction(sdk, kv) {
@@ -8580,7 +8967,6 @@ function computeScore(action, edges, now) {
8580
8967
  if (action.status === "active") score += 15;
8581
8968
  return Math.round(score * 100) / 100;
8582
8969
  }
8583
-
8584
8970
  //#endregion
8585
8971
  //#region src/functions/leases.ts
8586
8972
  const DEFAULT_LEASE_TTL_MS = 600 * 1e3;
@@ -8761,7 +9147,6 @@ function registerLeasesFunction(sdk, kv) {
8761
9147
  };
8762
9148
  });
8763
9149
  }
8764
-
8765
9150
  //#endregion
8766
9151
  //#region src/functions/routines.ts
8767
9152
  function registerRoutinesFunction(sdk, kv) {
@@ -9008,7 +9393,6 @@ function registerRoutinesFunction(sdk, kv) {
9008
9393
  });
9009
9394
  });
9010
9395
  }
9011
-
9012
9396
  //#endregion
9013
9397
  //#region src/functions/signals.ts
9014
9398
  function registerSignalsFunction(sdk, kv) {
@@ -9140,7 +9524,6 @@ function registerSignalsFunction(sdk, kv) {
9140
9524
  };
9141
9525
  });
9142
9526
  }
9143
-
9144
9527
  //#endregion
9145
9528
  //#region src/functions/checkpoints.ts
9146
9529
  function registerCheckpointsFunction(sdk, kv) {
@@ -9313,7 +9696,6 @@ function registerCheckpointsFunction(sdk, kv) {
9313
9696
  };
9314
9697
  });
9315
9698
  }
9316
-
9317
9699
  //#endregion
9318
9700
  //#region src/functions/flow-compress.ts
9319
9701
  const FLOW_COMPRESS_SYSTEM = `You are a workflow summarizer. Given a completed action chain, produce a concise summary capturing:
@@ -9458,7 +9840,6 @@ function extractFiles(actions) {
9458
9840
  }
9459
9841
  return Array.from(files);
9460
9842
  }
9461
-
9462
9843
  //#endregion
9463
9844
  //#region src/functions/mesh.ts
9464
9845
  function isPrivateIP(ip) {
@@ -9775,7 +10156,6 @@ async function applySyncData(kv, data, scopes) {
9775
10156
  if (scopes.includes("graph:edges")) applied += await lwwMergeList(kv, KV.graphEdges, data.graphEdges, "mem:gedge", "createdAt");
9776
10157
  return applied;
9777
10158
  }
9778
-
9779
10159
  //#endregion
9780
10160
  //#region src/functions/branch-aware.ts
9781
10161
  function execAsync(cmd, args, cwd) {
@@ -9890,7 +10270,6 @@ function registerBranchAwareFunction(sdk, kv) {
9890
10270
  };
9891
10271
  });
9892
10272
  }
9893
-
9894
10273
  //#endregion
9895
10274
  //#region src/functions/sentinels.ts
9896
10275
  const VALID_TYPES = [
@@ -10211,7 +10590,6 @@ async function unblockLinkedActions(kv, sentinel) {
10211
10590
  });
10212
10591
  return unblockedCount;
10213
10592
  }
10214
-
10215
10593
  //#endregion
10216
10594
  //#region src/functions/sketches.ts
10217
10595
  function registerSketchesFunction(sdk, kv) {
@@ -10453,7 +10831,6 @@ function registerSketchesFunction(sdk, kv) {
10453
10831
  };
10454
10832
  });
10455
10833
  }
10456
-
10457
10834
  //#endregion
10458
10835
  //#region src/functions/crystallize.ts
10459
10836
  const CRYSTALLIZE_SYSTEM = `You are summarizing a completed chain of agent actions into a compact digest.
@@ -10650,7 +11027,6 @@ function parseDigest(response) {
10650
11027
  };
10651
11028
  }
10652
11029
  }
10653
-
10654
11030
  //#endregion
10655
11031
  //#region src/functions/diagnostics.ts
10656
11032
  const ALL_CATEGORIES = [
@@ -11364,7 +11740,6 @@ function registerDiagnosticsFunction(sdk, kv) {
11364
11740
  };
11365
11741
  });
11366
11742
  }
11367
-
11368
11743
  //#endregion
11369
11744
  //#region src/functions/facets.ts
11370
11745
  function registerFacetsFunction(sdk, kv) {
@@ -11536,7 +11911,6 @@ function registerFacetsFunction(sdk, kv) {
11536
11911
  };
11537
11912
  });
11538
11913
  }
11539
-
11540
11914
  //#endregion
11541
11915
  //#region src/functions/verify.ts
11542
11916
  function registerVerifyFunction(sdk, kv) {
@@ -11631,7 +12005,6 @@ async function findObservation(kv, obsId, hintSessionIds) {
11631
12005
  }
11632
12006
  return null;
11633
12007
  }
11634
-
11635
12008
  //#endregion
11636
12009
  //#region src/functions/cascade.ts
11637
12010
  function registerCascadeFunction(sdk, kv) {
@@ -11701,7 +12074,6 @@ function registerCascadeFunction(sdk, kv) {
11701
12074
  };
11702
12075
  });
11703
12076
  }
11704
-
11705
12077
  //#endregion
11706
12078
  //#region src/functions/lessons.ts
11707
12079
  function reinforceLesson(lesson) {
@@ -11887,7 +12259,6 @@ function registerLessonsFunctions(sdk, kv) {
11887
12259
  };
11888
12260
  });
11889
12261
  }
11890
-
11891
12262
  //#endregion
11892
12263
  //#region src/functions/obsidian-export.ts
11893
12264
  const DEFAULT_EXPORT_ROOT = join(homedir(), ".agentmemory");
@@ -11903,6 +12274,20 @@ function resolveVaultDir(vaultDir) {
11903
12274
  function sanitize(name) {
11904
12275
  return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").slice(0, 100);
11905
12276
  }
12277
+ function hasExportId(item) {
12278
+ return !!item && typeof item.id === "string" && item.id.length > 0;
12279
+ }
12280
+ function safeArray(value) {
12281
+ return Array.isArray(value) ? value : [];
12282
+ }
12283
+ function safeString(value, fallback = "") {
12284
+ return typeof value === "string" ? value : fallback;
12285
+ }
12286
+ function safeTimestamp(value) {
12287
+ if (typeof value !== "string") return 0;
12288
+ const time = new Date(value).getTime();
12289
+ return Number.isFinite(time) ? time : 0;
12290
+ }
11906
12291
  function toFrontmatter(obj) {
11907
12292
  const lines = ["---"];
11908
12293
  for (const [key, value] of Object.entries(obj)) {
@@ -11914,6 +12299,11 @@ function toFrontmatter(obj) {
11914
12299
  return lines.join("\n");
11915
12300
  }
11916
12301
  function memoryToMd(m) {
12302
+ const concepts = safeArray(m.concepts);
12303
+ const files = safeArray(m.files);
12304
+ const relatedIds = safeArray(m.relatedIds);
12305
+ const supersedes = safeArray(m.supersedes);
12306
+ const title = safeString(m.title, m.id);
11917
12307
  const fm = toFrontmatter({
11918
12308
  id: m.id,
11919
12309
  type: m.type,
@@ -11921,24 +12311,28 @@ function memoryToMd(m) {
11921
12311
  updated: m.updatedAt,
11922
12312
  strength: m.strength,
11923
12313
  version: m.version,
11924
- concepts: m.concepts,
11925
- files: m.files
12314
+ concepts,
12315
+ files
11926
12316
  });
11927
- const related = (m.relatedIds || []).map((id) => `- [[${id}]]`).join("\n");
11928
- const supersedes = (m.supersedes || []).map((id) => `- [[${id}]] (superseded)`).join("\n");
12317
+ const relatedLines = relatedIds.map((id) => `- [[${id}]]`).join("\n");
12318
+ const supersedesLines = supersedes.map((id) => `- [[${id}]] (superseded)`).join("\n");
11929
12319
  const sections = [
11930
12320
  fm,
11931
12321
  "",
11932
- `# ${m.title}`,
12322
+ `# ${title}`,
11933
12323
  "",
11934
- m.content
12324
+ safeString(m.content)
11935
12325
  ];
11936
- if (m.concepts.length > 0) sections.push("", "## Concepts", m.concepts.map((c) => `#${c.replace(/\s+/g, "-")}`).join(" "));
11937
- if (related) sections.push("", "## Related", related);
11938
- if (supersedes) sections.push("", "## Supersedes", supersedes);
12326
+ if (concepts.length > 0) sections.push("", "## Concepts", concepts.map((c) => `#${c.replace(/\s+/g, "-")}`).join(" "));
12327
+ if (relatedLines) sections.push("", "## Related", relatedLines);
12328
+ if (supersedesLines) sections.push("", "## Supersedes", supersedesLines);
11939
12329
  return sections.join("\n");
11940
12330
  }
11941
12331
  function lessonToMd(l) {
12332
+ const tags = safeArray(l.tags);
12333
+ const sourceIds = safeArray(l.sourceIds);
12334
+ const content = safeString(l.content);
12335
+ const headline = content ? content.slice(0, 80) : l.id;
11942
12336
  const fm = toFrontmatter({
11943
12337
  id: l.id,
11944
12338
  type: "lesson",
@@ -11948,66 +12342,76 @@ function lessonToMd(l) {
11948
12342
  created: l.createdAt,
11949
12343
  updated: l.updatedAt,
11950
12344
  project: l.project,
11951
- tags: l.tags,
12345
+ tags,
11952
12346
  decayRate: l.decayRate
11953
12347
  });
11954
- const sourceLinks = l.sourceIds.map((id) => `- [[${id}]]`).join("\n");
12348
+ const sourceLinks = sourceIds.map((id) => `- [[${id}]]`).join("\n");
11955
12349
  const sections = [
11956
12350
  fm,
11957
12351
  "",
11958
- `# Lesson: ${l.content.slice(0, 80)}`,
12352
+ `# Lesson: ${headline}`,
11959
12353
  "",
11960
- l.content
12354
+ content
11961
12355
  ];
11962
12356
  if (l.context) sections.push("", "## Context", l.context);
11963
- if (l.tags.length > 0) sections.push("", "## Tags", l.tags.map((t) => `#${t.replace(/\s+/g, "-")}`).join(" "));
12357
+ if (tags.length > 0) sections.push("", "## Tags", tags.map((t) => `#${t.replace(/\s+/g, "-")}`).join(" "));
11964
12358
  if (sourceLinks) sections.push("", "## Sources", sourceLinks);
11965
12359
  return sections.join("\n");
11966
12360
  }
11967
12361
  function crystalToMd(c) {
12362
+ const keyOutcomes = safeArray(c.keyOutcomes);
12363
+ const lessons = safeArray(c.lessons);
12364
+ const filesAffected = safeArray(c.filesAffected);
12365
+ const sourceActionIds = safeArray(c.sourceActionIds);
12366
+ const narrative = safeString(c.narrative);
12367
+ const headline = narrative ? narrative.slice(0, 80) : c.id;
11968
12368
  const fm = toFrontmatter({
11969
12369
  id: c.id,
11970
12370
  type: "crystal",
11971
12371
  created: c.createdAt,
11972
12372
  project: c.project,
11973
12373
  sessionId: c.sessionId,
11974
- filesAffected: c.filesAffected
12374
+ filesAffected
11975
12375
  });
11976
- const actionLinks = c.sourceActionIds.map((id) => `- [[${id}]]`).join("\n");
12376
+ const actionLinks = sourceActionIds.map((id) => `- [[${id}]]`).join("\n");
11977
12377
  const sections = [
11978
12378
  fm,
11979
12379
  "",
11980
- `# Crystal: ${c.narrative.slice(0, 80)}`,
12380
+ `# Crystal: ${headline}`,
11981
12381
  "",
11982
- c.narrative,
12382
+ narrative,
11983
12383
  "",
11984
12384
  "## Key Outcomes",
11985
- ...c.keyOutcomes.map((o) => `- ${o}`)
12385
+ ...keyOutcomes.map((o) => `- ${o}`)
11986
12386
  ];
11987
- if (c.lessons.length > 0) sections.push("", "## Lessons", ...c.lessons.map((l) => `- ${l}`));
11988
- if (c.filesAffected.length > 0) sections.push("", "## Files", ...c.filesAffected.map((f) => `- \`${f}\``));
12387
+ if (lessons.length > 0) sections.push("", "## Lessons", ...lessons.map((l) => `- ${l}`));
12388
+ if (filesAffected.length > 0) sections.push("", "## Files", ...filesAffected.map((f) => `- \`${f}\``));
11989
12389
  if (actionLinks) sections.push("", "## Source Actions", actionLinks);
11990
12390
  return sections.join("\n");
11991
12391
  }
11992
12392
  function sessionToMd(s) {
12393
+ const project = safeString(s.project, "unknown");
12394
+ const status = safeString(s.status, "unknown");
12395
+ const startedAt = safeString(s.startedAt, "");
12396
+ const cwd = safeString(s.cwd, "");
11993
12397
  return [
11994
12398
  toFrontmatter({
11995
12399
  id: s.id,
11996
12400
  type: "session",
11997
- project: s.project,
11998
- status: s.status,
11999
- started: s.startedAt,
12401
+ project,
12402
+ status,
12403
+ started: startedAt || void 0,
12000
12404
  ended: s.endedAt,
12001
12405
  observations: s.observationCount
12002
12406
  }),
12003
12407
  "",
12004
- `# Session: ${s.project}`,
12408
+ `# Session: ${project}`,
12005
12409
  "",
12006
- `**Status:** ${s.status}`,
12007
- `**Started:** ${s.startedAt}`,
12410
+ `**Status:** ${status}`,
12411
+ startedAt ? `**Started:** ${startedAt}` : "",
12008
12412
  s.endedAt ? `**Ended:** ${s.endedAt}` : "",
12009
- `**Observations:** ${s.observationCount}`,
12010
- `**CWD:** \`${s.cwd}\``
12413
+ `**Observations:** ${s.observationCount ?? 0}`,
12414
+ cwd ? `**CWD:** \`${cwd}\`` : ""
12011
12415
  ].filter(Boolean).join("\n");
12012
12416
  }
12013
12417
  function registerObsidianExportFunction(sdk, kv) {
@@ -12043,122 +12447,131 @@ function registerObsidianExportFunction(sdk, kv) {
12043
12447
  crystals: join(vaultDir, "crystals"),
12044
12448
  sessions: join(vaultDir, "sessions")
12045
12449
  };
12046
- await Promise.all(Object.values(dirs).map((dir) => mkdir(dir, { recursive: true })));
12047
- const stats = {
12048
- memories: 0,
12049
- lessons: 0,
12050
- crystals: 0,
12051
- sessions: 0
12052
- };
12053
- const errors = [];
12054
- const memoryMoc = [];
12055
- const lessonMoc = [];
12056
- const crystalMoc = [];
12057
- const sessionMoc = [];
12058
- const [memories, lessons, crystals, sessions] = await Promise.all([
12059
- exportTypes.has("memories") ? kv.list(KV.memories) : Promise.resolve([]),
12060
- exportTypes.has("lessons") ? kv.list(KV.lessons) : Promise.resolve([]),
12061
- exportTypes.has("crystals") ? kv.list(KV.crystals) : Promise.resolve([]),
12062
- exportTypes.has("sessions") ? kv.list(KV.sessions) : Promise.resolve([])
12063
- ]);
12064
- for (const m of memories.filter((m) => m.isLatest)) {
12065
- const filename = `${sanitize(m.id)}.md`;
12066
- const filepath = join(dirs.memories, filename);
12067
- try {
12068
- await writeFile(filepath, memoryToMd(m));
12069
- stats.memories++;
12070
- memoryMoc.push(`- [[memories/${sanitize(m.id)}|${m.title}]] (${m.type}, strength: ${m.strength})`);
12071
- } catch (err) {
12072
- errors.push({
12073
- id: m.id,
12074
- path: filepath,
12075
- error: err instanceof Error ? err.message : String(err)
12076
- });
12450
+ try {
12451
+ await Promise.all(Object.values(dirs).map((dir) => mkdir(dir, { recursive: true })));
12452
+ const stats = {
12453
+ memories: 0,
12454
+ lessons: 0,
12455
+ crystals: 0,
12456
+ sessions: 0
12457
+ };
12458
+ const errors = [];
12459
+ const memoryMoc = [];
12460
+ const lessonMoc = [];
12461
+ const crystalMoc = [];
12462
+ const sessionMoc = [];
12463
+ const [memories, lessons, crystals, sessions] = await Promise.all([
12464
+ exportTypes.has("memories") ? kv.list(KV.memories) : Promise.resolve([]),
12465
+ exportTypes.has("lessons") ? kv.list(KV.lessons) : Promise.resolve([]),
12466
+ exportTypes.has("crystals") ? kv.list(KV.crystals) : Promise.resolve([]),
12467
+ exportTypes.has("sessions") ? kv.list(KV.sessions) : Promise.resolve([])
12468
+ ]);
12469
+ for (const m of memories.filter((m) => hasExportId(m) && m.isLatest === true)) {
12470
+ const filename = `${sanitize(m.id)}.md`;
12471
+ const filepath = join(dirs.memories, filename);
12472
+ try {
12473
+ await writeFile(filepath, memoryToMd(m));
12474
+ stats.memories++;
12475
+ memoryMoc.push(`- [[memories/${sanitize(m.id)}|${safeString(m.title, m.id)}]] (${m.type}, strength: ${m.strength ?? 0})`);
12476
+ } catch (err) {
12477
+ errors.push({
12478
+ id: m.id,
12479
+ path: filepath,
12480
+ error: err instanceof Error ? err.message : String(err)
12481
+ });
12482
+ }
12077
12483
  }
12078
- }
12079
- for (const l of lessons.filter((l) => !l.deleted)) {
12080
- const filename = `${sanitize(l.id)}.md`;
12081
- const filepath = join(dirs.lessons, filename);
12082
- try {
12083
- await writeFile(filepath, lessonToMd(l));
12084
- stats.lessons++;
12085
- lessonMoc.push(`- [[lessons/${sanitize(l.id)}|${l.content.slice(0, 60)}]] (confidence: ${l.confidence})`);
12086
- } catch (err) {
12087
- errors.push({
12088
- id: l.id,
12089
- path: filepath,
12090
- error: err instanceof Error ? err.message : String(err)
12091
- });
12484
+ for (const l of lessons.filter((l) => hasExportId(l) && !l.deleted)) {
12485
+ const filename = `${sanitize(l.id)}.md`;
12486
+ const filepath = join(dirs.lessons, filename);
12487
+ try {
12488
+ await writeFile(filepath, lessonToMd(l));
12489
+ stats.lessons++;
12490
+ const headline = safeString(l.content).slice(0, 60) || l.id;
12491
+ lessonMoc.push(`- [[lessons/${sanitize(l.id)}|${headline}]] (confidence: ${l.confidence ?? 0})`);
12492
+ } catch (err) {
12493
+ errors.push({
12494
+ id: l.id,
12495
+ path: filepath,
12496
+ error: err instanceof Error ? err.message : String(err)
12497
+ });
12498
+ }
12092
12499
  }
12093
- }
12094
- for (const c of crystals) {
12095
- const filename = `${sanitize(c.id)}.md`;
12096
- const filepath = join(dirs.crystals, filename);
12097
- try {
12098
- await writeFile(filepath, crystalToMd(c));
12099
- stats.crystals++;
12100
- crystalMoc.push(`- [[crystals/${sanitize(c.id)}|${c.narrative.slice(0, 60)}]]`);
12101
- } catch (err) {
12102
- errors.push({
12103
- id: c.id,
12104
- path: filepath,
12105
- error: err instanceof Error ? err.message : String(err)
12106
- });
12500
+ for (const c of crystals.filter(hasExportId)) {
12501
+ const filename = `${sanitize(c.id)}.md`;
12502
+ const filepath = join(dirs.crystals, filename);
12503
+ try {
12504
+ await writeFile(filepath, crystalToMd(c));
12505
+ stats.crystals++;
12506
+ const headline = safeString(c.narrative).slice(0, 60) || c.id;
12507
+ crystalMoc.push(`- [[crystals/${sanitize(c.id)}|${headline}]]`);
12508
+ } catch (err) {
12509
+ errors.push({
12510
+ id: c.id,
12511
+ path: filepath,
12512
+ error: err instanceof Error ? err.message : String(err)
12513
+ });
12514
+ }
12107
12515
  }
12108
- }
12109
- const recent = sessions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()).slice(0, 50);
12110
- for (const s of recent) {
12111
- const filename = `${sanitize(s.id)}.md`;
12112
- const filepath = join(dirs.sessions, filename);
12113
- try {
12114
- await writeFile(filepath, sessionToMd(s));
12115
- stats.sessions++;
12116
- sessionMoc.push(`- [[sessions/${sanitize(s.id)}|${s.project} (${s.status})]]`);
12117
- } catch (err) {
12118
- errors.push({
12119
- id: s.id,
12120
- path: filepath,
12121
- error: err instanceof Error ? err.message : String(err)
12122
- });
12516
+ const recent = sessions.filter(hasExportId).sort((a, b) => safeTimestamp(b.startedAt) - safeTimestamp(a.startedAt)).slice(0, 50);
12517
+ for (const s of recent) {
12518
+ const filename = `${sanitize(s.id)}.md`;
12519
+ const filepath = join(dirs.sessions, filename);
12520
+ try {
12521
+ await writeFile(filepath, sessionToMd(s));
12522
+ stats.sessions++;
12523
+ sessionMoc.push(`- [[sessions/${sanitize(s.id)}|${safeString(s.project, "unknown")} (${safeString(s.status, "unknown")})]]`);
12524
+ } catch (err) {
12525
+ errors.push({
12526
+ id: s.id,
12527
+ path: filepath,
12528
+ error: err instanceof Error ? err.message : String(err)
12529
+ });
12530
+ }
12123
12531
  }
12532
+ const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
12533
+ const moc = [
12534
+ "---",
12535
+ "type: moc",
12536
+ `exported: ${exportedAt}`,
12537
+ "---",
12538
+ "",
12539
+ "# agentmemory vault",
12540
+ "",
12541
+ `Exported: ${exportedAt}`,
12542
+ "",
12543
+ `## Memories (${stats.memories})`,
12544
+ ...memoryMoc,
12545
+ "",
12546
+ `## Lessons (${stats.lessons})`,
12547
+ ...lessonMoc,
12548
+ "",
12549
+ `## Crystals (${stats.crystals})`,
12550
+ ...crystalMoc,
12551
+ "",
12552
+ `## Sessions (${stats.sessions})`,
12553
+ ...sessionMoc
12554
+ ].join("\n");
12555
+ await writeFile(join(vaultDir, "MOC.md"), moc);
12556
+ await recordAudit(kv, "obsidian_export", "mem::obsidian-export", [], {
12557
+ vaultDir,
12558
+ stats
12559
+ });
12560
+ return {
12561
+ success: true,
12562
+ exported: stats,
12563
+ errors: errors.length > 0 ? errors : void 0,
12564
+ vaultDir
12565
+ };
12566
+ } catch (err) {
12567
+ return {
12568
+ success: false,
12569
+ error: err instanceof Error ? err.message : String(err),
12570
+ vaultDir
12571
+ };
12124
12572
  }
12125
- const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
12126
- const moc = [
12127
- "---",
12128
- "type: moc",
12129
- `exported: ${exportedAt}`,
12130
- "---",
12131
- "",
12132
- "# agentmemory vault",
12133
- "",
12134
- `Exported: ${exportedAt}`,
12135
- "",
12136
- `## Memories (${stats.memories})`,
12137
- ...memoryMoc,
12138
- "",
12139
- `## Lessons (${stats.lessons})`,
12140
- ...lessonMoc,
12141
- "",
12142
- `## Crystals (${stats.crystals})`,
12143
- ...crystalMoc,
12144
- "",
12145
- `## Sessions (${stats.sessions})`,
12146
- ...sessionMoc
12147
- ].join("\n");
12148
- await writeFile(join(vaultDir, "MOC.md"), moc);
12149
- await recordAudit(kv, "obsidian_export", "mem::obsidian-export", [], {
12150
- vaultDir,
12151
- stats
12152
- });
12153
- return {
12154
- success: true,
12155
- exported: stats,
12156
- errors: errors.length > 0 ? errors : void 0,
12157
- vaultDir
12158
- };
12159
12573
  });
12160
12574
  }
12161
-
12162
12575
  //#endregion
12163
12576
  //#region src/prompts/reflect.ts
12164
12577
  const REFLECT_SYSTEM = `You are a higher-order reasoning engine. Given a cluster of related concepts, facts, lessons, and action outcomes, synthesize cross-cutting insights that span multiple individual memories.
@@ -12186,7 +12599,6 @@ function buildReflectPrompt(cluster) {
12186
12599
  if (cluster.crystalNarratives.length > 0) sections.push("\n## Completed Work Summaries", ...cluster.crystalNarratives.map((n) => `- ${n}`));
12187
12600
  return `Synthesize higher-order insights from this cluster of related memories:\n\n${sections.join("\n")}`;
12188
12601
  }
12189
-
12190
12602
  //#endregion
12191
12603
  //#region src/functions/reflect.ts
12192
12604
  function reinforceInsight(insight) {
@@ -12479,7 +12891,6 @@ function registerReflectFunctions(sdk, kv, provider) {
12479
12891
  };
12480
12892
  });
12481
12893
  }
12482
-
12483
12894
  //#endregion
12484
12895
  //#region src/functions/working-memory.ts
12485
12896
  const CORE_SCOPE = "mem:core-memory";
@@ -12654,7 +13065,6 @@ function registerWorkingMemoryFunctions(sdk, kv, tokenBudget) {
12654
13065
  };
12655
13066
  });
12656
13067
  }
12657
-
12658
13068
  //#endregion
12659
13069
  //#region src/functions/skill-extract.ts
12660
13070
  const SKILL_EXTRACT_SYSTEM = `You are a skill extraction engine. Given a completed multi-step task session, extract a reusable procedural skill document.
@@ -12857,7 +13267,6 @@ function registerSkillExtractFunctions(sdk, kv, provider) {
12857
13267
  };
12858
13268
  });
12859
13269
  }
12860
-
12861
13270
  //#endregion
12862
13271
  //#region src/functions/sliding-window.ts
12863
13272
  const SLIDING_WINDOW_SYSTEM = `You are a contextual enrichment engine. Given a primary observation and its surrounding context window (previous and next observations from the same session), produce an enriched version.
@@ -13044,7 +13453,6 @@ function registerSlidingWindowFunction(sdk, kv, provider) {
13044
13453
  };
13045
13454
  });
13046
13455
  }
13047
-
13048
13456
  //#endregion
13049
13457
  //#region src/functions/temporal-graph.ts
13050
13458
  const TEMPORAL_EXTRACTION_SYSTEM = `You are a temporal knowledge extraction engine. Given observations, extract entities AND their temporal relationships with full context metadata.
@@ -13308,7 +13716,6 @@ function buildTimeline(edges) {
13308
13716
  context: e.context
13309
13717
  }));
13310
13718
  }
13311
-
13312
13719
  //#endregion
13313
13720
  //#region src/functions/retention.ts
13314
13721
  const DEFAULT_DECAY = {
@@ -13542,7 +13949,6 @@ function registerRetentionFunctions(sdk, kv) {
13542
13949
  };
13543
13950
  });
13544
13951
  }
13545
-
13546
13952
  //#endregion
13547
13953
  //#region src/functions/compress-file.ts
13548
13954
  const SENSITIVE_PATH_TERMS = [
@@ -13684,7 +14090,6 @@ function registerCompressFileFunction(sdk, kv, provider) {
13684
14090
  };
13685
14091
  });
13686
14092
  }
13687
-
13688
14093
  //#endregion
13689
14094
  //#region src/replay/jsonl-parser.ts
13690
14095
  function deriveProject(cwd) {
@@ -13811,7 +14216,6 @@ function parseJsonlText(text, fallbackSessionId) {
13811
14216
  observations
13812
14217
  };
13813
14218
  }
13814
-
13815
14219
  //#endregion
13816
14220
  //#region src/replay/timeline.ts
13817
14221
  const DEFAULT_CHARS_PER_SEC = 40;
@@ -13906,10 +14310,6 @@ function projectTimeline(observations) {
13906
14310
  events
13907
14311
  };
13908
14312
  }
13909
-
13910
- //#endregion
13911
- //#region src/functions/replay.ts
13912
- const MAX_FILES_DEFAULT = 200;
13913
14313
  const MAX_FILES_UPPER_BOUND = 1e3;
13914
14314
  const SENSITIVE_PATH_PATTERNS = [
13915
14315
  /(^|[\\/_.-])secret([\\/_.-]|s?$)/i,
@@ -14129,7 +14529,7 @@ function registerReplayFunctions(sdk, kv) {
14129
14529
  error: "path not found"
14130
14530
  };
14131
14531
  }
14132
- const maxFiles = Number.isInteger(data.maxFiles) && data.maxFiles > 0 ? Math.min(data.maxFiles, MAX_FILES_UPPER_BOUND) : MAX_FILES_DEFAULT;
14532
+ const maxFiles = Number.isInteger(data.maxFiles) && data.maxFiles > 0 ? Math.min(data.maxFiles, MAX_FILES_UPPER_BOUND) : 200;
14133
14533
  let files = [];
14134
14534
  let truncated = false;
14135
14535
  let discovered = 0;
@@ -14185,7 +14585,8 @@ function registerReplayFunctions(sdk, kv) {
14185
14585
  const existingTags = existing.tags || [];
14186
14586
  if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
14187
14587
  if (!existing.firstPrompt && firstPrompt) existing.firstPrompt = firstPrompt;
14188
- await kv.set(KV.sessions, existing.id, existing);
14588
+ if (!existing.id) existing.id = parsed.sessionId;
14589
+ await kv.set(KV.sessions, parsed.sessionId, existing);
14189
14590
  } else {
14190
14591
  const session = {
14191
14592
  id: parsed.sessionId,
@@ -14231,7 +14632,6 @@ function registerReplayFunctions(sdk, kv) {
14231
14632
  };
14232
14633
  });
14233
14634
  }
14234
-
14235
14635
  //#endregion
14236
14636
  //#region src/health/thresholds.ts
14237
14637
  const DEFAULTS = {
@@ -14290,7 +14690,6 @@ function evaluateHealth(snapshot, config = {}) {
14290
14690
  notes
14291
14691
  };
14292
14692
  }
14293
-
14294
14693
  //#endregion
14295
14694
  //#region src/health/monitor.ts
14296
14695
  function registerHealthMonitor(sdk, kv) {
@@ -14378,7 +14777,6 @@ function registerHealthMonitor(sdk, kv) {
14378
14777
  async function getLatestHealth(kv) {
14379
14778
  return kv.get(KV.health, "latest");
14380
14779
  }
14381
-
14382
14780
  //#endregion
14383
14781
  //#region src/auth.ts
14384
14782
  const hmacKey = randomBytes(32);
@@ -14404,7 +14802,6 @@ function buildViewerCsp(nonce) {
14404
14802
  "font-src 'self'"
14405
14803
  ].join("; ");
14406
14804
  }
14407
-
14408
14805
  //#endregion
14409
14806
  //#region src/viewer/document.ts
14410
14807
  const VIEWER_VERSION_PLACEHOLDER = "__AGENTMEMORY_VERSION__";
@@ -14430,7 +14827,6 @@ function renderViewerDocument() {
14430
14827
  csp: buildViewerCsp(nonce)
14431
14828
  };
14432
14829
  }
14433
-
14434
14830
  //#endregion
14435
14831
  //#region src/viewer/server.ts
14436
14832
  function loadViewerFavicon() {
@@ -14445,18 +14841,30 @@ function loadViewerFavicon() {
14445
14841
  } catch {}
14446
14842
  return null;
14447
14843
  }
14844
+ const VIEWER_FAVICON = loadViewerFavicon();
14448
14845
  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());
14449
- const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
14450
- function buildAllowedHosts(origins, listenPort) {
14846
+ function readAllowedHostsOverride() {
14847
+ return (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
14848
+ }
14849
+ function resolveViewerHost() {
14850
+ return process.env.AGENTMEMORY_VIEWER_HOST?.trim() || "127.0.0.1";
14851
+ }
14852
+ function isLoopbackHost(host) {
14853
+ const h = host.trim().toLowerCase();
14854
+ return h === "127.0.0.1" || h === "::1" || h === "localhost";
14855
+ }
14856
+ function buildAllowedHosts(origins, listenPort, bindHost = "127.0.0.1") {
14451
14857
  const hosts = /* @__PURE__ */ new Set();
14452
- for (const o of origins) try {
14453
- const parsed = new URL(o);
14454
- if (parsed.host) hosts.add(parsed.host.toLowerCase());
14455
- } catch {}
14456
- hosts.add(`localhost:${listenPort}`);
14457
- hosts.add(`127.0.0.1:${listenPort}`);
14458
- hosts.add(`[::1]:${listenPort}`);
14459
- for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
14858
+ if (isLoopbackHost(bindHost)) {
14859
+ for (const o of origins) try {
14860
+ const parsed = new URL(o);
14861
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
14862
+ } catch {}
14863
+ hosts.add(`localhost:${listenPort}`);
14864
+ hosts.add(`127.0.0.1:${listenPort}`);
14865
+ hosts.add(`[::1]:${listenPort}`);
14866
+ }
14867
+ for (const h of readAllowedHostsOverride()) hosts.add(h);
14460
14868
  return hosts;
14461
14869
  }
14462
14870
  function isHostAllowed(headerHost, allowed) {
@@ -14465,11 +14873,16 @@ function isHostAllowed(headerHost, allowed) {
14465
14873
  if (!lower) return false;
14466
14874
  return allowed.has(lower);
14467
14875
  }
14876
+ function requireInboundBearer(authHeader, secret) {
14877
+ if (typeof authHeader !== "string") return false;
14878
+ const match = /^Bearer\s+(\S+)\s*$/i.exec(authHeader);
14879
+ if (!match) return false;
14880
+ return timingSafeCompare(match[1], secret);
14881
+ }
14468
14882
  function corsHeaders(req) {
14469
14883
  const origin = req.headers.origin || "";
14470
- const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
14471
14884
  return {
14472
- "Access-Control-Allow-Origin": allowed,
14885
+ "Access-Control-Allow-Origin": ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0],
14473
14886
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
14474
14887
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
14475
14888
  Vary: "Origin"
@@ -14513,16 +14926,29 @@ function getBoundViewerPort() {
14513
14926
  function getViewerSkipped() {
14514
14927
  return viewerSkipped;
14515
14928
  }
14929
+ var ViewerConfigError = class extends Error {
14930
+ constructor(message) {
14931
+ super(message);
14932
+ this.name = "ViewerConfigError";
14933
+ }
14934
+ };
14516
14935
  function startViewerServer(port, _kv, _sdk, secret, restPort) {
14517
14936
  boundViewerPort = null;
14518
14937
  viewerSkipped = false;
14519
14938
  const resolvedRestPort = restPort ?? port - 2;
14520
14939
  const requestedPort = port;
14940
+ const host = resolveViewerHost();
14941
+ let inboundSecret = null;
14942
+ if (!isLoopbackHost(host)) {
14943
+ if (!secret) throw new ViewerConfigError(`AGENTMEMORY_VIEWER_HOST=${host} requires AGENTMEMORY_SECRET to be set so the viewer can validate inbound bearer tokens. To fix: unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind, or set AGENTMEMORY_SECRET. For Fly images, it is printed on first boot; see deploy/fly/README.md.`);
14944
+ if (readAllowedHostsOverride().length === 0) throw new ViewerConfigError(`AGENTMEMORY_VIEWER_HOST=${host} requires VIEWER_ALLOWED_HOSTS because non-loopback viewer binds only trust explicit Host headers. To fix: set VIEWER_ALLOWED_HOSTS to a comma-separated list of trusted Host header values (e.g. "localhost:3113" for fly proxy), or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind.`);
14945
+ inboundSecret = secret;
14946
+ }
14521
14947
  let allowedHosts = null;
14522
14948
  const server = createServer(async (req, res) => {
14523
14949
  if (!allowedHosts) {
14524
14950
  const addr = server.address();
14525
- allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
14951
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port, host);
14526
14952
  }
14527
14953
  if (!isHostAllowed(req.headers.host, allowedHosts)) {
14528
14954
  res.writeHead(403, { "Content-Type": "text/plain" });
@@ -14558,19 +14984,26 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14558
14984
  return;
14559
14985
  }
14560
14986
  if (method === "GET" && pathname === "/favicon.svg") {
14561
- const favicon = loadViewerFavicon();
14562
- if (favicon) {
14987
+ if (VIEWER_FAVICON) {
14563
14988
  res.writeHead(200, {
14564
14989
  "Content-Type": "image/svg+xml",
14565
14990
  "Cache-Control": "public, max-age=3600"
14566
14991
  });
14567
- res.end(favicon);
14992
+ res.end(VIEWER_FAVICON);
14568
14993
  return;
14569
14994
  }
14570
14995
  res.writeHead(404, { "Content-Type": "text/plain" });
14571
14996
  res.end("favicon not found");
14572
14997
  return;
14573
14998
  }
14999
+ if (inboundSecret !== null && !requireInboundBearer(req.headers.authorization, inboundSecret)) {
15000
+ res.writeHead(401, {
15001
+ "Content-Type": "text/plain",
15002
+ "WWW-Authenticate": "Bearer realm=\"agentmemory-viewer\""
15003
+ });
15004
+ res.end("unauthorized");
15005
+ return;
15006
+ }
14574
15007
  try {
14575
15008
  await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
14576
15009
  } catch (err) {
@@ -14581,17 +15014,23 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14581
15014
  let attempt = 0;
14582
15015
  let currentPort = requestedPort;
14583
15016
  const tryListen = () => {
14584
- server.listen(currentPort, "127.0.0.1");
15017
+ server.listen(currentPort, host);
14585
15018
  };
14586
15019
  server.on("listening", () => {
14587
15020
  const addr = server.address();
14588
- boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
15021
+ const actualPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
15022
+ boundViewerPort = actualPort;
14589
15023
  viewerSkipped = false;
14590
- if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
14591
- else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
15024
+ if (inboundSecret !== null) {
15025
+ const allowedHosts = readAllowedHostsOverride().join(", ");
15026
+ console.log(`[agentmemory] Viewer: http://localhost:${actualPort} (bound to ${host}; inbound Bearer required; allowed Host headers: ${allowedHosts})`);
15027
+ return;
15028
+ }
15029
+ if (actualPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${actualPort}`);
15030
+ else console.log(`[agentmemory] Viewer started on http://localhost:${actualPort} (fallback from ${requestedPort})`);
14592
15031
  });
14593
15032
  server.on("error", (err) => {
14594
- if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
15033
+ if (err.code === "EADDRINUSE" && inboundSecret === null && attempt < MAX_VIEWER_PORT_RETRIES) {
14595
15034
  attempt++;
14596
15035
  currentPort = requestedPort + attempt;
14597
15036
  setImmediate(tryListen);
@@ -14600,7 +15039,8 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14600
15039
  if (err.code === "EADDRINUSE") {
14601
15040
  boundViewerPort = null;
14602
15041
  viewerSkipped = true;
14603
- console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
15042
+ if (inboundSecret !== null) console.warn(`[agentmemory] Viewer port ${requestedPort} is in use while bound to ${host}; not retrying because non-loopback viewer binds require VIEWER_ALLOWED_HOSTS to match the exact port. Free the port, choose another viewer port, or unset AGENTMEMORY_VIEWER_HOST to keep the safe loopback bind.`);
15043
+ else console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
14604
15044
  } else {
14605
15045
  boundViewerPort = null;
14606
15046
  viewerSkipped = true;
@@ -14645,7 +15085,6 @@ async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret)
14645
15085
  res.writeHead(upstream.status, responseHeaders);
14646
15086
  res.end(responseBody);
14647
15087
  }
14648
-
14649
15088
  //#endregion
14650
15089
  //#region src/triggers/api.ts
14651
15090
  function parseOptionalInt(raw) {
@@ -15068,7 +15507,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15068
15507
  }
15069
15508
  if (body.maxFiles !== void 0) {
15070
15509
  const n = body.maxFiles;
15071
- if (!Number.isInteger(n) || n < 1 || n > MAX_FILES_UPPER_BOUND) return {
15510
+ if (!Number.isInteger(n) || n < 1 || n > 1e3) return {
15072
15511
  status_code: 400,
15073
15512
  body: { error: `maxFiles must be an integer between 1 and ${MAX_FILES_UPPER_BOUND}` }
15074
15513
  };
@@ -15152,9 +15591,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15152
15591
  value: "completed"
15153
15592
  }]);
15154
15593
  try {
15155
- sdk.triggerVoid("event::session::stopped", { sessionId });
15594
+ sdk.trigger({
15595
+ function_id: "event::session::stopped",
15596
+ payload: { sessionId },
15597
+ action: TriggerAction.Void()
15598
+ });
15156
15599
  } catch (err) {
15157
- logger.warn("event::session::stopped triggerVoid failed", {
15600
+ logger.warn("event::session::stopped trigger failed", {
15158
15601
  sessionId,
15159
15602
  error: err instanceof Error ? err.message : String(err)
15160
15603
  });
@@ -15574,11 +16017,24 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15574
16017
  status_code: 400,
15575
16018
  body: { error: "query or expandIds is required" }
15576
16019
  };
16020
+ const headers = req.headers || {};
16021
+ const sourceHeader = headers["x-agentmemory-source"] ?? headers["X-Agentmemory-Source"];
16022
+ const sourceFromHeader = Array.isArray(sourceHeader) ? sourceHeader[0] : sourceHeader;
16023
+ const payload = {
16024
+ query: req.body?.query,
16025
+ expandIds: req.body?.expandIds,
16026
+ limit: req.body?.limit,
16027
+ project: req.body?.project,
16028
+ includeLessons: req.body?.includeLessons,
16029
+ agentId: req.body?.agentId,
16030
+ sessionId: req.body?.sessionId,
16031
+ source: req.body?.source ?? sourceFromHeader
16032
+ };
15577
16033
  return {
15578
16034
  status_code: 200,
15579
16035
  body: await sdk.trigger({
15580
16036
  function_id: "mem::smart-search",
15581
- payload: req.body
16037
+ payload
15582
16038
  })
15583
16039
  };
15584
16040
  });
@@ -15590,6 +16046,29 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15590
16046
  http_method: "POST"
15591
16047
  }
15592
16048
  });
16049
+ sdk.registerFunction("api::diagnostic-followup", async (req) => {
16050
+ const authErr = checkAuth(req, secret);
16051
+ if (authErr) return authErr;
16052
+ return {
16053
+ status_code: 200,
16054
+ body: {
16055
+ ...await sdk.trigger({
16056
+ function_id: "mem::diagnostic::followup-stats",
16057
+ payload: {}
16058
+ }),
16059
+ caveat: "Directional signal: overcounts on legitimate query refinement. Tune via AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS."
16060
+ }
16061
+ };
16062
+ });
16063
+ sdk.registerTrigger({
16064
+ type: "http",
16065
+ function_id: "api::diagnostic-followup",
16066
+ config: {
16067
+ api_path: "/agentmemory/diagnostics/followup",
16068
+ http_method: "GET",
16069
+ middleware_function_ids: ["middleware::api-auth"]
16070
+ }
16071
+ });
15593
16072
  sdk.registerFunction("api::timeline", async (req) => {
15594
16073
  const authErr = checkAuth(req, secret);
15595
16074
  if (authErr) return authErr;
@@ -15811,12 +16290,20 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15811
16290
  sdk.registerFunction("api::graph-query", async (req) => {
15812
16291
  const authErr = checkAuth(req, secret);
15813
16292
  if (authErr) return authErr;
16293
+ const payload = {
16294
+ startNodeId: req.body?.startNodeId,
16295
+ nodeType: req.body?.nodeType,
16296
+ maxDepth: req.body?.maxDepth,
16297
+ query: req.body?.query,
16298
+ limit: req.body?.limit,
16299
+ offset: req.body?.offset
16300
+ };
15814
16301
  try {
15815
16302
  return {
15816
16303
  status_code: 200,
15817
16304
  body: await sdk.trigger({
15818
16305
  function_id: "mem::graph-query",
15819
- payload: req.body || {}
16306
+ payload
15820
16307
  })
15821
16308
  };
15822
16309
  } catch {
@@ -18097,7 +18584,6 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
18097
18584
  }
18098
18585
  });
18099
18586
  }
18100
-
18101
18587
  //#endregion
18102
18588
  //#region src/triggers/events.ts
18103
18589
  function registerEventTriggers(sdk, kv) {
@@ -18142,18 +18628,26 @@ function registerEventTriggers(sdk, kv) {
18142
18628
  payload: data
18143
18629
  });
18144
18630
  if (isReflectEnabled()) try {
18145
- sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
18631
+ sdk.trigger({
18632
+ function_id: "mem::slot-reflect",
18633
+ payload: { sessionId: data.sessionId },
18634
+ action: TriggerAction.Void()
18635
+ });
18146
18636
  } catch (err) {
18147
- logger.warn("slot-reflect triggerVoid failed", {
18637
+ logger.warn("slot-reflect trigger failed", {
18148
18638
  sessionId: data.sessionId,
18149
18639
  error: err instanceof Error ? err.message : String(err)
18150
18640
  });
18151
18641
  }
18152
18642
  if (isGraphExtractionEnabled()) try {
18153
18643
  const compressed = (await kv.list(KV.observations(data.sessionId))).filter((o) => o.title);
18154
- if (compressed.length > 0) sdk.triggerVoid("mem::graph-extract", { observations: compressed });
18644
+ if (compressed.length > 0) sdk.trigger({
18645
+ function_id: "mem::graph-extract",
18646
+ payload: { observations: compressed },
18647
+ action: TriggerAction.Void()
18648
+ });
18155
18649
  } catch (err) {
18156
- logger.warn("graph-extract triggerVoid failed", {
18650
+ logger.warn("graph-extract trigger failed", {
18157
18651
  sessionId: data.sessionId,
18158
18652
  error: err instanceof Error ? err.message : String(err)
18159
18653
  });
@@ -18211,7 +18705,6 @@ function registerEventTriggers(sdk, kv) {
18211
18705
  config: { scope: KV.sessions }
18212
18706
  });
18213
18707
  }
18214
-
18215
18708
  //#endregion
18216
18709
  //#region src/mcp/tools-registry.ts
18217
18710
  const CORE_TOOLS = [
@@ -19321,7 +19814,6 @@ function getVisibleTools() {
19321
19814
  if ((process.env["AGENTMEMORY_TOOLS"] || "all") === "core") return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
19322
19815
  return getAllTools();
19323
19816
  }
19324
-
19325
19817
  //#endregion
19326
19818
  //#region src/mcp/server.ts
19327
19819
  function asNonEmptyString(value) {
@@ -20958,7 +21450,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
20958
21450
  }
20959
21451
  });
20960
21452
  }
20961
-
20962
21453
  //#endregion
20963
21454
  //#region src/eval/metrics-store.ts
20964
21455
  var MetricsStore = class {
@@ -21001,7 +21492,6 @@ var MetricsStore = class {
21001
21492
  return Array.from(merged.values());
21002
21493
  }
21003
21494
  };
21004
-
21005
21495
  //#endregion
21006
21496
  //#region src/functions/dedup.ts
21007
21497
  const TTL_MS = 300 * 1e3;
@@ -21043,57 +21533,6 @@ var DedupMap = class {
21043
21533
  return this.entries.size;
21044
21534
  }
21045
21535
  };
21046
-
21047
- //#endregion
21048
- //#region src/telemetry/setup.ts
21049
- const OTEL_CONFIG = {
21050
- serviceName: "agentmemory",
21051
- serviceVersion: VERSION,
21052
- metricsExportIntervalMs: 3e4
21053
- };
21054
- let counters = null;
21055
- let histograms = null;
21056
- const NOOP_COUNTER = { add: () => {} };
21057
- const NOOP_HISTOGRAM = { record: () => {} };
21058
- const COUNTER_NAMES = [
21059
- ["observationsTotal", "observations.total"],
21060
- ["compressionSuccess", "compression.success"],
21061
- ["compressionFailure", "compression.failure"],
21062
- ["searchTotal", "search.total"],
21063
- ["dedupSkipped", "dedup.skipped"],
21064
- ["evictionTotal", "eviction.total"],
21065
- ["circuitBreakerOpen", "circuit_breaker.open"],
21066
- ["embeddingSuccess", "embedding.success"],
21067
- ["embeddingFailure", "embedding.failure"],
21068
- ["vectorSearchTotal", "vector_search.total"],
21069
- ["autoForgetTotal", "auto_forget.total"],
21070
- ["profileGenerated", "profile.generated"],
21071
- ["claudeBridgeSync", "claude_bridge.sync"],
21072
- ["graphExtraction", "graph.extraction"],
21073
- ["consolidationRun", "consolidation.run"],
21074
- ["teamShare", "team.share"],
21075
- ["auditLog", "audit.log"],
21076
- ["snapshotCreate", "snapshot.create"],
21077
- ["governanceDelete", "governance.delete"]
21078
- ];
21079
- const HISTOGRAM_NAMES = [
21080
- ["compressionLatency", "compression.latency_ms"],
21081
- ["searchLatency", "search.latency_ms"],
21082
- ["contextTokens", "context.tokens"],
21083
- ["qualityScore", "quality.score"],
21084
- ["embeddingLatency", "embedding.latency_ms"],
21085
- ["vectorSearchLatency", "vector_search.latency_ms"]
21086
- ];
21087
- function initMetrics(getMeter) {
21088
- const meter = getMeter?.("agentmemory");
21089
- counters = Object.fromEntries(COUNTER_NAMES.map(([key, name]) => [key, meter ? meter.createCounter(name) : NOOP_COUNTER]));
21090
- histograms = Object.fromEntries(HISTOGRAM_NAMES.map(([key, name]) => [key, meter ? meter.createHistogram(name) : NOOP_HISTOGRAM]));
21091
- return {
21092
- counters,
21093
- histograms
21094
- };
21095
- }
21096
-
21097
21536
  //#endregion
21098
21537
  //#region src/index.ts
21099
21538
  function workerPidfilePath() {
@@ -21242,6 +21681,7 @@ async function main() {
21242
21681
  const graphWeight = parseFloat(getEnvVar("AGENTMEMORY_GRAPH_WEIGHT") || "0.3");
21243
21682
  const hybridSearch = new HybridSearch(bm25Index, vectorIndex, embeddingProvider, kv, embeddingConfig.bm25Weight, embeddingConfig.vectorWeight, graphWeight);
21244
21683
  registerSmartSearchFunction(sdk, kv, (query, limit) => hybridSearch.search(query, limit));
21684
+ registerRecentSearchesSweepFunction(sdk, kv);
21245
21685
  registerApiTriggers(sdk, kv, secret, metricsStore, provider);
21246
21686
  registerEventTriggers(sdk, kv);
21247
21687
  registerMcpEndpoints(sdk, kv, secret);
@@ -21309,7 +21749,7 @@ async function main() {
21309
21749
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
21310
21750
  }
21311
21751
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
21312
- bootLog(`REST API: 125 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21752
+ bootLog(`REST API: 126 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21313
21753
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
21314
21754
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
21315
21755
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
@@ -21344,6 +21784,14 @@ async function main() {
21344
21784
  });
21345
21785
  } catch {}
21346
21786
  }, 864e5).unref();
21787
+ setInterval(async () => {
21788
+ try {
21789
+ await sdk.trigger({
21790
+ function_id: "mem::diagnostic::recent-searches-sweep",
21791
+ payload: {}
21792
+ });
21793
+ } catch {}
21794
+ }, 3600 * 1e3).unref();
21347
21795
  if (isConsolidationEnabled()) {
21348
21796
  setInterval(async () => {
21349
21797
  try {
@@ -21375,7 +21823,7 @@ main().catch((err) => {
21375
21823
  console.error(`[agentmemory] Fatal:`, err);
21376
21824
  process.exit(1);
21377
21825
  });
21378
-
21379
21826
  //#endregion
21380
- export { };
21827
+ export {};
21828
+
21381
21829
  //# sourceMappingURL=index.mjs.map