@agentmemory/agentmemory 0.9.24 → 0.9.26

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 +942 -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-u7kAEUC0.mjs} +937 -483
  44. package/dist/src-u7kAEUC0.mjs.map +1 -0
  45. package/dist/{standalone-CPfsVTBA.mjs → standalone-C1yPO519.mjs} +6 -10
  46. package/dist/{standalone-CPfsVTBA.mjs.map → standalone-C1yPO519.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-BMFYFFut.mjs +6 -0
  52. package/dist/version-BMFYFFut.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 && typeof manifest.value === "object") 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.26";
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,9 @@ 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",
7317
+ "0.9.26"
6939
7318
  ]).has(importData.version)) return {
6940
7319
  success: false,
6941
7320
  error: `Unsupported export version: ${importData.version}`
@@ -7253,7 +7632,6 @@ function registerExportImportFunction(sdk, kv) {
7253
7632
  };
7254
7633
  });
7255
7634
  }
7256
-
7257
7635
  //#endregion
7258
7636
  //#region src/functions/enrich.ts
7259
7637
  const MAX_CONTEXT_LENGTH = 4e3;
@@ -7314,7 +7692,6 @@ function registerEnrichFunction(sdk, kv) {
7314
7692
  };
7315
7693
  });
7316
7694
  }
7317
-
7318
7695
  //#endregion
7319
7696
  //#region src/functions/claude-bridge.ts
7320
7697
  function parseMemoryMd(content) {
@@ -7435,7 +7812,6 @@ function registerClaudeBridgeFunction(sdk, kv, config) {
7435
7812
  }
7436
7813
  });
7437
7814
  }
7438
-
7439
7815
  //#endregion
7440
7816
  //#region src/prompts/graph-extraction.ts
7441
7817
  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 +7834,41 @@ Rules:
7458
7834
  function buildGraphExtractionPrompt(observations) {
7459
7835
  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
7836
  }
7461
-
7462
7837
  //#endregion
7463
7838
  //#region src/functions/graph.ts
7839
+ const DEFAULT_GRAPH_QUERY_LIMIT = 500;
7840
+ const MAX_GRAPH_QUERY_LIMIT = 5e3;
7841
+ function resolvePagination(rawLimit, rawOffset) {
7842
+ return {
7843
+ limit: Math.max(1, Math.min(typeof rawLimit === "number" && Number.isFinite(rawLimit) ? Math.floor(rawLimit) : DEFAULT_GRAPH_QUERY_LIMIT, MAX_GRAPH_QUERY_LIMIT)),
7844
+ offset: Math.max(0, typeof rawOffset === "number" && Number.isFinite(rawOffset) ? Math.floor(rawOffset) : 0)
7845
+ };
7846
+ }
7847
+ function rankByDegree(nodes, edges) {
7848
+ const degree = /* @__PURE__ */ new Map();
7849
+ for (const edge of edges) {
7850
+ degree.set(edge.sourceNodeId, (degree.get(edge.sourceNodeId) ?? 0) + 1);
7851
+ degree.set(edge.targetNodeId, (degree.get(edge.targetNodeId) ?? 0) + 1);
7852
+ }
7853
+ return [...nodes].sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0));
7854
+ }
7855
+ function paginate(nodes, allEdges, depth, limit, offset) {
7856
+ const totalNodes = nodes.length;
7857
+ const pageNodes = nodes.slice(offset, offset + limit);
7858
+ const pageNodeIds = new Set(pageNodes.map((n) => n.id));
7859
+ const pageEdges = allEdges.filter((e) => pageNodeIds.has(e.sourceNodeId) && pageNodeIds.has(e.targetNodeId));
7860
+ const universeIds = new Set(nodes.map((n) => n.id));
7861
+ return {
7862
+ nodes: pageNodes,
7863
+ edges: pageEdges,
7864
+ depth,
7865
+ totalNodes,
7866
+ totalEdges: allEdges.reduce((count, e) => universeIds.has(e.sourceNodeId) && universeIds.has(e.targetNodeId) ? count + 1 : count, 0),
7867
+ truncated: totalNodes > pageNodes.length,
7868
+ limit,
7869
+ offset
7870
+ };
7871
+ }
7464
7872
  function parseAttrs(raw) {
7465
7873
  const attrs = {};
7466
7874
  const attrRegex = /([A-Za-z_][\w:-]*)="([^"]*)"/g;
@@ -7597,15 +8005,10 @@ function registerGraphFunction(sdk, kv, provider) {
7597
8005
  const allNodes = (await kv.list(KV.graphNodes)).filter((n) => !n.stale);
7598
8006
  const allEdges = (await kv.list(KV.graphEdges)).filter((e) => !e.stale);
7599
8007
  const maxDepth = Math.min(data.maxDepth || 3, 5);
8008
+ const { limit, offset } = resolvePagination(data.limit, data.offset);
7600
8009
  if (data.query) {
7601
8010
  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
- };
8011
+ 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
8012
  }
7610
8013
  if (data.startNodeId) {
7611
8014
  const visited = /* @__PURE__ */ new Set();
@@ -7637,19 +8040,11 @@ function registerGraphFunction(sdk, kv, provider) {
7637
8040
  });
7638
8041
  }
7639
8042
  }
7640
- return {
7641
- nodes: resultNodes,
7642
- edges: resultEdges,
7643
- depth: maxDepth
7644
- };
8043
+ return paginate(resultNodes, resultEdges, maxDepth, limit, offset);
7645
8044
  }
7646
8045
  let filtered = allNodes;
7647
8046
  if (data.nodeType) filtered = allNodes.filter((n) => n.type === data.nodeType);
7648
- return {
7649
- nodes: filtered,
7650
- edges: allEdges,
7651
- depth: 0
7652
- };
8047
+ return paginate(rankByDegree(filtered, allEdges), allEdges, 0, limit, offset);
7653
8048
  });
7654
8049
  sdk.registerFunction("mem::graph-stats", async () => {
7655
8050
  const nodes = await kv.list(KV.graphNodes);
@@ -7666,7 +8061,6 @@ function registerGraphFunction(sdk, kv, provider) {
7666
8061
  };
7667
8062
  });
7668
8063
  }
7669
-
7670
8064
  //#endregion
7671
8065
  //#region src/prompts/consolidation.ts
7672
8066
  const SEMANTIC_MERGE_SYSTEM = `You are a memory consolidation engine. Given overlapping episodic memories (session summaries), extract stable factual knowledge.
@@ -7701,7 +8095,6 @@ Rules:
7701
8095
  function buildProceduralExtractionPrompt(patterns) {
7702
8096
  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
8097
  }
7704
-
7705
8098
  //#endregion
7706
8099
  //#region src/functions/consolidation-pipeline.ts
7707
8100
  function applyDecay(items, decayDays) {
@@ -7895,7 +8288,6 @@ function registerConsolidationPipelineFunction(sdk, kv, provider) {
7895
8288
  };
7896
8289
  });
7897
8290
  }
7898
-
7899
8291
  //#endregion
7900
8292
  //#region src/functions/team.ts
7901
8293
  const VALID_ITEM_TYPES = new Set([
@@ -7999,7 +8391,6 @@ function registerTeamFunction(sdk, kv, config) {
7999
8391
  return profile;
8000
8392
  });
8001
8393
  }
8002
-
8003
8394
  //#endregion
8004
8395
  //#region src/functions/governance.ts
8005
8396
  function registerGovernanceFunction(sdk, kv) {
@@ -8108,7 +8499,6 @@ function registerGovernanceFunction(sdk, kv) {
8108
8499
  return queryAudit(kv, data);
8109
8500
  });
8110
8501
  }
8111
-
8112
8502
  //#endregion
8113
8503
  //#region src/functions/snapshot.ts
8114
8504
  const COMMIT_HASH_RE = /^[0-9a-f]{7,40}$/i;
@@ -8273,7 +8663,6 @@ function registerSnapshotFunction(sdk, kv, snapshotDir) {
8273
8663
  }
8274
8664
  });
8275
8665
  }
8276
-
8277
8666
  //#endregion
8278
8667
  //#region src/functions/actions.ts
8279
8668
  function registerActionsFunction(sdk, kv) {
@@ -8475,7 +8864,6 @@ async function propagateCompletion(kv, completedActionId) {
8475
8864
  });
8476
8865
  }
8477
8866
  }
8478
-
8479
8867
  //#endregion
8480
8868
  //#region src/functions/frontier.ts
8481
8869
  function registerFrontierFunction(sdk, kv) {
@@ -8580,7 +8968,6 @@ function computeScore(action, edges, now) {
8580
8968
  if (action.status === "active") score += 15;
8581
8969
  return Math.round(score * 100) / 100;
8582
8970
  }
8583
-
8584
8971
  //#endregion
8585
8972
  //#region src/functions/leases.ts
8586
8973
  const DEFAULT_LEASE_TTL_MS = 600 * 1e3;
@@ -8761,7 +9148,6 @@ function registerLeasesFunction(sdk, kv) {
8761
9148
  };
8762
9149
  });
8763
9150
  }
8764
-
8765
9151
  //#endregion
8766
9152
  //#region src/functions/routines.ts
8767
9153
  function registerRoutinesFunction(sdk, kv) {
@@ -9008,7 +9394,6 @@ function registerRoutinesFunction(sdk, kv) {
9008
9394
  });
9009
9395
  });
9010
9396
  }
9011
-
9012
9397
  //#endregion
9013
9398
  //#region src/functions/signals.ts
9014
9399
  function registerSignalsFunction(sdk, kv) {
@@ -9140,7 +9525,6 @@ function registerSignalsFunction(sdk, kv) {
9140
9525
  };
9141
9526
  });
9142
9527
  }
9143
-
9144
9528
  //#endregion
9145
9529
  //#region src/functions/checkpoints.ts
9146
9530
  function registerCheckpointsFunction(sdk, kv) {
@@ -9313,7 +9697,6 @@ function registerCheckpointsFunction(sdk, kv) {
9313
9697
  };
9314
9698
  });
9315
9699
  }
9316
-
9317
9700
  //#endregion
9318
9701
  //#region src/functions/flow-compress.ts
9319
9702
  const FLOW_COMPRESS_SYSTEM = `You are a workflow summarizer. Given a completed action chain, produce a concise summary capturing:
@@ -9458,7 +9841,6 @@ function extractFiles(actions) {
9458
9841
  }
9459
9842
  return Array.from(files);
9460
9843
  }
9461
-
9462
9844
  //#endregion
9463
9845
  //#region src/functions/mesh.ts
9464
9846
  function isPrivateIP(ip) {
@@ -9775,7 +10157,6 @@ async function applySyncData(kv, data, scopes) {
9775
10157
  if (scopes.includes("graph:edges")) applied += await lwwMergeList(kv, KV.graphEdges, data.graphEdges, "mem:gedge", "createdAt");
9776
10158
  return applied;
9777
10159
  }
9778
-
9779
10160
  //#endregion
9780
10161
  //#region src/functions/branch-aware.ts
9781
10162
  function execAsync(cmd, args, cwd) {
@@ -9890,7 +10271,6 @@ function registerBranchAwareFunction(sdk, kv) {
9890
10271
  };
9891
10272
  });
9892
10273
  }
9893
-
9894
10274
  //#endregion
9895
10275
  //#region src/functions/sentinels.ts
9896
10276
  const VALID_TYPES = [
@@ -10211,7 +10591,6 @@ async function unblockLinkedActions(kv, sentinel) {
10211
10591
  });
10212
10592
  return unblockedCount;
10213
10593
  }
10214
-
10215
10594
  //#endregion
10216
10595
  //#region src/functions/sketches.ts
10217
10596
  function registerSketchesFunction(sdk, kv) {
@@ -10453,7 +10832,6 @@ function registerSketchesFunction(sdk, kv) {
10453
10832
  };
10454
10833
  });
10455
10834
  }
10456
-
10457
10835
  //#endregion
10458
10836
  //#region src/functions/crystallize.ts
10459
10837
  const CRYSTALLIZE_SYSTEM = `You are summarizing a completed chain of agent actions into a compact digest.
@@ -10650,7 +11028,6 @@ function parseDigest(response) {
10650
11028
  };
10651
11029
  }
10652
11030
  }
10653
-
10654
11031
  //#endregion
10655
11032
  //#region src/functions/diagnostics.ts
10656
11033
  const ALL_CATEGORIES = [
@@ -11364,7 +11741,6 @@ function registerDiagnosticsFunction(sdk, kv) {
11364
11741
  };
11365
11742
  });
11366
11743
  }
11367
-
11368
11744
  //#endregion
11369
11745
  //#region src/functions/facets.ts
11370
11746
  function registerFacetsFunction(sdk, kv) {
@@ -11536,7 +11912,6 @@ function registerFacetsFunction(sdk, kv) {
11536
11912
  };
11537
11913
  });
11538
11914
  }
11539
-
11540
11915
  //#endregion
11541
11916
  //#region src/functions/verify.ts
11542
11917
  function registerVerifyFunction(sdk, kv) {
@@ -11631,7 +12006,6 @@ async function findObservation(kv, obsId, hintSessionIds) {
11631
12006
  }
11632
12007
  return null;
11633
12008
  }
11634
-
11635
12009
  //#endregion
11636
12010
  //#region src/functions/cascade.ts
11637
12011
  function registerCascadeFunction(sdk, kv) {
@@ -11701,7 +12075,6 @@ function registerCascadeFunction(sdk, kv) {
11701
12075
  };
11702
12076
  });
11703
12077
  }
11704
-
11705
12078
  //#endregion
11706
12079
  //#region src/functions/lessons.ts
11707
12080
  function reinforceLesson(lesson) {
@@ -11887,7 +12260,6 @@ function registerLessonsFunctions(sdk, kv) {
11887
12260
  };
11888
12261
  });
11889
12262
  }
11890
-
11891
12263
  //#endregion
11892
12264
  //#region src/functions/obsidian-export.ts
11893
12265
  const DEFAULT_EXPORT_ROOT = join(homedir(), ".agentmemory");
@@ -11903,6 +12275,20 @@ function resolveVaultDir(vaultDir) {
11903
12275
  function sanitize(name) {
11904
12276
  return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_").slice(0, 100);
11905
12277
  }
12278
+ function hasExportId(item) {
12279
+ return !!item && typeof item.id === "string" && item.id.length > 0;
12280
+ }
12281
+ function safeArray(value) {
12282
+ return Array.isArray(value) ? value : [];
12283
+ }
12284
+ function safeString(value, fallback = "") {
12285
+ return typeof value === "string" ? value : fallback;
12286
+ }
12287
+ function safeTimestamp(value) {
12288
+ if (typeof value !== "string") return 0;
12289
+ const time = new Date(value).getTime();
12290
+ return Number.isFinite(time) ? time : 0;
12291
+ }
11906
12292
  function toFrontmatter(obj) {
11907
12293
  const lines = ["---"];
11908
12294
  for (const [key, value] of Object.entries(obj)) {
@@ -11914,6 +12300,11 @@ function toFrontmatter(obj) {
11914
12300
  return lines.join("\n");
11915
12301
  }
11916
12302
  function memoryToMd(m) {
12303
+ const concepts = safeArray(m.concepts);
12304
+ const files = safeArray(m.files);
12305
+ const relatedIds = safeArray(m.relatedIds);
12306
+ const supersedes = safeArray(m.supersedes);
12307
+ const title = safeString(m.title, m.id);
11917
12308
  const fm = toFrontmatter({
11918
12309
  id: m.id,
11919
12310
  type: m.type,
@@ -11921,24 +12312,28 @@ function memoryToMd(m) {
11921
12312
  updated: m.updatedAt,
11922
12313
  strength: m.strength,
11923
12314
  version: m.version,
11924
- concepts: m.concepts,
11925
- files: m.files
12315
+ concepts,
12316
+ files
11926
12317
  });
11927
- const related = (m.relatedIds || []).map((id) => `- [[${id}]]`).join("\n");
11928
- const supersedes = (m.supersedes || []).map((id) => `- [[${id}]] (superseded)`).join("\n");
12318
+ const relatedLines = relatedIds.map((id) => `- [[${id}]]`).join("\n");
12319
+ const supersedesLines = supersedes.map((id) => `- [[${id}]] (superseded)`).join("\n");
11929
12320
  const sections = [
11930
12321
  fm,
11931
12322
  "",
11932
- `# ${m.title}`,
12323
+ `# ${title}`,
11933
12324
  "",
11934
- m.content
12325
+ safeString(m.content)
11935
12326
  ];
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);
12327
+ if (concepts.length > 0) sections.push("", "## Concepts", concepts.map((c) => `#${c.replace(/\s+/g, "-")}`).join(" "));
12328
+ if (relatedLines) sections.push("", "## Related", relatedLines);
12329
+ if (supersedesLines) sections.push("", "## Supersedes", supersedesLines);
11939
12330
  return sections.join("\n");
11940
12331
  }
11941
12332
  function lessonToMd(l) {
12333
+ const tags = safeArray(l.tags);
12334
+ const sourceIds = safeArray(l.sourceIds);
12335
+ const content = safeString(l.content);
12336
+ const headline = content ? content.slice(0, 80) : l.id;
11942
12337
  const fm = toFrontmatter({
11943
12338
  id: l.id,
11944
12339
  type: "lesson",
@@ -11948,66 +12343,76 @@ function lessonToMd(l) {
11948
12343
  created: l.createdAt,
11949
12344
  updated: l.updatedAt,
11950
12345
  project: l.project,
11951
- tags: l.tags,
12346
+ tags,
11952
12347
  decayRate: l.decayRate
11953
12348
  });
11954
- const sourceLinks = l.sourceIds.map((id) => `- [[${id}]]`).join("\n");
12349
+ const sourceLinks = sourceIds.map((id) => `- [[${id}]]`).join("\n");
11955
12350
  const sections = [
11956
12351
  fm,
11957
12352
  "",
11958
- `# Lesson: ${l.content.slice(0, 80)}`,
12353
+ `# Lesson: ${headline}`,
11959
12354
  "",
11960
- l.content
12355
+ content
11961
12356
  ];
11962
12357
  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(" "));
12358
+ if (tags.length > 0) sections.push("", "## Tags", tags.map((t) => `#${t.replace(/\s+/g, "-")}`).join(" "));
11964
12359
  if (sourceLinks) sections.push("", "## Sources", sourceLinks);
11965
12360
  return sections.join("\n");
11966
12361
  }
11967
12362
  function crystalToMd(c) {
12363
+ const keyOutcomes = safeArray(c.keyOutcomes);
12364
+ const lessons = safeArray(c.lessons);
12365
+ const filesAffected = safeArray(c.filesAffected);
12366
+ const sourceActionIds = safeArray(c.sourceActionIds);
12367
+ const narrative = safeString(c.narrative);
12368
+ const headline = narrative ? narrative.slice(0, 80) : c.id;
11968
12369
  const fm = toFrontmatter({
11969
12370
  id: c.id,
11970
12371
  type: "crystal",
11971
12372
  created: c.createdAt,
11972
12373
  project: c.project,
11973
12374
  sessionId: c.sessionId,
11974
- filesAffected: c.filesAffected
12375
+ filesAffected
11975
12376
  });
11976
- const actionLinks = c.sourceActionIds.map((id) => `- [[${id}]]`).join("\n");
12377
+ const actionLinks = sourceActionIds.map((id) => `- [[${id}]]`).join("\n");
11977
12378
  const sections = [
11978
12379
  fm,
11979
12380
  "",
11980
- `# Crystal: ${c.narrative.slice(0, 80)}`,
12381
+ `# Crystal: ${headline}`,
11981
12382
  "",
11982
- c.narrative,
12383
+ narrative,
11983
12384
  "",
11984
12385
  "## Key Outcomes",
11985
- ...c.keyOutcomes.map((o) => `- ${o}`)
12386
+ ...keyOutcomes.map((o) => `- ${o}`)
11986
12387
  ];
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}\``));
12388
+ if (lessons.length > 0) sections.push("", "## Lessons", ...lessons.map((l) => `- ${l}`));
12389
+ if (filesAffected.length > 0) sections.push("", "## Files", ...filesAffected.map((f) => `- \`${f}\``));
11989
12390
  if (actionLinks) sections.push("", "## Source Actions", actionLinks);
11990
12391
  return sections.join("\n");
11991
12392
  }
11992
12393
  function sessionToMd(s) {
12394
+ const project = safeString(s.project, "unknown");
12395
+ const status = safeString(s.status, "unknown");
12396
+ const startedAt = safeString(s.startedAt, "");
12397
+ const cwd = safeString(s.cwd, "");
11993
12398
  return [
11994
12399
  toFrontmatter({
11995
12400
  id: s.id,
11996
12401
  type: "session",
11997
- project: s.project,
11998
- status: s.status,
11999
- started: s.startedAt,
12402
+ project,
12403
+ status,
12404
+ started: startedAt || void 0,
12000
12405
  ended: s.endedAt,
12001
12406
  observations: s.observationCount
12002
12407
  }),
12003
12408
  "",
12004
- `# Session: ${s.project}`,
12409
+ `# Session: ${project}`,
12005
12410
  "",
12006
- `**Status:** ${s.status}`,
12007
- `**Started:** ${s.startedAt}`,
12411
+ `**Status:** ${status}`,
12412
+ startedAt ? `**Started:** ${startedAt}` : "",
12008
12413
  s.endedAt ? `**Ended:** ${s.endedAt}` : "",
12009
- `**Observations:** ${s.observationCount}`,
12010
- `**CWD:** \`${s.cwd}\``
12414
+ `**Observations:** ${s.observationCount ?? 0}`,
12415
+ cwd ? `**CWD:** \`${cwd}\`` : ""
12011
12416
  ].filter(Boolean).join("\n");
12012
12417
  }
12013
12418
  function registerObsidianExportFunction(sdk, kv) {
@@ -12043,122 +12448,131 @@ function registerObsidianExportFunction(sdk, kv) {
12043
12448
  crystals: join(vaultDir, "crystals"),
12044
12449
  sessions: join(vaultDir, "sessions")
12045
12450
  };
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
- });
12451
+ try {
12452
+ await Promise.all(Object.values(dirs).map((dir) => mkdir(dir, { recursive: true })));
12453
+ const stats = {
12454
+ memories: 0,
12455
+ lessons: 0,
12456
+ crystals: 0,
12457
+ sessions: 0
12458
+ };
12459
+ const errors = [];
12460
+ const memoryMoc = [];
12461
+ const lessonMoc = [];
12462
+ const crystalMoc = [];
12463
+ const sessionMoc = [];
12464
+ const [memories, lessons, crystals, sessions] = await Promise.all([
12465
+ exportTypes.has("memories") ? kv.list(KV.memories) : Promise.resolve([]),
12466
+ exportTypes.has("lessons") ? kv.list(KV.lessons) : Promise.resolve([]),
12467
+ exportTypes.has("crystals") ? kv.list(KV.crystals) : Promise.resolve([]),
12468
+ exportTypes.has("sessions") ? kv.list(KV.sessions) : Promise.resolve([])
12469
+ ]);
12470
+ for (const m of memories.filter((m) => hasExportId(m) && m.isLatest === true)) {
12471
+ const filename = `${sanitize(m.id)}.md`;
12472
+ const filepath = join(dirs.memories, filename);
12473
+ try {
12474
+ await writeFile(filepath, memoryToMd(m));
12475
+ stats.memories++;
12476
+ memoryMoc.push(`- [[memories/${sanitize(m.id)}|${safeString(m.title, m.id)}]] (${m.type}, strength: ${m.strength ?? 0})`);
12477
+ } catch (err) {
12478
+ errors.push({
12479
+ id: m.id,
12480
+ path: filepath,
12481
+ error: err instanceof Error ? err.message : String(err)
12482
+ });
12483
+ }
12077
12484
  }
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
- });
12485
+ for (const l of lessons.filter((l) => hasExportId(l) && !l.deleted)) {
12486
+ const filename = `${sanitize(l.id)}.md`;
12487
+ const filepath = join(dirs.lessons, filename);
12488
+ try {
12489
+ await writeFile(filepath, lessonToMd(l));
12490
+ stats.lessons++;
12491
+ const headline = safeString(l.content).slice(0, 60) || l.id;
12492
+ lessonMoc.push(`- [[lessons/${sanitize(l.id)}|${headline}]] (confidence: ${l.confidence ?? 0})`);
12493
+ } catch (err) {
12494
+ errors.push({
12495
+ id: l.id,
12496
+ path: filepath,
12497
+ error: err instanceof Error ? err.message : String(err)
12498
+ });
12499
+ }
12092
12500
  }
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
- });
12501
+ for (const c of crystals.filter(hasExportId)) {
12502
+ const filename = `${sanitize(c.id)}.md`;
12503
+ const filepath = join(dirs.crystals, filename);
12504
+ try {
12505
+ await writeFile(filepath, crystalToMd(c));
12506
+ stats.crystals++;
12507
+ const headline = safeString(c.narrative).slice(0, 60) || c.id;
12508
+ crystalMoc.push(`- [[crystals/${sanitize(c.id)}|${headline}]]`);
12509
+ } catch (err) {
12510
+ errors.push({
12511
+ id: c.id,
12512
+ path: filepath,
12513
+ error: err instanceof Error ? err.message : String(err)
12514
+ });
12515
+ }
12107
12516
  }
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
- });
12517
+ const recent = sessions.filter(hasExportId).sort((a, b) => safeTimestamp(b.startedAt) - safeTimestamp(a.startedAt)).slice(0, 50);
12518
+ for (const s of recent) {
12519
+ const filename = `${sanitize(s.id)}.md`;
12520
+ const filepath = join(dirs.sessions, filename);
12521
+ try {
12522
+ await writeFile(filepath, sessionToMd(s));
12523
+ stats.sessions++;
12524
+ sessionMoc.push(`- [[sessions/${sanitize(s.id)}|${safeString(s.project, "unknown")} (${safeString(s.status, "unknown")})]]`);
12525
+ } catch (err) {
12526
+ errors.push({
12527
+ id: s.id,
12528
+ path: filepath,
12529
+ error: err instanceof Error ? err.message : String(err)
12530
+ });
12531
+ }
12123
12532
  }
12533
+ const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
12534
+ const moc = [
12535
+ "---",
12536
+ "type: moc",
12537
+ `exported: ${exportedAt}`,
12538
+ "---",
12539
+ "",
12540
+ "# agentmemory vault",
12541
+ "",
12542
+ `Exported: ${exportedAt}`,
12543
+ "",
12544
+ `## Memories (${stats.memories})`,
12545
+ ...memoryMoc,
12546
+ "",
12547
+ `## Lessons (${stats.lessons})`,
12548
+ ...lessonMoc,
12549
+ "",
12550
+ `## Crystals (${stats.crystals})`,
12551
+ ...crystalMoc,
12552
+ "",
12553
+ `## Sessions (${stats.sessions})`,
12554
+ ...sessionMoc
12555
+ ].join("\n");
12556
+ await writeFile(join(vaultDir, "MOC.md"), moc);
12557
+ await recordAudit(kv, "obsidian_export", "mem::obsidian-export", [], {
12558
+ vaultDir,
12559
+ stats
12560
+ });
12561
+ return {
12562
+ success: true,
12563
+ exported: stats,
12564
+ errors: errors.length > 0 ? errors : void 0,
12565
+ vaultDir
12566
+ };
12567
+ } catch (err) {
12568
+ return {
12569
+ success: false,
12570
+ error: err instanceof Error ? err.message : String(err),
12571
+ vaultDir
12572
+ };
12124
12573
  }
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
12574
  });
12160
12575
  }
12161
-
12162
12576
  //#endregion
12163
12577
  //#region src/prompts/reflect.ts
12164
12578
  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 +12600,6 @@ function buildReflectPrompt(cluster) {
12186
12600
  if (cluster.crystalNarratives.length > 0) sections.push("\n## Completed Work Summaries", ...cluster.crystalNarratives.map((n) => `- ${n}`));
12187
12601
  return `Synthesize higher-order insights from this cluster of related memories:\n\n${sections.join("\n")}`;
12188
12602
  }
12189
-
12190
12603
  //#endregion
12191
12604
  //#region src/functions/reflect.ts
12192
12605
  function reinforceInsight(insight) {
@@ -12479,7 +12892,6 @@ function registerReflectFunctions(sdk, kv, provider) {
12479
12892
  };
12480
12893
  });
12481
12894
  }
12482
-
12483
12895
  //#endregion
12484
12896
  //#region src/functions/working-memory.ts
12485
12897
  const CORE_SCOPE = "mem:core-memory";
@@ -12654,7 +13066,6 @@ function registerWorkingMemoryFunctions(sdk, kv, tokenBudget) {
12654
13066
  };
12655
13067
  });
12656
13068
  }
12657
-
12658
13069
  //#endregion
12659
13070
  //#region src/functions/skill-extract.ts
12660
13071
  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 +13268,6 @@ function registerSkillExtractFunctions(sdk, kv, provider) {
12857
13268
  };
12858
13269
  });
12859
13270
  }
12860
-
12861
13271
  //#endregion
12862
13272
  //#region src/functions/sliding-window.ts
12863
13273
  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 +13454,6 @@ function registerSlidingWindowFunction(sdk, kv, provider) {
13044
13454
  };
13045
13455
  });
13046
13456
  }
13047
-
13048
13457
  //#endregion
13049
13458
  //#region src/functions/temporal-graph.ts
13050
13459
  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 +13717,6 @@ function buildTimeline(edges) {
13308
13717
  context: e.context
13309
13718
  }));
13310
13719
  }
13311
-
13312
13720
  //#endregion
13313
13721
  //#region src/functions/retention.ts
13314
13722
  const DEFAULT_DECAY = {
@@ -13542,7 +13950,6 @@ function registerRetentionFunctions(sdk, kv) {
13542
13950
  };
13543
13951
  });
13544
13952
  }
13545
-
13546
13953
  //#endregion
13547
13954
  //#region src/functions/compress-file.ts
13548
13955
  const SENSITIVE_PATH_TERMS = [
@@ -13684,7 +14091,6 @@ function registerCompressFileFunction(sdk, kv, provider) {
13684
14091
  };
13685
14092
  });
13686
14093
  }
13687
-
13688
14094
  //#endregion
13689
14095
  //#region src/replay/jsonl-parser.ts
13690
14096
  function deriveProject(cwd) {
@@ -13811,7 +14217,6 @@ function parseJsonlText(text, fallbackSessionId) {
13811
14217
  observations
13812
14218
  };
13813
14219
  }
13814
-
13815
14220
  //#endregion
13816
14221
  //#region src/replay/timeline.ts
13817
14222
  const DEFAULT_CHARS_PER_SEC = 40;
@@ -13906,10 +14311,6 @@ function projectTimeline(observations) {
13906
14311
  events
13907
14312
  };
13908
14313
  }
13909
-
13910
- //#endregion
13911
- //#region src/functions/replay.ts
13912
- const MAX_FILES_DEFAULT = 200;
13913
14314
  const MAX_FILES_UPPER_BOUND = 1e3;
13914
14315
  const SENSITIVE_PATH_PATTERNS = [
13915
14316
  /(^|[\\/_.-])secret([\\/_.-]|s?$)/i,
@@ -14129,7 +14530,7 @@ function registerReplayFunctions(sdk, kv) {
14129
14530
  error: "path not found"
14130
14531
  };
14131
14532
  }
14132
- const maxFiles = Number.isInteger(data.maxFiles) && data.maxFiles > 0 ? Math.min(data.maxFiles, MAX_FILES_UPPER_BOUND) : MAX_FILES_DEFAULT;
14533
+ const maxFiles = Number.isInteger(data.maxFiles) && data.maxFiles > 0 ? Math.min(data.maxFiles, MAX_FILES_UPPER_BOUND) : 200;
14133
14534
  let files = [];
14134
14535
  let truncated = false;
14135
14536
  let discovered = 0;
@@ -14185,7 +14586,8 @@ function registerReplayFunctions(sdk, kv) {
14185
14586
  const existingTags = existing.tags || [];
14186
14587
  if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
14187
14588
  if (!existing.firstPrompt && firstPrompt) existing.firstPrompt = firstPrompt;
14188
- await kv.set(KV.sessions, existing.id, existing);
14589
+ if (!existing.id) existing.id = parsed.sessionId;
14590
+ await kv.set(KV.sessions, parsed.sessionId, existing);
14189
14591
  } else {
14190
14592
  const session = {
14191
14593
  id: parsed.sessionId,
@@ -14231,7 +14633,6 @@ function registerReplayFunctions(sdk, kv) {
14231
14633
  };
14232
14634
  });
14233
14635
  }
14234
-
14235
14636
  //#endregion
14236
14637
  //#region src/health/thresholds.ts
14237
14638
  const DEFAULTS = {
@@ -14290,7 +14691,6 @@ function evaluateHealth(snapshot, config = {}) {
14290
14691
  notes
14291
14692
  };
14292
14693
  }
14293
-
14294
14694
  //#endregion
14295
14695
  //#region src/health/monitor.ts
14296
14696
  function registerHealthMonitor(sdk, kv) {
@@ -14378,7 +14778,6 @@ function registerHealthMonitor(sdk, kv) {
14378
14778
  async function getLatestHealth(kv) {
14379
14779
  return kv.get(KV.health, "latest");
14380
14780
  }
14381
-
14382
14781
  //#endregion
14383
14782
  //#region src/auth.ts
14384
14783
  const hmacKey = randomBytes(32);
@@ -14404,7 +14803,6 @@ function buildViewerCsp(nonce) {
14404
14803
  "font-src 'self'"
14405
14804
  ].join("; ");
14406
14805
  }
14407
-
14408
14806
  //#endregion
14409
14807
  //#region src/viewer/document.ts
14410
14808
  const VIEWER_VERSION_PLACEHOLDER = "__AGENTMEMORY_VERSION__";
@@ -14430,7 +14828,6 @@ function renderViewerDocument() {
14430
14828
  csp: buildViewerCsp(nonce)
14431
14829
  };
14432
14830
  }
14433
-
14434
14831
  //#endregion
14435
14832
  //#region src/viewer/server.ts
14436
14833
  function loadViewerFavicon() {
@@ -14445,18 +14842,30 @@ function loadViewerFavicon() {
14445
14842
  } catch {}
14446
14843
  return null;
14447
14844
  }
14845
+ const VIEWER_FAVICON = loadViewerFavicon();
14448
14846
  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) {
14847
+ function readAllowedHostsOverride() {
14848
+ return (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
14849
+ }
14850
+ function resolveViewerHost() {
14851
+ return process.env.AGENTMEMORY_VIEWER_HOST?.trim() || "127.0.0.1";
14852
+ }
14853
+ function isLoopbackHost(host) {
14854
+ const h = host.trim().toLowerCase();
14855
+ return h === "127.0.0.1" || h === "::1" || h === "localhost";
14856
+ }
14857
+ function buildAllowedHosts(origins, listenPort, bindHost = "127.0.0.1") {
14451
14858
  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);
14859
+ if (isLoopbackHost(bindHost)) {
14860
+ for (const o of origins) try {
14861
+ const parsed = new URL(o);
14862
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
14863
+ } catch {}
14864
+ hosts.add(`localhost:${listenPort}`);
14865
+ hosts.add(`127.0.0.1:${listenPort}`);
14866
+ hosts.add(`[::1]:${listenPort}`);
14867
+ }
14868
+ for (const h of readAllowedHostsOverride()) hosts.add(h);
14460
14869
  return hosts;
14461
14870
  }
14462
14871
  function isHostAllowed(headerHost, allowed) {
@@ -14465,11 +14874,16 @@ function isHostAllowed(headerHost, allowed) {
14465
14874
  if (!lower) return false;
14466
14875
  return allowed.has(lower);
14467
14876
  }
14877
+ function requireInboundBearer(authHeader, secret) {
14878
+ if (typeof authHeader !== "string") return false;
14879
+ const match = /^Bearer\s+(\S+)\s*$/i.exec(authHeader);
14880
+ if (!match) return false;
14881
+ return timingSafeCompare(match[1], secret);
14882
+ }
14468
14883
  function corsHeaders(req) {
14469
14884
  const origin = req.headers.origin || "";
14470
- const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
14471
14885
  return {
14472
- "Access-Control-Allow-Origin": allowed,
14886
+ "Access-Control-Allow-Origin": ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0],
14473
14887
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
14474
14888
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
14475
14889
  Vary: "Origin"
@@ -14513,16 +14927,29 @@ function getBoundViewerPort() {
14513
14927
  function getViewerSkipped() {
14514
14928
  return viewerSkipped;
14515
14929
  }
14930
+ var ViewerConfigError = class extends Error {
14931
+ constructor(message) {
14932
+ super(message);
14933
+ this.name = "ViewerConfigError";
14934
+ }
14935
+ };
14516
14936
  function startViewerServer(port, _kv, _sdk, secret, restPort) {
14517
14937
  boundViewerPort = null;
14518
14938
  viewerSkipped = false;
14519
14939
  const resolvedRestPort = restPort ?? port - 2;
14520
14940
  const requestedPort = port;
14941
+ const host = resolveViewerHost();
14942
+ let inboundSecret = null;
14943
+ if (!isLoopbackHost(host)) {
14944
+ 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.`);
14945
+ 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.`);
14946
+ inboundSecret = secret;
14947
+ }
14521
14948
  let allowedHosts = null;
14522
14949
  const server = createServer(async (req, res) => {
14523
14950
  if (!allowedHosts) {
14524
14951
  const addr = server.address();
14525
- allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
14952
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port, host);
14526
14953
  }
14527
14954
  if (!isHostAllowed(req.headers.host, allowedHosts)) {
14528
14955
  res.writeHead(403, { "Content-Type": "text/plain" });
@@ -14558,19 +14985,26 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14558
14985
  return;
14559
14986
  }
14560
14987
  if (method === "GET" && pathname === "/favicon.svg") {
14561
- const favicon = loadViewerFavicon();
14562
- if (favicon) {
14988
+ if (VIEWER_FAVICON) {
14563
14989
  res.writeHead(200, {
14564
14990
  "Content-Type": "image/svg+xml",
14565
14991
  "Cache-Control": "public, max-age=3600"
14566
14992
  });
14567
- res.end(favicon);
14993
+ res.end(VIEWER_FAVICON);
14568
14994
  return;
14569
14995
  }
14570
14996
  res.writeHead(404, { "Content-Type": "text/plain" });
14571
14997
  res.end("favicon not found");
14572
14998
  return;
14573
14999
  }
15000
+ if (inboundSecret !== null && !requireInboundBearer(req.headers.authorization, inboundSecret)) {
15001
+ res.writeHead(401, {
15002
+ "Content-Type": "text/plain",
15003
+ "WWW-Authenticate": "Bearer realm=\"agentmemory-viewer\""
15004
+ });
15005
+ res.end("unauthorized");
15006
+ return;
15007
+ }
14574
15008
  try {
14575
15009
  await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
14576
15010
  } catch (err) {
@@ -14581,17 +15015,23 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14581
15015
  let attempt = 0;
14582
15016
  let currentPort = requestedPort;
14583
15017
  const tryListen = () => {
14584
- server.listen(currentPort, "127.0.0.1");
15018
+ server.listen(currentPort, host);
14585
15019
  };
14586
15020
  server.on("listening", () => {
14587
15021
  const addr = server.address();
14588
- boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
15022
+ const actualPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
15023
+ boundViewerPort = actualPort;
14589
15024
  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})`);
15025
+ if (inboundSecret !== null) {
15026
+ const allowedHosts = readAllowedHostsOverride().join(", ");
15027
+ console.log(`[agentmemory] Viewer: http://localhost:${actualPort} (bound to ${host}; inbound Bearer required; allowed Host headers: ${allowedHosts})`);
15028
+ return;
15029
+ }
15030
+ if (actualPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${actualPort}`);
15031
+ else console.log(`[agentmemory] Viewer started on http://localhost:${actualPort} (fallback from ${requestedPort})`);
14592
15032
  });
14593
15033
  server.on("error", (err) => {
14594
- if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
15034
+ if (err.code === "EADDRINUSE" && inboundSecret === null && attempt < MAX_VIEWER_PORT_RETRIES) {
14595
15035
  attempt++;
14596
15036
  currentPort = requestedPort + attempt;
14597
15037
  setImmediate(tryListen);
@@ -14600,7 +15040,8 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14600
15040
  if (err.code === "EADDRINUSE") {
14601
15041
  boundViewerPort = null;
14602
15042
  viewerSkipped = true;
14603
- console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
15043
+ 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.`);
15044
+ else console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
14604
15045
  } else {
14605
15046
  boundViewerPort = null;
14606
15047
  viewerSkipped = true;
@@ -14645,7 +15086,6 @@ async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret)
14645
15086
  res.writeHead(upstream.status, responseHeaders);
14646
15087
  res.end(responseBody);
14647
15088
  }
14648
-
14649
15089
  //#endregion
14650
15090
  //#region src/triggers/api.ts
14651
15091
  function parseOptionalInt(raw) {
@@ -15068,7 +15508,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15068
15508
  }
15069
15509
  if (body.maxFiles !== void 0) {
15070
15510
  const n = body.maxFiles;
15071
- if (!Number.isInteger(n) || n < 1 || n > MAX_FILES_UPPER_BOUND) return {
15511
+ if (!Number.isInteger(n) || n < 1 || n > 1e3) return {
15072
15512
  status_code: 400,
15073
15513
  body: { error: `maxFiles must be an integer between 1 and ${MAX_FILES_UPPER_BOUND}` }
15074
15514
  };
@@ -15152,9 +15592,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15152
15592
  value: "completed"
15153
15593
  }]);
15154
15594
  try {
15155
- sdk.triggerVoid("event::session::stopped", { sessionId });
15595
+ sdk.trigger({
15596
+ function_id: "event::session::stopped",
15597
+ payload: { sessionId },
15598
+ action: TriggerAction.Void()
15599
+ });
15156
15600
  } catch (err) {
15157
- logger.warn("event::session::stopped triggerVoid failed", {
15601
+ logger.warn("event::session::stopped trigger failed", {
15158
15602
  sessionId,
15159
15603
  error: err instanceof Error ? err.message : String(err)
15160
15604
  });
@@ -15574,11 +16018,24 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15574
16018
  status_code: 400,
15575
16019
  body: { error: "query or expandIds is required" }
15576
16020
  };
16021
+ const headers = req.headers || {};
16022
+ const sourceHeader = headers["x-agentmemory-source"] ?? headers["X-Agentmemory-Source"];
16023
+ const sourceFromHeader = Array.isArray(sourceHeader) ? sourceHeader[0] : sourceHeader;
16024
+ const payload = {
16025
+ query: req.body?.query,
16026
+ expandIds: req.body?.expandIds,
16027
+ limit: req.body?.limit,
16028
+ project: req.body?.project,
16029
+ includeLessons: req.body?.includeLessons,
16030
+ agentId: req.body?.agentId,
16031
+ sessionId: req.body?.sessionId,
16032
+ source: req.body?.source ?? sourceFromHeader
16033
+ };
15577
16034
  return {
15578
16035
  status_code: 200,
15579
16036
  body: await sdk.trigger({
15580
16037
  function_id: "mem::smart-search",
15581
- payload: req.body
16038
+ payload
15582
16039
  })
15583
16040
  };
15584
16041
  });
@@ -15590,6 +16047,29 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15590
16047
  http_method: "POST"
15591
16048
  }
15592
16049
  });
16050
+ sdk.registerFunction("api::diagnostic-followup", async (req) => {
16051
+ const authErr = checkAuth(req, secret);
16052
+ if (authErr) return authErr;
16053
+ return {
16054
+ status_code: 200,
16055
+ body: {
16056
+ ...await sdk.trigger({
16057
+ function_id: "mem::diagnostic::followup-stats",
16058
+ payload: {}
16059
+ }),
16060
+ caveat: "Directional signal: overcounts on legitimate query refinement. Tune via AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS."
16061
+ }
16062
+ };
16063
+ });
16064
+ sdk.registerTrigger({
16065
+ type: "http",
16066
+ function_id: "api::diagnostic-followup",
16067
+ config: {
16068
+ api_path: "/agentmemory/diagnostics/followup",
16069
+ http_method: "GET",
16070
+ middleware_function_ids: ["middleware::api-auth"]
16071
+ }
16072
+ });
15593
16073
  sdk.registerFunction("api::timeline", async (req) => {
15594
16074
  const authErr = checkAuth(req, secret);
15595
16075
  if (authErr) return authErr;
@@ -15811,12 +16291,20 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15811
16291
  sdk.registerFunction("api::graph-query", async (req) => {
15812
16292
  const authErr = checkAuth(req, secret);
15813
16293
  if (authErr) return authErr;
16294
+ const payload = {
16295
+ startNodeId: req.body?.startNodeId,
16296
+ nodeType: req.body?.nodeType,
16297
+ maxDepth: req.body?.maxDepth,
16298
+ query: req.body?.query,
16299
+ limit: req.body?.limit,
16300
+ offset: req.body?.offset
16301
+ };
15814
16302
  try {
15815
16303
  return {
15816
16304
  status_code: 200,
15817
16305
  body: await sdk.trigger({
15818
16306
  function_id: "mem::graph-query",
15819
- payload: req.body || {}
16307
+ payload
15820
16308
  })
15821
16309
  };
15822
16310
  } catch {
@@ -18097,7 +18585,6 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
18097
18585
  }
18098
18586
  });
18099
18587
  }
18100
-
18101
18588
  //#endregion
18102
18589
  //#region src/triggers/events.ts
18103
18590
  function registerEventTriggers(sdk, kv) {
@@ -18142,18 +18629,26 @@ function registerEventTriggers(sdk, kv) {
18142
18629
  payload: data
18143
18630
  });
18144
18631
  if (isReflectEnabled()) try {
18145
- sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
18632
+ sdk.trigger({
18633
+ function_id: "mem::slot-reflect",
18634
+ payload: { sessionId: data.sessionId },
18635
+ action: TriggerAction.Void()
18636
+ });
18146
18637
  } catch (err) {
18147
- logger.warn("slot-reflect triggerVoid failed", {
18638
+ logger.warn("slot-reflect trigger failed", {
18148
18639
  sessionId: data.sessionId,
18149
18640
  error: err instanceof Error ? err.message : String(err)
18150
18641
  });
18151
18642
  }
18152
18643
  if (isGraphExtractionEnabled()) try {
18153
18644
  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 });
18645
+ if (compressed.length > 0) sdk.trigger({
18646
+ function_id: "mem::graph-extract",
18647
+ payload: { observations: compressed },
18648
+ action: TriggerAction.Void()
18649
+ });
18155
18650
  } catch (err) {
18156
- logger.warn("graph-extract triggerVoid failed", {
18651
+ logger.warn("graph-extract trigger failed", {
18157
18652
  sessionId: data.sessionId,
18158
18653
  error: err instanceof Error ? err.message : String(err)
18159
18654
  });
@@ -18211,7 +18706,6 @@ function registerEventTriggers(sdk, kv) {
18211
18706
  config: { scope: KV.sessions }
18212
18707
  });
18213
18708
  }
18214
-
18215
18709
  //#endregion
18216
18710
  //#region src/mcp/tools-registry.ts
18217
18711
  const CORE_TOOLS = [
@@ -19321,7 +19815,6 @@ function getVisibleTools() {
19321
19815
  if ((process.env["AGENTMEMORY_TOOLS"] || "all") === "core") return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
19322
19816
  return getAllTools();
19323
19817
  }
19324
-
19325
19818
  //#endregion
19326
19819
  //#region src/mcp/server.ts
19327
19820
  function asNonEmptyString(value) {
@@ -20958,7 +21451,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
20958
21451
  }
20959
21452
  });
20960
21453
  }
20961
-
20962
21454
  //#endregion
20963
21455
  //#region src/eval/metrics-store.ts
20964
21456
  var MetricsStore = class {
@@ -21001,7 +21493,6 @@ var MetricsStore = class {
21001
21493
  return Array.from(merged.values());
21002
21494
  }
21003
21495
  };
21004
-
21005
21496
  //#endregion
21006
21497
  //#region src/functions/dedup.ts
21007
21498
  const TTL_MS = 300 * 1e3;
@@ -21043,57 +21534,6 @@ var DedupMap = class {
21043
21534
  return this.entries.size;
21044
21535
  }
21045
21536
  };
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
21537
  //#endregion
21098
21538
  //#region src/index.ts
21099
21539
  function workerPidfilePath() {
@@ -21242,6 +21682,7 @@ async function main() {
21242
21682
  const graphWeight = parseFloat(getEnvVar("AGENTMEMORY_GRAPH_WEIGHT") || "0.3");
21243
21683
  const hybridSearch = new HybridSearch(bm25Index, vectorIndex, embeddingProvider, kv, embeddingConfig.bm25Weight, embeddingConfig.vectorWeight, graphWeight);
21244
21684
  registerSmartSearchFunction(sdk, kv, (query, limit) => hybridSearch.search(query, limit));
21685
+ registerRecentSearchesSweepFunction(sdk, kv);
21245
21686
  registerApiTriggers(sdk, kv, secret, metricsStore, provider);
21246
21687
  registerEventTriggers(sdk, kv);
21247
21688
  registerMcpEndpoints(sdk, kv, secret);
@@ -21309,7 +21750,7 @@ async function main() {
21309
21750
  console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
21310
21751
  }
21311
21752
  bootLog(`Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
21312
- bootLog(`REST API: 125 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21753
+ bootLog(`REST API: 126 endpoints at http://localhost:${config.restPort}/agentmemory/*`);
21313
21754
  bootLog(`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`);
21314
21755
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
21315
21756
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
@@ -21344,6 +21785,14 @@ async function main() {
21344
21785
  });
21345
21786
  } catch {}
21346
21787
  }, 864e5).unref();
21788
+ setInterval(async () => {
21789
+ try {
21790
+ await sdk.trigger({
21791
+ function_id: "mem::diagnostic::recent-searches-sweep",
21792
+ payload: {}
21793
+ });
21794
+ } catch {}
21795
+ }, 3600 * 1e3).unref();
21347
21796
  if (isConsolidationEnabled()) {
21348
21797
  setInterval(async () => {
21349
21798
  try {
@@ -21375,7 +21824,7 @@ main().catch((err) => {
21375
21824
  console.error(`[agentmemory] Fatal:`, err);
21376
21825
  process.exit(1);
21377
21826
  });
21378
-
21379
21827
  //#endregion
21380
- export { };
21828
+ export {};
21829
+
21381
21830
  //# sourceMappingURL=index.mjs.map