@agentmemory/agentmemory 0.9.3 → 0.9.5

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.
package/dist/index.mjs CHANGED
@@ -50,7 +50,14 @@ function loadEnvFile() {
50
50
  if (eqIdx === -1) continue;
51
51
  const key = trimmed.slice(0, eqIdx).trim();
52
52
  let val = trimmed.slice(eqIdx + 1).trim();
53
- if (val.startsWith("\"") && val.endsWith("\"") || val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
53
+ const quoteChar = val[0] === "\"" || val[0] === "'" ? val[0] : "";
54
+ if (quoteChar) {
55
+ const closeIdx = val.indexOf(quoteChar, 1);
56
+ if (closeIdx !== -1) val = val.slice(1, closeIdx);
57
+ } else {
58
+ const hashIdx = val.indexOf(" #");
59
+ if (hashIdx !== -1) val = val.slice(0, hashIdx).trim();
60
+ }
54
61
  vars[key] = val;
55
62
  }
56
63
  return vars;
@@ -123,6 +130,11 @@ function getMergedEnv(overrides) {
123
130
  function getEnvVar(key) {
124
131
  return getMergedEnv()[key];
125
132
  }
133
+ function detectLlmProviderKind() {
134
+ const env = getMergedEnv();
135
+ if (hasRealValue(env["ANTHROPIC_API_KEY"]) || hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"]) || hasRealValue(env["OPENROUTER_API_KEY"]) || hasRealValue(env["MINIMAX_API_KEY"])) return "llm";
136
+ return "noop";
137
+ }
126
138
  function loadEmbeddingConfig() {
127
139
  const env = getMergedEnv();
128
140
  let bm25Weight = parseFloat(env["BM25_WEIGHT"] || "0.4");
@@ -883,22 +895,38 @@ let imageEmbeddingProvider = null;
883
895
  function createImageEmbeddingProvider() {
884
896
  if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] !== "true") return null;
885
897
  if (imageEmbeddingProvider) return imageEmbeddingProvider;
886
- imageEmbeddingProvider = new ClipEmbeddingProvider();
898
+ imageEmbeddingProvider = withDimensionGuard(new ClipEmbeddingProvider());
887
899
  return imageEmbeddingProvider;
888
900
  }
889
901
  function createEmbeddingProvider() {
890
902
  const detected = detectEmbeddingProvider();
891
903
  if (!detected) return null;
892
904
  switch (detected) {
893
- case "gemini": return new GeminiEmbeddingProvider(getEnvVar("GEMINI_API_KEY"));
894
- case "openai": return new OpenAIEmbeddingProvider(getEnvVar("OPENAI_API_KEY"));
895
- case "voyage": return new VoyageEmbeddingProvider(getEnvVar("VOYAGE_API_KEY"));
896
- case "cohere": return new CohereEmbeddingProvider(getEnvVar("COHERE_API_KEY"));
897
- case "openrouter": return new OpenRouterEmbeddingProvider(getEnvVar("OPENROUTER_API_KEY"));
898
- case "local": return new LocalEmbeddingProvider();
905
+ case "gemini": return withDimensionGuard(new GeminiEmbeddingProvider(getEnvVar("GEMINI_API_KEY")));
906
+ case "openai": return withDimensionGuard(new OpenAIEmbeddingProvider(getEnvVar("OPENAI_API_KEY")));
907
+ case "voyage": return withDimensionGuard(new VoyageEmbeddingProvider(getEnvVar("VOYAGE_API_KEY")));
908
+ case "cohere": return withDimensionGuard(new CohereEmbeddingProvider(getEnvVar("COHERE_API_KEY")));
909
+ case "openrouter": return withDimensionGuard(new OpenRouterEmbeddingProvider(getEnvVar("OPENROUTER_API_KEY")));
910
+ case "local": return withDimensionGuard(new LocalEmbeddingProvider());
899
911
  default: return null;
900
912
  }
901
913
  }
914
+ function withDimensionGuard(provider) {
915
+ const expected = provider.dimensions;
916
+ const check = (v, where) => {
917
+ if (v.length !== expected) throw new Error(`Embedding dimension mismatch in ${provider.name}.${where}: expected ${expected}, got ${v.length}`);
918
+ return v;
919
+ };
920
+ const wrapped = Object.create(provider);
921
+ wrapped.embed = async (t) => check(await provider.embed(t), "embed");
922
+ wrapped.embedBatch = async (ts) => {
923
+ const out = await provider.embedBatch(ts);
924
+ out.forEach((v, i) => check(v, `embedBatch[${i}]`));
925
+ return out;
926
+ };
927
+ if (provider.embedImage) wrapped.embedImage = async (s) => check(await provider.embedImage(s), "embedImage");
928
+ return wrapped;
929
+ }
902
930
 
903
931
  //#endregion
904
932
  //#region src/providers/index.ts
@@ -994,6 +1022,75 @@ var StateKV = class {
994
1022
  }
995
1023
  };
996
1024
 
1025
+ //#endregion
1026
+ //#region src/state/schema.ts
1027
+ const KV = {
1028
+ sessions: "mem:sessions",
1029
+ observations: (sessionId) => `mem:obs:${sessionId}`,
1030
+ memories: "mem:memories",
1031
+ summaries: "mem:summaries",
1032
+ config: "mem:config",
1033
+ metrics: "mem:metrics",
1034
+ health: "mem:health",
1035
+ embeddings: (obsId) => `mem:emb:${obsId}`,
1036
+ bm25Index: "mem:index:bm25",
1037
+ relations: "mem:relations",
1038
+ profiles: "mem:profiles",
1039
+ claudeBridge: "mem:claude-bridge",
1040
+ graphNodes: "mem:graph:nodes",
1041
+ graphEdges: "mem:graph:edges",
1042
+ semantic: "mem:semantic",
1043
+ procedural: "mem:procedural",
1044
+ teamShared: (teamId) => `mem:team:${teamId}:shared`,
1045
+ teamUsers: (teamId, userId) => `mem:team:${teamId}:users:${userId}`,
1046
+ teamProfile: (teamId) => `mem:team:${teamId}:profile`,
1047
+ audit: "mem:audit",
1048
+ actions: "mem:actions",
1049
+ actionEdges: "mem:action-edges",
1050
+ leases: "mem:leases",
1051
+ routines: "mem:routines",
1052
+ routineRuns: "mem:routine-runs",
1053
+ signals: "mem:signals",
1054
+ checkpoints: "mem:checkpoints",
1055
+ mesh: "mem:mesh",
1056
+ sketches: "mem:sketches",
1057
+ facets: "mem:facets",
1058
+ sentinels: "mem:sentinels",
1059
+ crystals: "mem:crystals",
1060
+ lessons: "mem:lessons",
1061
+ insights: "mem:insights",
1062
+ graphEdgeHistory: "mem:graph:edge-history",
1063
+ enrichedChunks: (sessionId) => `mem:enriched:${sessionId}`,
1064
+ latentEmbeddings: (obsId) => `mem:latent:${obsId}`,
1065
+ retentionScores: "mem:retention",
1066
+ accessLog: "mem:access",
1067
+ imageRefs: "mem:image-refs",
1068
+ imageEmbeddings: "mem:image-embeddings",
1069
+ slots: "mem:slots",
1070
+ globalSlots: "mem:slots:global",
1071
+ state: "mem:state"
1072
+ };
1073
+ const STREAM = {
1074
+ name: "mem-live",
1075
+ group: (sessionId) => sessionId,
1076
+ viewerGroup: "viewer"
1077
+ };
1078
+ function generateId(prefix) {
1079
+ return `${prefix}_${Date.now().toString(36)}_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
1080
+ }
1081
+ function fingerprintId(prefix, content) {
1082
+ return `${prefix}_${createHash("sha256").update(content).digest("hex").slice(0, 16)}`;
1083
+ }
1084
+ function jaccardSimilarity(a, b) {
1085
+ const setA = new Set(a.split(/\s+/).filter((t) => t.length > 2));
1086
+ const setB = new Set(b.split(/\s+/).filter((t) => t.length > 2));
1087
+ if (setA.size === 0 && setB.size === 0) return 1;
1088
+ if (setA.size === 0 || setB.size === 0) return 0;
1089
+ let intersection = 0;
1090
+ for (const word of setA) if (setB.has(word)) intersection++;
1091
+ return intersection / (setA.size + setB.size - intersection);
1092
+ }
1093
+
997
1094
  //#endregion
998
1095
  //#region src/state/vector-index.ts
999
1096
  function float32ToBase64(arr) {
@@ -1057,6 +1154,22 @@ var VectorIndex = class VectorIndex {
1057
1154
  get size() {
1058
1155
  return this.vectors.size;
1059
1156
  }
1157
+ validateDimensions(expected) {
1158
+ const mismatches = [];
1159
+ const seenDimensions = /* @__PURE__ */ new Set();
1160
+ for (const [obsId, entry] of this.vectors) {
1161
+ const dim = entry.embedding.length;
1162
+ seenDimensions.add(dim);
1163
+ if (dim !== expected) mismatches.push({
1164
+ obsId,
1165
+ dim
1166
+ });
1167
+ }
1168
+ return {
1169
+ mismatches,
1170
+ seenDimensions
1171
+ };
1172
+ }
1060
1173
  clear() {
1061
1174
  this.vectors.clear();
1062
1175
  }
@@ -1100,75 +1213,6 @@ var VectorIndex = class VectorIndex {
1100
1213
  }
1101
1214
  };
1102
1215
 
1103
- //#endregion
1104
- //#region src/state/schema.ts
1105
- const KV = {
1106
- sessions: "mem:sessions",
1107
- observations: (sessionId) => `mem:obs:${sessionId}`,
1108
- memories: "mem:memories",
1109
- summaries: "mem:summaries",
1110
- config: "mem:config",
1111
- metrics: "mem:metrics",
1112
- health: "mem:health",
1113
- embeddings: (obsId) => `mem:emb:${obsId}`,
1114
- bm25Index: "mem:index:bm25",
1115
- relations: "mem:relations",
1116
- profiles: "mem:profiles",
1117
- claudeBridge: "mem:claude-bridge",
1118
- graphNodes: "mem:graph:nodes",
1119
- graphEdges: "mem:graph:edges",
1120
- semantic: "mem:semantic",
1121
- procedural: "mem:procedural",
1122
- teamShared: (teamId) => `mem:team:${teamId}:shared`,
1123
- teamUsers: (teamId, userId) => `mem:team:${teamId}:users:${userId}`,
1124
- teamProfile: (teamId) => `mem:team:${teamId}:profile`,
1125
- audit: "mem:audit",
1126
- actions: "mem:actions",
1127
- actionEdges: "mem:action-edges",
1128
- leases: "mem:leases",
1129
- routines: "mem:routines",
1130
- routineRuns: "mem:routine-runs",
1131
- signals: "mem:signals",
1132
- checkpoints: "mem:checkpoints",
1133
- mesh: "mem:mesh",
1134
- sketches: "mem:sketches",
1135
- facets: "mem:facets",
1136
- sentinels: "mem:sentinels",
1137
- crystals: "mem:crystals",
1138
- lessons: "mem:lessons",
1139
- insights: "mem:insights",
1140
- graphEdgeHistory: "mem:graph:edge-history",
1141
- enrichedChunks: (sessionId) => `mem:enriched:${sessionId}`,
1142
- latentEmbeddings: (obsId) => `mem:latent:${obsId}`,
1143
- retentionScores: "mem:retention",
1144
- accessLog: "mem:access",
1145
- imageRefs: "mem:image-refs",
1146
- imageEmbeddings: "mem:image-embeddings",
1147
- slots: "mem:slots",
1148
- globalSlots: "mem:slots:global",
1149
- state: "mem:state"
1150
- };
1151
- const STREAM = {
1152
- name: "mem-live",
1153
- group: (sessionId) => sessionId,
1154
- viewerGroup: "viewer"
1155
- };
1156
- function generateId(prefix) {
1157
- return `${prefix}_${Date.now().toString(36)}_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`;
1158
- }
1159
- function fingerprintId(prefix, content) {
1160
- return `${prefix}_${createHash("sha256").update(content).digest("hex").slice(0, 16)}`;
1161
- }
1162
- function jaccardSimilarity(a, b) {
1163
- const setA = new Set(a.split(/\s+/).filter((t) => t.length > 2));
1164
- const setB = new Set(b.split(/\s+/).filter((t) => t.length > 2));
1165
- if (setA.size === 0 && setB.size === 0) return 1;
1166
- if (setA.size === 0 || setB.size === 0) return 0;
1167
- let intersection = 0;
1168
- for (const word of setA) if (setB.has(word)) intersection++;
1169
- return intersection / (setA.size + setB.size - intersection);
1170
- }
1171
-
1172
1216
  //#endregion
1173
1217
  //#region src/functions/graph-retrieval.ts
1174
1218
  function buildGraphContext(path) {
@@ -2040,6 +2084,9 @@ var SearchIndex = class SearchIndex {
2040
2084
  }
2041
2085
  this.sortedTerms = null;
2042
2086
  }
2087
+ has(id) {
2088
+ return this.entries.has(id);
2089
+ }
2043
2090
  search(query, limit = 20) {
2044
2091
  const rawTerms = this.tokenize(query.toLowerCase());
2045
2092
  if (rawTerms.length === 0) return [];
@@ -2183,8 +2230,10 @@ var SearchIndex = class SearchIndex {
2183
2230
  //#endregion
2184
2231
  //#region src/state/index-persistence.ts
2185
2232
  const DEBOUNCE_MS = 5e3;
2233
+ const FAILURE_LOG_THROTTLE_MS = 6e4;
2186
2234
  var IndexPersistence = class {
2187
2235
  timer = null;
2236
+ lastFailureLogAt = 0;
2188
2237
  constructor(kv, bm25, vector) {
2189
2238
  this.kv = kv;
2190
2239
  this.bm25 = bm25;
@@ -2192,15 +2241,21 @@ var IndexPersistence = class {
2192
2241
  }
2193
2242
  scheduleSave() {
2194
2243
  if (this.timer) clearTimeout(this.timer);
2195
- this.timer = setTimeout(() => this.save(), DEBOUNCE_MS);
2244
+ this.timer = setTimeout(() => {
2245
+ this.save().catch((err) => this.logFailure(err));
2246
+ }, DEBOUNCE_MS);
2196
2247
  }
2197
2248
  async save() {
2198
2249
  if (this.timer) {
2199
2250
  clearTimeout(this.timer);
2200
2251
  this.timer = null;
2201
2252
  }
2202
- await this.kv.set(KV.bm25Index, "data", this.bm25.serialize());
2203
- if (this.vector && this.vector.size > 0) await this.kv.set(KV.bm25Index, "vectors", this.vector.serialize());
2253
+ try {
2254
+ await this.kv.set(KV.bm25Index, "data", this.bm25.serialize());
2255
+ if (this.vector && this.vector.size > 0) await this.kv.set(KV.bm25Index, "vectors", this.vector.serialize());
2256
+ } catch (err) {
2257
+ this.logFailure(err);
2258
+ }
2204
2259
  }
2205
2260
  async load() {
2206
2261
  let bm25 = null;
@@ -2220,6 +2275,18 @@ var IndexPersistence = class {
2220
2275
  this.timer = null;
2221
2276
  }
2222
2277
  }
2278
+ logFailure(err) {
2279
+ const now = Date.now();
2280
+ if (now - this.lastFailureLogAt < FAILURE_LOG_THROTTLE_MS) return;
2281
+ this.lastFailureLogAt = now;
2282
+ const code = err?.code;
2283
+ const message = err instanceof Error ? err.message : String(err);
2284
+ logger.warn("index persistence: failed to save BM25/vector index", {
2285
+ code,
2286
+ message,
2287
+ 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
2288
+ });
2289
+ }
2223
2290
  };
2224
2291
 
2225
2292
  //#endregion
@@ -2440,6 +2507,20 @@ async function deleteAccessLog(kv, memoryId) {
2440
2507
 
2441
2508
  //#endregion
2442
2509
  //#region src/functions/search.ts
2510
+ function memoryAsIndexable$1(memory) {
2511
+ return {
2512
+ id: memory.id,
2513
+ sessionId: memory.sessionIds[0] ?? "memory",
2514
+ timestamp: memory.createdAt,
2515
+ type: "decision",
2516
+ title: memory.title,
2517
+ facts: [memory.content],
2518
+ narrative: memory.content,
2519
+ concepts: memory.concepts,
2520
+ files: memory.files,
2521
+ importance: memory.strength
2522
+ };
2523
+ }
2443
2524
  let index = null;
2444
2525
  function getSearchIndex() {
2445
2526
  if (!index) index = new SearchIndex();
@@ -2448,9 +2529,20 @@ function getSearchIndex() {
2448
2529
  async function rebuildIndex(kv) {
2449
2530
  const idx = getSearchIndex();
2450
2531
  idx.clear();
2451
- const sessions = await kv.list(KV.sessions);
2452
- if (!sessions.length) return 0;
2453
2532
  let count = 0;
2533
+ try {
2534
+ const memories = await kv.list(KV.memories);
2535
+ for (const memory of memories) {
2536
+ if (memory.isLatest === false) continue;
2537
+ if (!memory.title || !memory.content) continue;
2538
+ idx.add(memoryAsIndexable$1(memory));
2539
+ count++;
2540
+ }
2541
+ } catch (err) {
2542
+ logger.warn("rebuildIndex: failed to load memories", { error: err instanceof Error ? err.message : String(err) });
2543
+ }
2544
+ const sessions = await kv.list(KV.sessions);
2545
+ if (!sessions.length) return count;
2454
2546
  const obsPerSession = [];
2455
2547
  const failedSessions = [];
2456
2548
  for (let batch = 0; batch < sessions.length; batch += 10) {
@@ -4756,6 +4848,20 @@ function registerPatternsFunction(sdk, kv) {
4756
4848
 
4757
4849
  //#endregion
4758
4850
  //#region src/functions/remember.ts
4851
+ function memoryAsIndexable(memory) {
4852
+ return {
4853
+ id: memory.id,
4854
+ sessionId: memory.sessionIds[0] ?? "memory",
4855
+ timestamp: memory.createdAt,
4856
+ type: "decision",
4857
+ title: memory.title,
4858
+ facts: [memory.content],
4859
+ narrative: memory.content,
4860
+ concepts: memory.concepts,
4861
+ files: memory.files,
4862
+ importance: memory.strength
4863
+ };
4864
+ }
4759
4865
  function registerRememberFunction(sdk, kv) {
4760
4866
  sdk.registerFunction("mem::remember", async (data) => {
4761
4867
  if (!data.content || typeof data.content !== "string" || !data.content.trim()) return {
@@ -4821,6 +4927,14 @@ function registerRememberFunction(sdk, kv) {
4821
4927
  await kv.set(KV.memories, supersededMemory.id, supersededMemory);
4822
4928
  }
4823
4929
  await kv.set(KV.memories, memory.id, memory);
4930
+ try {
4931
+ getSearchIndex().add(memoryAsIndexable(memory));
4932
+ } catch (err) {
4933
+ logger.warn("Failed to index saved memory into BM25", {
4934
+ memId: memory.id,
4935
+ error: err instanceof Error ? err.message : String(err)
4936
+ });
4937
+ }
4824
4938
  if (supersededId) await sdk.trigger({
4825
4939
  function_id: "mem::cascade-update",
4826
4940
  payload: { supersededMemoryId: supersededId },
@@ -5618,7 +5732,7 @@ function registerAutoForgetFunction(sdk, kv) {
5618
5732
 
5619
5733
  //#endregion
5620
5734
  //#region src/version.ts
5621
- const VERSION = "0.9.3";
5735
+ const VERSION = "0.9.5";
5622
5736
 
5623
5737
  //#endregion
5624
5738
  //#region src/functions/export-import.ts
@@ -5739,7 +5853,9 @@ function registerExportImportFunction(sdk, kv) {
5739
5853
  "0.9.0",
5740
5854
  "0.9.1",
5741
5855
  "0.9.2",
5742
- "0.9.3"
5856
+ "0.9.3",
5857
+ "0.9.4",
5858
+ "0.9.5"
5743
5859
  ]).has(importData.version)) return {
5744
5860
  success: false,
5745
5861
  error: `Unsupported export version: ${importData.version}`
@@ -12357,7 +12473,7 @@ function parseJsonlText(text, fallbackSessionId) {
12357
12473
  const parsed = JSON.parse(line);
12358
12474
  if (parsed && typeof parsed === "object") entries.push(parsed);
12359
12475
  } catch {}
12360
- let sessionId = fallbackSessionId || "";
12476
+ let sessionId = "";
12361
12477
  let cwd = "";
12362
12478
  let firstTs = "";
12363
12479
  let lastTs = "";
@@ -12418,7 +12534,7 @@ function parseJsonlText(text, fallbackSessionId) {
12418
12534
  });
12419
12535
  } else if (entry.type === "summary" || entry.type === "system") {}
12420
12536
  }
12421
- const effectiveSessionId = sessionId || generateId("sess");
12537
+ const effectiveSessionId = sessionId || fallbackSessionId || generateId("sess");
12422
12538
  for (const obs of observations) if (obs.sessionId === "imported") obs.sessionId = effectiveSessionId;
12423
12539
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
12424
12540
  return {
@@ -12528,6 +12644,8 @@ function projectTimeline(observations) {
12528
12644
 
12529
12645
  //#endregion
12530
12646
  //#region src/functions/replay.ts
12647
+ const MAX_FILES_DEFAULT = 200;
12648
+ const MAX_FILES_UPPER_BOUND = 1e3;
12531
12649
  const SENSITIVE_PATH_PATTERNS = [
12532
12650
  /(^|[\\/_.-])secret([\\/_.-]|s?$)/i,
12533
12651
  /(^|[\\/_.-])credentials?([\\/_.-]|$)/i,
@@ -12662,8 +12780,11 @@ async function loadObservations(kv, sessionId) {
12662
12780
  }
12663
12781
  async function findJsonlFiles(root, limit = 200) {
12664
12782
  const out = [];
12783
+ let discovered = 0;
12784
+ let walked = 0;
12785
+ const traversalCap = Math.max(limit * 50, 5e4);
12665
12786
  async function walk(dir) {
12666
- if (out.length >= limit) return;
12787
+ if (walked >= traversalCap) return;
12667
12788
  let names;
12668
12789
  try {
12669
12790
  names = await readdir(dir);
@@ -12671,7 +12792,8 @@ async function findJsonlFiles(root, limit = 200) {
12671
12792
  return;
12672
12793
  }
12673
12794
  for (const name of names) {
12674
- if (out.length >= limit) return;
12795
+ if (walked >= traversalCap) return;
12796
+ walked++;
12675
12797
  const full = join(dir, name);
12676
12798
  let st;
12677
12799
  try {
@@ -12681,11 +12803,20 @@ async function findJsonlFiles(root, limit = 200) {
12681
12803
  }
12682
12804
  if (st.isSymbolicLink()) continue;
12683
12805
  if (st.isDirectory()) await walk(full);
12684
- else if (st.isFile() && name.endsWith(".jsonl")) out.push(full);
12806
+ else if (st.isFile() && name.endsWith(".jsonl")) {
12807
+ discovered++;
12808
+ if (out.length < limit) out.push(full);
12809
+ }
12685
12810
  }
12686
12811
  }
12687
12812
  await walk(root);
12688
- return out;
12813
+ const traversalCapped = walked >= traversalCap;
12814
+ return {
12815
+ files: out,
12816
+ truncated: discovered > out.length || traversalCapped,
12817
+ discovered,
12818
+ traversalCapped
12819
+ };
12689
12820
  }
12690
12821
  function registerReplayFunctions(sdk, kv) {
12691
12822
  sdk.registerFunction("mem::replay::load", async (data) => {
@@ -12733,10 +12864,21 @@ function registerReplayFunctions(sdk, kv) {
12733
12864
  error: "path not found"
12734
12865
  };
12735
12866
  }
12867
+ const maxFiles = Number.isInteger(data.maxFiles) && data.maxFiles > 0 ? Math.min(data.maxFiles, MAX_FILES_UPPER_BOUND) : MAX_FILES_DEFAULT;
12736
12868
  let files = [];
12737
- if (stat.isDirectory()) files = await findJsonlFiles(abs, data.maxFiles || 200);
12738
- else if (stat.isFile() && abs.endsWith(".jsonl")) files = [abs];
12739
- else return {
12869
+ let truncated = false;
12870
+ let discovered = 0;
12871
+ let traversalCapped = false;
12872
+ if (stat.isDirectory()) {
12873
+ const found = await findJsonlFiles(abs, maxFiles);
12874
+ files = found.files;
12875
+ truncated = found.truncated;
12876
+ discovered = found.discovered;
12877
+ traversalCapped = found.traversalCapped;
12878
+ } else if (stat.isFile() && abs.endsWith(".jsonl")) {
12879
+ files = [abs];
12880
+ discovered = 1;
12881
+ } else return {
12740
12882
  success: false,
12741
12883
  error: "path must be a .jsonl file or directory"
12742
12884
  };
@@ -12744,7 +12886,12 @@ function registerReplayFunctions(sdk, kv) {
12744
12886
  success: true,
12745
12887
  imported: 0,
12746
12888
  sessionIds: [],
12747
- observations: 0
12889
+ observations: 0,
12890
+ discovered,
12891
+ truncated,
12892
+ traversalCapped,
12893
+ maxFiles,
12894
+ maxFilesUpperBound: MAX_FILES_UPPER_BOUND
12748
12895
  };
12749
12896
  const sessionIds = [];
12750
12897
  let observationCount = 0;
@@ -12810,7 +12957,12 @@ function registerReplayFunctions(sdk, kv) {
12810
12957
  success: true,
12811
12958
  imported: files.length,
12812
12959
  sessionIds,
12813
- observations: observationCount
12960
+ observations: observationCount,
12961
+ discovered,
12962
+ truncated,
12963
+ traversalCapped,
12964
+ maxFiles,
12965
+ maxFilesUpperBound: MAX_FILES_UPPER_BOUND
12814
12966
  };
12815
12967
  });
12816
12968
  }
@@ -13113,12 +13265,11 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13113
13265
  sdk.registerFunction("api::config-flags", async (req) => {
13114
13266
  const authErr = checkAuth(req, secret);
13115
13267
  if (authErr) return authErr;
13116
- const env = process.env;
13117
13268
  return {
13118
13269
  status_code: 200,
13119
13270
  body: {
13120
13271
  version: VERSION,
13121
- provider: env["ANTHROPIC_API_KEY"] || env["GEMINI_API_KEY"] || env["OPENROUTER_API_KEY"] || env["MINIMAX_API_KEY"] ? "llm" : "noop",
13272
+ provider: detectLlmProviderKind(),
13122
13273
  embeddingProvider: detectEmbeddingProvider() ? "embeddings" : "none",
13123
13274
  flags: [
13124
13275
  {
@@ -13416,11 +13567,12 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13416
13567
  payload.path = body.path.trim();
13417
13568
  }
13418
13569
  if (body.maxFiles !== void 0) {
13419
- if (!Number.isInteger(body.maxFiles) || body.maxFiles < 1) return {
13570
+ const n = body.maxFiles;
13571
+ if (!Number.isInteger(n) || n < 1 || n > MAX_FILES_UPPER_BOUND) return {
13420
13572
  status_code: 400,
13421
- body: { error: "maxFiles must be a positive integer" }
13573
+ body: { error: `maxFiles must be an integer between 1 and ${MAX_FILES_UPPER_BOUND}` }
13422
13574
  };
13423
- payload.maxFiles = body.maxFiles;
13575
+ payload.maxFiles = n;
13424
13576
  }
13425
13577
  return {
13426
13578
  status_code: 202,
@@ -14336,6 +14488,32 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14336
14488
  http_method: "GET"
14337
14489
  }
14338
14490
  });
14491
+ sdk.registerFunction("api::memory-by-id", async (req) => {
14492
+ const authErr = checkAuth(req, secret);
14493
+ if (authErr) return authErr;
14494
+ const id = req.path_params?.["id"];
14495
+ if (!id || typeof id !== "string") return {
14496
+ status_code: 400,
14497
+ body: { error: "id path parameter is required" }
14498
+ };
14499
+ const memory = await kv.get(KV.memories, id);
14500
+ if (!memory) return {
14501
+ status_code: 404,
14502
+ body: { error: `memory not found: ${id}` }
14503
+ };
14504
+ return {
14505
+ status_code: 200,
14506
+ body: { memory }
14507
+ };
14508
+ });
14509
+ sdk.registerTrigger({
14510
+ type: "http",
14511
+ function_id: "api::memory-by-id",
14512
+ config: {
14513
+ api_path: "/agentmemory/memories/:id",
14514
+ http_method: "GET"
14515
+ }
14516
+ });
14339
14517
  sdk.registerFunction("api::semantic-list", async (req) => {
14340
14518
  const authErr = checkAuth(req, secret);
14341
14519
  if (authErr) return authErr;
@@ -16214,6 +16392,15 @@ function registerEventTriggers(sdk, kv) {
16214
16392
  error: err instanceof Error ? err.message : String(err)
16215
16393
  });
16216
16394
  }
16395
+ if (isGraphExtractionEnabled()) try {
16396
+ const compressed = (await kv.list(KV.observations(data.sessionId))).filter((o) => o.title);
16397
+ if (compressed.length > 0) sdk.triggerVoid("mem::graph-extract", { observations: compressed });
16398
+ } catch (err) {
16399
+ logger.warn("graph-extract triggerVoid failed", {
16400
+ sessionId: data.sessionId,
16401
+ error: err instanceof Error ? err.message : String(err)
16402
+ });
16403
+ }
16217
16404
  return summary;
16218
16405
  });
16219
16406
  sdk.registerTrigger({
@@ -19198,6 +19385,14 @@ function initMetrics(getMeter) {
19198
19385
  function hasGetMeter(sdk) {
19199
19386
  return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
19200
19387
  }
19388
+ let lastUnhandledLogAt = 0;
19389
+ process.on("unhandledRejection", (reason) => {
19390
+ const now = Date.now();
19391
+ if (now - lastUnhandledLogAt < 6e4) return;
19392
+ lastUnhandledLogAt = now;
19393
+ const r = reason;
19394
+ console.warn(`[agentmemory] unhandledRejection (suppressed):`, r?.code ? `${r.code} ${r.function_id ?? ""} ${r.message ?? ""}`.trim() : reason);
19395
+ });
19201
19396
  async function main() {
19202
19397
  const config = loadConfig();
19203
19398
  const embeddingConfig = loadEmbeddingConfig();
@@ -19215,6 +19410,7 @@ async function main() {
19215
19410
  console.log(`[agentmemory] Streams: ws://localhost:${config.streamsPort}`);
19216
19411
  const sdk = registerWorker(config.engineUrl, {
19217
19412
  workerName: "agentmemory",
19413
+ invocationTimeoutMs: 18e4,
19218
19414
  otel: {
19219
19415
  serviceName: OTEL_CONFIG.serviceName,
19220
19416
  serviceVersion: OTEL_CONFIG.serviceVersion,
@@ -19323,8 +19519,20 @@ async function main() {
19323
19519
  console.log(`[agentmemory] Loaded persisted BM25 index (${bm25Index.size} docs)`);
19324
19520
  }
19325
19521
  if (loaded?.vector && vectorIndex && loaded.vector.size > 0) {
19326
- vectorIndex.restoreFrom(loaded.vector);
19327
- console.log(`[agentmemory] Loaded persisted vector index (${vectorIndex.size} vectors)`);
19522
+ const activeDim = embeddingProvider?.dimensions ?? 0;
19523
+ const { mismatches, seenDimensions } = activeDim > 0 ? loaded.vector.validateDimensions(activeDim) : {
19524
+ mismatches: [],
19525
+ seenDimensions: /* @__PURE__ */ new Set()
19526
+ };
19527
+ if (mismatches.length > 0) {
19528
+ const sample = mismatches.slice(0, 5).map((m) => `${m.obsId} (dim=${m.dim})`).join(", ");
19529
+ const distinct = Array.from(seenDimensions).sort((a, b) => a - b).join(", ");
19530
+ if (process.env["AGENTMEMORY_DROP_STALE_INDEX"] === "true") console.warn(`[agentmemory] Persisted vector index has ${mismatches.length} of ${loaded.vector.size} vectors with the wrong dimension. Active provider (${embeddingProvider?.name}) declares ${activeDim}; dimensions seen on disk: ${distinct}. AGENTMEMORY_DROP_STALE_INDEX=true is set — discarding the persisted vectors. Live observations will rebuild the index over time.`);
19531
+ else throw new Error(`[agentmemory] Refusing to start: persisted vector index has ${mismatches.length} of ${loaded.vector.size} vectors with the wrong dimension. Active provider (${embeddingProvider?.name}) declares ${activeDim}; dimensions seen on disk: ${distinct}. First mismatched obsIds: ${sample}. Loading would silently corrupt search (cross-dimension cosine returns 0). Choose one:\n - Re-embed the existing index against the new provider, then start.\n - Set AGENTMEMORY_DROP_STALE_INDEX=true to discard the persisted vectors and rebuild from live observations.\n - Switch the embedding provider back to the one that wrote the index.`);
19532
+ } else {
19533
+ vectorIndex.restoreFrom(loaded.vector);
19534
+ console.log(`[agentmemory] Loaded persisted vector index (${vectorIndex.size} vectors)`);
19535
+ }
19328
19536
  }
19329
19537
  if (bm25Index.size === 0) {
19330
19538
  const indexCount = await rebuildIndex(kv).catch((err) => {
@@ -19332,9 +19540,36 @@ async function main() {
19332
19540
  return 0;
19333
19541
  });
19334
19542
  if (indexCount > 0) {
19335
- console.log(`[agentmemory] Search index rebuilt: ${indexCount} observations`);
19543
+ console.log(`[agentmemory] Search index rebuilt: ${indexCount} entries`);
19336
19544
  indexPersistence.scheduleSave();
19337
19545
  }
19546
+ } else try {
19547
+ const memories = await kv.list(KV.memories);
19548
+ let backfilled = 0;
19549
+ for (const memory of memories) {
19550
+ if (memory.isLatest === false) continue;
19551
+ if (!memory.title || !memory.content) continue;
19552
+ if (bm25Index.has(memory.id)) continue;
19553
+ bm25Index.add({
19554
+ id: memory.id,
19555
+ sessionId: memory.sessionIds[0] ?? "memory",
19556
+ timestamp: memory.createdAt,
19557
+ type: "decision",
19558
+ title: memory.title,
19559
+ facts: [memory.content],
19560
+ narrative: memory.content,
19561
+ concepts: memory.concepts,
19562
+ files: memory.files,
19563
+ importance: memory.strength
19564
+ });
19565
+ backfilled++;
19566
+ }
19567
+ if (backfilled > 0) {
19568
+ console.log(`[agentmemory] Backfilled ${backfilled} memories into BM25 (legacy gap before #257)`);
19569
+ indexPersistence.scheduleSave();
19570
+ }
19571
+ } catch (err) {
19572
+ console.warn(`[agentmemory] Failed to backfill memories into BM25:`, err);
19338
19573
  }
19339
19574
  console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
19340
19575
  console.log(`[agentmemory] Endpoints: 107 REST + ${getAllTools().length} MCP tools + 6 MCP resources + 3 MCP prompts`);