@agentmemory/agentmemory 0.9.20 → 0.9.22

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 (55) hide show
  1. package/.env.example +2 -0
  2. package/README.md +166 -12
  3. package/dist/.env.example +2 -0
  4. package/dist/cli.d.mts +5 -1
  5. package/dist/cli.d.mts.map +1 -0
  6. package/dist/cli.mjs +122 -693
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/connect-BQQXpyDS.mjs +763 -0
  9. package/dist/connect-BQQXpyDS.mjs.map +1 -0
  10. package/dist/hooks/post-tool-use.mjs +1 -1
  11. package/dist/hooks/post-tool-use.mjs.map +1 -1
  12. package/dist/hooks/stop.mjs +8 -0
  13. package/dist/hooks/stop.mjs.map +1 -1
  14. package/dist/{image-refs-R3tin9MR.mjs → image-refs-CJS5B9Gq.mjs} +2 -2
  15. package/dist/{image-refs-R3tin9MR.mjs.map → image-refs-CJS5B9Gq.mjs.map} +1 -1
  16. package/dist/{image-store-DyrKZKqZ.mjs → image-store-CdE0amb1.mjs} +1 -1
  17. package/dist/index.mjs +881 -281
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/logger-xlVlvCWX.mjs +43 -0
  20. package/dist/logger-xlVlvCWX.mjs.map +1 -0
  21. package/dist/schema-BkALl7Z_.mjs +74 -0
  22. package/dist/schema-BkALl7Z_.mjs.map +1 -0
  23. package/dist/{src-DPSaLB5-.mjs → src-gpTAJuBy.mjs} +861 -287
  24. package/dist/src-gpTAJuBy.mjs.map +1 -0
  25. package/dist/{standalone-DMLk7YxP.mjs → standalone-C4i7ktpn.mjs} +48 -12
  26. package/dist/standalone-C4i7ktpn.mjs.map +1 -0
  27. package/dist/standalone.d.mts.map +1 -1
  28. package/dist/standalone.mjs +45 -10
  29. package/dist/standalone.mjs.map +1 -1
  30. package/dist/{tools-registry-Dz8ssuMf.mjs → tools-registry-B7Y6nJsr.mjs} +39 -11
  31. package/dist/tools-registry-B7Y6nJsr.mjs.map +1 -0
  32. package/dist/version-DvQMNbEH.mjs +6 -0
  33. package/dist/version-DvQMNbEH.mjs.map +1 -0
  34. package/dist/viewer/index.html +134 -21
  35. package/package.json +6 -4
  36. package/plugin/.claude-plugin/plugin.json +1 -1
  37. package/plugin/.codex-plugin/plugin.json +1 -1
  38. package/plugin/.mcp.json +3 -2
  39. package/plugin/hooks/hooks.codex.json +6 -6
  40. package/plugin/hooks/hooks.json +12 -12
  41. package/plugin/opencode/README.md +229 -0
  42. package/plugin/opencode/agentmemory-capture.ts +687 -0
  43. package/plugin/opencode/commands/recall.md +19 -0
  44. package/plugin/opencode/commands/remember.md +19 -0
  45. package/plugin/opencode/plugin.json +12 -0
  46. package/plugin/scripts/diagnostics.d.mts +17 -0
  47. package/plugin/scripts/diagnostics.d.mts.map +1 -0
  48. package/plugin/scripts/diagnostics.mjs.map +1 -0
  49. package/plugin/scripts/post-tool-use.mjs +1 -1
  50. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  51. package/plugin/scripts/stop.mjs +8 -0
  52. package/plugin/scripts/stop.mjs.map +1 -1
  53. package/dist/src-DPSaLB5-.mjs.map +0 -1
  54. package/dist/standalone-DMLk7YxP.mjs.map +0 -1
  55. package/dist/tools-registry-Dz8ssuMf.mjs.map +0 -1
@@ -1,9 +1,11 @@
1
- import { a as STREAM, c as jaccardSimilarity, i as KV, n as bootLog, o as fingerprintId, r as logger, s as generateId, t as VERSION } from "./cli.mjs";
2
- import { a as isManagedImagePath, getImageRefCount, i as getMaxBytes, n as IMAGES_DIR, r as deleteImage, t as withKeyedLock } from "./image-refs-R3tin9MR.mjs";
3
- import { _ as loadTeamConfig, a as getConsolidationDecayDays, c as isAutoCompressEnabled, d as isGraphExtractionEnabled, f as loadClaudeBridgeConfig, g as loadSnapshotConfig, h as loadFallbackConfig, i as detectLlmProviderKind, l as isConsolidationEnabled, m as loadEmbeddingConfig, n as getVisibleTools, o as getEnvVar, p as loadConfig, r as detectEmbeddingProvider, t as getAllTools, u as isContextInjectionEnabled } from "./tools-registry-Dz8ssuMf.mjs";
1
+ import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./schema-BkALl7Z_.mjs";
2
+ import { n as logger, t as bootLog } from "./logger-xlVlvCWX.mjs";
3
+ import { t as VERSION } from "./version-DvQMNbEH.mjs";
4
+ import { a as isManagedImagePath, getImageRefCount, i as getMaxBytes, n as IMAGES_DIR, r as deleteImage, t as withKeyedLock } from "./image-refs-CJS5B9Gq.mjs";
5
+ import { _ as loadEmbeddingConfig, a as getAgentId, b as loadTeamConfig, d as isConsolidationEnabled, f as isContextInjectionEnabled, g as loadConfig, h as loadClaudeBridgeConfig, i as detectLlmProviderKind, l as isAgentScopeIsolated, m as isGraphExtractionEnabled, n as getVisibleTools, o as getConsolidationDecayDays, p as isDropStaleIndexEnabled, r as detectEmbeddingProvider, s as getEnvVar, t as getAllTools, u as isAutoCompressEnabled, v as loadFallbackConfig, y as loadSnapshotConfig } from "./tools-registry-B7Y6nJsr.mjs";
4
6
  import { createRequire } from "node:module";
5
7
  import { execFile } from "node:child_process";
6
- import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
7
9
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
8
10
  import { fileURLToPath } from "node:url";
9
11
  import { homedir } from "node:os";
@@ -229,13 +231,25 @@ function v1AzureUrl(baseUrl, path) {
229
231
  url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
230
232
  return url.toString();
231
233
  }
234
+ function appendOpenAIRoute(baseUrl, route) {
235
+ const trimmedBase = baseUrl.replace(/\/+$/, "");
236
+ const cleanRoute = route.startsWith("/") ? route : `/${route}`;
237
+ let pathname;
238
+ try {
239
+ pathname = new URL(trimmedBase).pathname.replace(/\/+$/, "");
240
+ } catch {
241
+ return `${trimmedBase}/v1${cleanRoute}`;
242
+ }
243
+ if (pathname === "" || pathname === "/") return `${trimmedBase}/v1${cleanRoute}`;
244
+ return `${trimmedBase}${cleanRoute}`;
245
+ }
232
246
  function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
233
247
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
234
- return `${baseUrl}/v1/chat/completions`;
248
+ return appendOpenAIRoute(baseUrl, "/chat/completions");
235
249
  }
236
250
  function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
237
251
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
238
- return `${baseUrl}/v1/embeddings`;
252
+ return appendOpenAIRoute(baseUrl, "/embeddings");
239
253
  }
240
254
  function buildAuthHeaders(apiKey, isAzure) {
241
255
  if (isAzure) return {
@@ -317,6 +331,7 @@ var OpenAIProvider = class {
317
331
  const body = {
318
332
  model: this.model,
319
333
  max_tokens: this.maxTokens,
334
+ stream: false,
320
335
  messages: [{
321
336
  role: "system",
322
337
  content: systemPrompt
@@ -345,7 +360,7 @@ var OpenAIProvider = class {
345
360
  const message = data.choices?.[0]?.message;
346
361
  const content = message?.content;
347
362
  if (content) return content;
348
- const reasoning = message?.reasoning;
363
+ const reasoning = message?.reasoning ?? message?.reasoning_content;
349
364
  if (reasoning) return reasoning;
350
365
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
351
366
  }
@@ -2182,6 +2197,24 @@ var SearchIndex = class SearchIndex {
2182
2197
  has(id) {
2183
2198
  return this.entries.has(id);
2184
2199
  }
2200
+ remove(id) {
2201
+ const entry = this.entries.get(id);
2202
+ if (!entry) return;
2203
+ const termFreq = this.docTermCounts.get(id);
2204
+ if (termFreq) {
2205
+ for (const term of termFreq.keys()) {
2206
+ const postingList = this.invertedIndex.get(term);
2207
+ if (postingList) {
2208
+ postingList.delete(id);
2209
+ if (postingList.size === 0) this.invertedIndex.delete(term);
2210
+ }
2211
+ }
2212
+ this.docTermCounts.delete(id);
2213
+ }
2214
+ this.totalDocLength = Math.max(0, this.totalDocLength - entry.termCount);
2215
+ this.entries.delete(id);
2216
+ this.sortedTerms = null;
2217
+ }
2185
2218
  search(query, limit = 20) {
2186
2219
  const rawTerms = this.tokenize(query.toLowerCase());
2187
2220
  if (rawTerms.length === 0) return [];
@@ -2521,6 +2554,7 @@ function buildSyntheticCompression(raw) {
2521
2554
  };
2522
2555
  if (raw.modality) result.modality = raw.modality;
2523
2556
  if (raw.imageData) result.imageData = raw.imageData;
2557
+ if (raw.agentId) result.agentId = raw.agentId;
2524
2558
  return result;
2525
2559
  }
2526
2560
 
@@ -2610,6 +2644,16 @@ function setVectorIndex(idx) {
2610
2644
  function setEmbeddingProvider(provider) {
2611
2645
  currentEmbeddingProvider = provider;
2612
2646
  }
2647
+ function vectorIndexRemove(id) {
2648
+ vectorIndex?.remove(id);
2649
+ }
2650
+ let indexPersistence = null;
2651
+ function setIndexPersistence(p) {
2652
+ indexPersistence = p;
2653
+ }
2654
+ async function flushIndexSave() {
2655
+ await indexPersistence?.save();
2656
+ }
2613
2657
  const EMBED_MAX_CHARS = 16e3;
2614
2658
  function clipEmbedInput(text) {
2615
2659
  if (text.length <= EMBED_MAX_CHARS) return text;
@@ -2643,20 +2687,108 @@ async function vectorIndexAddGuarded(id, sessionId, text, context) {
2643
2687
  return false;
2644
2688
  }
2645
2689
  }
2690
+ async function vectorIndexAddBatchGuarded(items) {
2691
+ const vi = vectorIndex;
2692
+ const ep = currentEmbeddingProvider;
2693
+ if (!vi || !ep || items.length === 0) return {
2694
+ ok: 0,
2695
+ fail: 0
2696
+ };
2697
+ let embeddings;
2698
+ try {
2699
+ embeddings = await ep.embedBatch(items.map((i) => clipEmbedInput(i.text)));
2700
+ } catch (err) {
2701
+ logger.warn("vector-index add batch: embed failed — skipping batch", {
2702
+ batchSize: items.length,
2703
+ provider: ep.name,
2704
+ error: err instanceof Error ? err.message : String(err)
2705
+ });
2706
+ return {
2707
+ ok: 0,
2708
+ fail: items.length
2709
+ };
2710
+ }
2711
+ if (embeddings.length !== items.length) {
2712
+ logger.warn("vector-index add batch: provider returned wrong length — skipping batch", {
2713
+ batchSize: items.length,
2714
+ returned: embeddings.length,
2715
+ provider: ep.name
2716
+ });
2717
+ return {
2718
+ ok: 0,
2719
+ fail: items.length
2720
+ };
2721
+ }
2722
+ let ok = 0;
2723
+ let fail = 0;
2724
+ for (let i = 0; i < items.length; i++) {
2725
+ const item = items[i];
2726
+ const embedding = embeddings[i];
2727
+ if (embedding.length !== ep.dimensions) {
2728
+ logger.warn("vector-index add batch: dimension mismatch — skipping item", {
2729
+ kind: item.context.kind,
2730
+ id: item.context.logId,
2731
+ provider: ep.name,
2732
+ expected: ep.dimensions,
2733
+ received: embedding.length
2734
+ });
2735
+ fail++;
2736
+ continue;
2737
+ }
2738
+ try {
2739
+ vi.add(item.id, item.sessionId, embedding);
2740
+ ok++;
2741
+ } catch (err) {
2742
+ logger.warn("vector-index add batch: index write failed — skipping item", {
2743
+ kind: item.context.kind,
2744
+ id: item.context.logId,
2745
+ error: err instanceof Error ? err.message : String(err)
2746
+ });
2747
+ fail++;
2748
+ }
2749
+ }
2750
+ return {
2751
+ ok,
2752
+ fail
2753
+ };
2754
+ }
2755
+ const DEFAULT_REBUILD_EMBED_BATCH = 32;
2756
+ function getRebuildEmbedBatchSize() {
2757
+ const raw = process.env.REBUILD_EMBED_BATCH_SIZE;
2758
+ if (!raw) return DEFAULT_REBUILD_EMBED_BATCH;
2759
+ const n = parseInt(raw, 10);
2760
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_REBUILD_EMBED_BATCH;
2761
+ }
2646
2762
  async function rebuildIndex(kv) {
2647
2763
  const idx = getSearchIndex();
2648
2764
  idx.clear();
2649
2765
  vectorIndex?.clear();
2766
+ const batchSize = getRebuildEmbedBatchSize();
2767
+ const pending = [];
2650
2768
  let count = 0;
2769
+ const flush = async () => {
2770
+ if (pending.length === 0) return;
2771
+ await vectorIndexAddBatchGuarded(pending);
2772
+ pending.length = 0;
2773
+ };
2774
+ const enqueue = async (job) => {
2775
+ pending.push(job);
2776
+ if (pending.length >= batchSize) await flush();
2777
+ };
2651
2778
  try {
2652
2779
  const memories = await kv.list(KV.memories);
2653
2780
  for (const memory of memories) {
2654
2781
  if (memory.isLatest === false) continue;
2655
2782
  if (!memory.title || !memory.content) continue;
2656
2783
  idx.add(memoryToObservation(memory));
2657
- await vectorIndexAddGuarded(memory.id, memory.sessionIds[0] ?? "memory", memory.title + " " + memory.content, {
2658
- kind: "memory",
2659
- logId: memory.id
2784
+ await enqueue({
2785
+ id: memory.id,
2786
+ sessionId: memory.sessionIds[0] ?? "memory",
2787
+ text: memory.title + " " + memory.content,
2788
+ context: {
2789
+ kind: "memory",
2790
+ logId: memory.id
2791
+ }
2660
2792
  });
2661
2793
  count++;
2662
2794
  }
@@ -2664,7 +2796,10 @@ async function rebuildIndex(kv) {
2664
2796
  logger.warn("rebuildIndex: failed to load memories", { error: err instanceof Error ? err.message : String(err) });
2665
2797
  }
2666
2798
  const sessions = await kv.list(KV.sessions);
2667
- if (!sessions.length) return count;
2799
+ if (!sessions.length) {
2800
+ await flush();
2801
+ return count;
2802
+ }
2668
2803
  const obsPerSession = [];
2669
2804
  const failedSessions = [];
2670
2805
  for (let batch = 0; batch < sessions.length; batch += 10) {
@@ -2682,12 +2817,18 @@ async function rebuildIndex(kv) {
2682
2817
  if (failedSessions.length > 0) logger.warn("rebuildIndex: failed to load observations for sessions", { failedSessions });
2683
2818
  for (const observations of obsPerSession) for (const obs of observations) if (obs.title && obs.narrative) {
2684
2819
  idx.add(obs);
2685
- await vectorIndexAddGuarded(obs.id, obs.sessionId, obs.title + " " + obs.narrative, {
2686
- kind: "observation",
2687
- logId: obs.id
2820
+ await enqueue({
2821
+ id: obs.id,
2822
+ sessionId: obs.sessionId,
2823
+ text: obs.title + " " + obs.narrative,
2824
+ context: {
2825
+ kind: "observation",
2826
+ logId: obs.id
2827
+ }
2688
2828
  });
2689
2829
  count++;
2690
2830
  }
2831
+ await flush();
2691
2832
  return count;
2692
2833
  }
2693
2834
  function registerSearchFunction(sdk, kv) {
@@ -2907,11 +3048,14 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2907
3048
  error: `Session observation limit reached (${maxObservationsPerSession})`
2908
3049
  };
2909
3050
  }
3051
+ const existingSession = await kv.get(KV.sessions, payload.sessionId);
3052
+ const inheritedAgentId = existingSession ? existingSession.agentId : getAgentId();
3053
+ if (inheritedAgentId) raw.agentId = inheritedAgentId;
2910
3054
  if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
2911
- const { saveImageToDisk } = await import("./image-store-DyrKZKqZ.mjs");
3055
+ const { saveImageToDisk } = await import("./image-store-CdE0amb1.mjs");
2912
3056
  const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
2913
3057
  raw.imageData = filePath;
2914
- const { incrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
3058
+ const { incrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
2915
3059
  await incrementImageRef(kv, filePath);
2916
3060
  sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: bytesWritten });
2917
3061
  if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.triggerVoid("mem::vision-embed", {
@@ -2924,7 +3068,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2924
3068
  await kv.set(KV.observations(payload.sessionId), obsId, raw);
2925
3069
  } catch (error) {
2926
3070
  if (raw.imageData) {
2927
- const { deleteImage } = await import("./image-store-DyrKZKqZ.mjs");
3071
+ const { deleteImage } = await import("./image-store-CdE0amb1.mjs");
2928
3072
  const { deletedBytes } = await deleteImage(raw.imageData);
2929
3073
  if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
2930
3074
  }
@@ -2958,7 +3102,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2958
3102
  },
2959
3103
  action: TriggerAction.Void()
2960
3104
  });
2961
- const session = await kv.get(KV.sessions, payload.sessionId);
3105
+ const session = existingSession;
2962
3106
  if (session) {
2963
3107
  const updates = [{
2964
3108
  type: "set",
@@ -2978,6 +3122,20 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2978
3122
  });
2979
3123
  }
2980
3124
  await kv.update(KV.sessions, payload.sessionId, updates);
3125
+ } else if (typeof payload.project === "string" && payload.project.trim().length > 0 && typeof payload.cwd === "string" && payload.cwd.trim().length > 0) {
3126
+ const trimmedPrompt = typeof raw.userPrompt === "string" ? raw.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
3127
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3128
+ await kv.set(KV.sessions, payload.sessionId, {
3129
+ id: payload.sessionId,
3130
+ project: payload.project,
3131
+ cwd: payload.cwd,
3132
+ startedAt: payload.timestamp ?? ts,
3133
+ updatedAt: ts,
3134
+ status: "active",
3135
+ observationCount: 1,
3136
+ ...inheritedAgentId ? { agentId: inheritedAgentId } : {},
3137
+ ...trimmedPrompt && trimmedPrompt.length > 0 ? { firstPrompt: trimmedPrompt } : {}
3138
+ });
2981
3139
  }
2982
3140
  if (isAutoCompressEnabled()) await sdk.trigger({
2983
3141
  function_id: "mem::compress",
@@ -4130,7 +4288,8 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
4130
4288
  confidence: qualityScore / 100,
4131
4289
  ...hasImage ? { modality: data.raw.modality } : {},
4132
4290
  ...imageDescription ? { imageDescription } : {},
4133
- ...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
4291
+ ...data.raw.imageData ? { imageRef: data.raw.imageData } : {},
4292
+ ...data.raw.agentId ? { agentId: data.raw.agentId } : {}
4134
4293
  };
4135
4294
  await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
4136
4295
  try {
@@ -4365,9 +4524,134 @@ function buildSummaryPrompt(observations) {
4365
4524
  });
4366
4525
  return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
4367
4526
  }
4527
+ const REDUCE_SYSTEM = `You are merging multiple partial summaries of the SAME coding session into one final session summary. The partials are chronological chunks of one continuous session — not separate sessions.
4528
+
4529
+ Output EXACTLY this XML format with no additional text:
4530
+
4531
+ <summary>
4532
+ <title>Short session title (max 100 chars)</title>
4533
+ <narrative>3-5 sentence narrative covering the whole session</narrative>
4534
+ <decisions>
4535
+ <decision>Key technical decision made</decision>
4536
+ </decisions>
4537
+ <files>
4538
+ <file>path/to/modified/file</file>
4539
+ </files>
4540
+ <concepts>
4541
+ <concept>key concept from session</concept>
4542
+ </concepts>
4543
+ </summary>
4544
+
4545
+ Rules:
4546
+ - Synthesize a single narrative that reflects the whole arc, not a chunk-by-chunk recap
4547
+ - Preserve every distinct decision across chunks
4548
+ - Union (deduplicate) all files and concepts
4549
+ - Title should capture the session's overall outcome`;
4550
+ function buildReducePrompt(partials) {
4551
+ const sections = partials.map((p, i) => {
4552
+ const decisions = p.keyDecisions.map((d) => ` - ${d}`).join("\n");
4553
+ const files = p.filesModified.map((f) => ` - ${f}`).join("\n");
4554
+ const concepts = p.concepts.join(", ");
4555
+ return `[Chunk ${i + 1} of ${partials.length} — obs ${p.obsRangeStart}-${p.obsRangeEnd}]
4556
+ Title: ${p.title}
4557
+ Narrative: ${p.narrative}
4558
+ Decisions:
4559
+ ${decisions}
4560
+ Files:
4561
+ ${files}
4562
+ Concepts: ${concepts}`;
4563
+ });
4564
+ return `Partial summaries (${partials.length} chunks of one session, chronological):\n\n${sections.join("\n\n---\n\n")}`;
4565
+ }
4368
4566
 
4369
4567
  //#endregion
4370
4568
  //#region src/functions/summarize.ts
4569
+ const CHUNK_SIZE_DEFAULT = 400;
4570
+ const CHUNK_CONCURRENCY_DEFAULT = 6;
4571
+ const MAX_SKIP_RATIO = .5;
4572
+ function getChunkSize() {
4573
+ const raw = process.env.SUMMARIZE_CHUNK_SIZE;
4574
+ if (!raw) return CHUNK_SIZE_DEFAULT;
4575
+ const n = parseInt(raw, 10);
4576
+ return Number.isFinite(n) && n > 0 ? n : CHUNK_SIZE_DEFAULT;
4577
+ }
4578
+ function getChunkConcurrency() {
4579
+ const raw = process.env.SUMMARIZE_CHUNK_CONCURRENCY;
4580
+ if (!raw) return CHUNK_CONCURRENCY_DEFAULT;
4581
+ const n = parseInt(raw, 10);
4582
+ return Number.isFinite(n) && n > 0 ? n : CHUNK_CONCURRENCY_DEFAULT;
4583
+ }
4584
+ async function summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, total) {
4585
+ for (let attempt = 1; attempt <= 2; attempt++) try {
4586
+ const parsed = parseSummaryXml(await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(chunk)), sessionId, project, chunk.length);
4587
+ if (parsed) return parsed;
4588
+ logger.warn("Summarize chunk parse failed", {
4589
+ sessionId,
4590
+ chunk: `${idx + 1}/${total}`,
4591
+ attempt
4592
+ });
4593
+ } catch (err) {
4594
+ logger.warn("Summarize chunk LLM call failed", {
4595
+ sessionId,
4596
+ chunk: `${idx + 1}/${total}`,
4597
+ attempt,
4598
+ error: err instanceof Error ? err.message : String(err)
4599
+ });
4600
+ }
4601
+ return null;
4602
+ }
4603
+ async function produceSummaryXml(provider, compressed, sessionId, project) {
4604
+ const chunkSize = getChunkSize();
4605
+ if (compressed.length <= chunkSize) return {
4606
+ response: await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(compressed)),
4607
+ mode: "single",
4608
+ chunks: 1
4609
+ };
4610
+ const chunks = [];
4611
+ for (let i = 0; i < compressed.length; i += chunkSize) chunks.push(compressed.slice(i, i + chunkSize));
4612
+ const concurrency = getChunkConcurrency();
4613
+ logger.info("Summarize chunking session", {
4614
+ sessionId,
4615
+ chunks: chunks.length,
4616
+ chunkSize,
4617
+ concurrency,
4618
+ totalObservations: compressed.length
4619
+ });
4620
+ const partialByIdx = new Array(chunks.length).fill(null);
4621
+ for (let batchStart = 0; batchStart < chunks.length; batchStart += concurrency) {
4622
+ const batch = chunks.slice(batchStart, batchStart + concurrency);
4623
+ await Promise.all(batch.map(async (chunk, j) => {
4624
+ const idx = batchStart + j;
4625
+ partialByIdx[idx] = await summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, chunks.length);
4626
+ }));
4627
+ }
4628
+ const skipped = partialByIdx.filter((p) => p === null).length;
4629
+ const partials = partialByIdx.filter((p) => p !== null);
4630
+ if (skipped > Math.floor(chunks.length * MAX_SKIP_RATIO)) throw new Error(`too_many_chunks_skipped: ${skipped}/${chunks.length} chunks failed to parse after retry`);
4631
+ if (skipped > 0) logger.warn("Summarize chunks partially skipped", {
4632
+ sessionId,
4633
+ skipped,
4634
+ total: chunks.length
4635
+ });
4636
+ const reduceInput = partials.map((p) => {
4637
+ const originalIdx = partialByIdx.indexOf(p);
4638
+ return {
4639
+ title: p.title,
4640
+ narrative: p.narrative,
4641
+ keyDecisions: p.keyDecisions,
4642
+ filesModified: p.filesModified,
4643
+ concepts: p.concepts,
4644
+ obsRangeStart: originalIdx * chunkSize + 1,
4645
+ obsRangeEnd: Math.min((originalIdx + 1) * chunkSize, compressed.length)
4646
+ };
4647
+ });
4648
+ return {
4649
+ response: await provider.summarize(REDUCE_SYSTEM, buildReducePrompt(reduceInput)),
4650
+ mode: "chunked",
4651
+ chunks: chunks.length,
4652
+ skipped
4653
+ };
4654
+ }
4371
4655
  function parseSummaryXml(xml, sessionId, project, obsCount) {
4372
4656
  const title = getXmlTag(xml, "title");
4373
4657
  if (!title) return null;
@@ -4416,16 +4700,15 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
4416
4700
  };
4417
4701
  }
4418
4702
  try {
4419
- const prompt = buildSummaryPrompt(compressed);
4420
- const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
4703
+ const { response, mode, chunks } = await produceSummaryXml(provider, compressed, sessionId, session.project);
4421
4704
  if (!response || !response.trim()) {
4422
4705
  const latencyMs = Date.now() - startMs;
4423
4706
  if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
4424
4707
  logger.warn("Empty provider response on summarize", {
4425
4708
  sessionId,
4426
4709
  provider: provider.name,
4427
- promptBytes: prompt.length,
4428
- systemBytes: SUMMARY_SYSTEM.length,
4710
+ mode,
4711
+ chunks,
4429
4712
  observationCount: compressed.length
4430
4713
  });
4431
4714
  return {
@@ -4977,6 +5260,7 @@ function registerRememberFunction(sdk, kv) {
4977
5260
  break;
4978
5261
  }
4979
5262
  }
5263
+ const callAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim().slice(0, 128) : getAgentId();
4980
5264
  const memory = {
4981
5265
  id: generateId("mem"),
4982
5266
  createdAt: now,
@@ -4992,7 +5276,8 @@ function registerRememberFunction(sdk, kv) {
4992
5276
  parentId: supersededId,
4993
5277
  supersedes: supersededId ? [supersededId] : [],
4994
5278
  sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
4995
- isLatest: true
5279
+ isLatest: true,
5280
+ ...callAgentId ? { agentId: callAgentId } : {}
4996
5281
  };
4997
5282
  if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
4998
5283
  if (supersededMemory) {
@@ -5032,12 +5317,14 @@ function registerRememberFunction(sdk, kv) {
5032
5317
  const deletedMemoryIds = [];
5033
5318
  const deletedObservationIds = [];
5034
5319
  let deletedSession = false;
5035
- const { decrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
5320
+ const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
5036
5321
  if (data.memoryId) {
5037
5322
  const mem = await kv.get(KV.memories, data.memoryId);
5038
5323
  await kv.delete(KV.memories, data.memoryId);
5039
5324
  if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
5040
5325
  await deleteAccessLog(kv, data.memoryId);
5326
+ getSearchIndex().remove(data.memoryId);
5327
+ vectorIndexRemove(data.memoryId);
5041
5328
  deletedMemoryIds.push(data.memoryId);
5042
5329
  deleted++;
5043
5330
  }
@@ -5046,6 +5333,8 @@ function registerRememberFunction(sdk, kv) {
5046
5333
  await kv.delete(KV.observations(data.sessionId), obsId);
5047
5334
  if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5048
5335
  if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5336
+ getSearchIndex().remove(obsId);
5337
+ vectorIndexRemove(obsId);
5049
5338
  deletedObservationIds.push(obsId);
5050
5339
  deleted++;
5051
5340
  }
@@ -5055,6 +5344,8 @@ function registerRememberFunction(sdk, kv) {
5055
5344
  await kv.delete(KV.observations(data.sessionId), obs.id);
5056
5345
  if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5057
5346
  if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5347
+ getSearchIndex().remove(obs.id);
5348
+ vectorIndexRemove(obs.id);
5058
5349
  deletedObservationIds.push(obs.id);
5059
5350
  deleted++;
5060
5351
  }
@@ -5063,14 +5354,17 @@ function registerRememberFunction(sdk, kv) {
5063
5354
  deletedSession = true;
5064
5355
  deleted += 2;
5065
5356
  }
5066
- if (deleted > 0) await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5067
- sessionId: data.sessionId,
5068
- deleted,
5069
- memoriesDeleted: deletedMemoryIds.length,
5070
- observationsDeleted: deletedObservationIds.length,
5071
- sessionDeleted: deletedSession,
5072
- reason: "user-initiated forget"
5073
- });
5357
+ if (deleted > 0) {
5358
+ await flushIndexSave();
5359
+ await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5360
+ sessionId: data.sessionId,
5361
+ deleted,
5362
+ memoriesDeleted: deletedMemoryIds.length,
5363
+ observationsDeleted: deletedObservationIds.length,
5364
+ sessionDeleted: deletedSession,
5365
+ reason: "user-initiated forget"
5366
+ });
5367
+ }
5074
5368
  logger.info("Memory forgotten", { deleted });
5075
5369
  return {
5076
5370
  success: true,
@@ -5131,7 +5425,7 @@ async function runRecoveredSessionConsolidation(sdk) {
5131
5425
  function registerEvictFunction(sdk, kv) {
5132
5426
  sdk.registerFunction("mem::evict", async (data) => {
5133
5427
  const dryRun = data?.dryRun ?? false;
5134
- const { decrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
5428
+ const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
5135
5429
  const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
5136
5430
  const cfg = {
5137
5431
  ...DEFAULTS$1,
@@ -5582,8 +5876,12 @@ async function findByKeyword(kv, keyword, project) {
5582
5876
 
5583
5877
  //#endregion
5584
5878
  //#region src/functions/smart-search.ts
5879
+ const LESSON_CONTENT_PREVIEW_CHARS = 240;
5585
5880
  function registerSmartSearchFunction(sdk, kv, searchFn) {
5586
5881
  sdk.registerFunction("mem::smart-search", async (data) => {
5882
+ const isolated = isAgentScopeIsolated();
5883
+ const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
5884
+ const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
5587
5885
  if (data.expandIds && data.expandIds.length > 0) {
5588
5886
  const raw = data.expandIds.slice(0, 20);
5589
5887
  const items = raw.map((entry) => {
@@ -5604,17 +5902,19 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
5604
5902
  observation: obs
5605
5903
  } : null)));
5606
5904
  for (const r of results) if (r) expanded.push(r);
5607
- recordAccessBatch(kv, expanded.map((e) => e.observation.id));
5905
+ const scoped = filterAgentId ? expanded.filter((e) => e.observation.agentId === filterAgentId) : expanded;
5906
+ recordAccessBatch(kv, scoped.map((e) => e.observation.id));
5608
5907
  const truncated = data.expandIds.length > raw.length;
5609
5908
  logger.info("Smart search expanded", {
5610
5909
  requested: data.expandIds.length,
5611
5910
  attempted: raw.length,
5612
- returned: expanded.length,
5911
+ returned: scoped.length,
5912
+ filteredOutOfScope: expanded.length - scoped.length,
5613
5913
  truncated
5614
5914
  });
5615
5915
  return {
5616
5916
  mode: "expanded",
5617
- results: expanded,
5917
+ results: scoped,
5618
5918
  truncated
5619
5919
  };
5620
5920
  }
@@ -5624,7 +5924,11 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
5624
5924
  error: "query is required"
5625
5925
  };
5626
5926
  const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
5627
- const compact = (await searchFn(data.query, limit)).map((r) => ({
5927
+ const lessonLimit = Math.min(limit, 10);
5928
+ const includeLessons = data.includeLessons !== false;
5929
+ const overFetchLimit = filterAgentId ? Math.min(limit * 3, 300) : limit;
5930
+ const [hybridResults, lessons] = await Promise.all([searchFn(data.query, overFetchLimit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
5931
+ const compact = (filterAgentId ? hybridResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : hybridResults.slice(0, limit)).map((r) => ({
5628
5932
  obsId: r.observation.id,
5629
5933
  sessionId: r.sessionId,
5630
5934
  title: r.observation.title,
@@ -5635,14 +5939,42 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
5635
5939
  recordAccessBatch(kv, compact.map((r) => r.obsId));
5636
5940
  logger.info("Smart search compact", {
5637
5941
  query: data.query,
5638
- results: compact.length
5942
+ results: compact.length,
5943
+ lessons: lessons.length
5639
5944
  });
5640
- return {
5945
+ const response = {
5641
5946
  mode: "compact",
5642
5947
  results: compact
5643
5948
  };
5949
+ if (includeLessons) response.lessons = lessons;
5950
+ return response;
5644
5951
  });
5645
5952
  }
5953
+ async function recallLessons(sdk, query, limit, project) {
5954
+ try {
5955
+ const result = await sdk.trigger({
5956
+ function_id: "mem::lesson-recall",
5957
+ payload: {
5958
+ query,
5959
+ limit,
5960
+ project
5961
+ }
5962
+ });
5963
+ if (!result?.success || !Array.isArray(result.lessons)) return [];
5964
+ return result.lessons.map((l) => ({
5965
+ lessonId: l.id,
5966
+ content: l.content.length > LESSON_CONTENT_PREVIEW_CHARS ? l.content.slice(0, LESSON_CONTENT_PREVIEW_CHARS) + "…" : l.content,
5967
+ confidence: l.confidence,
5968
+ score: l.score ?? l.confidence,
5969
+ createdAt: l.createdAt,
5970
+ project: l.project,
5971
+ tags: l.tags ?? []
5972
+ }));
5973
+ } catch (err) {
5974
+ logger.warn("Smart search: mem::lesson-recall failed; returning empty lesson list", { error: err instanceof Error ? err.message : String(err) });
5975
+ return [];
5976
+ }
5977
+ }
5646
5978
  async function findObservation$1(kv, obsId, sessionIdHint) {
5647
5979
  if (sessionIdHint) {
5648
5980
  const obs = await kv.get(KV.observations(sessionIdHint), obsId).catch(() => null);
@@ -5753,7 +6085,7 @@ function registerAutoForgetFunction(sdk, kv) {
5753
6085
  sdk.registerFunction("mem::auto-forget", async (data) => {
5754
6086
  const dryRun = data?.dryRun ?? false;
5755
6087
  const now = Date.now();
5756
- const { decrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
6088
+ const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
5757
6089
  const result = {
5758
6090
  ttlExpired: [],
5759
6091
  contradictions: [],
@@ -5775,6 +6107,8 @@ function registerAutoForgetFunction(sdk, kv) {
5775
6107
  timestamp: mem.forgetAfter
5776
6108
  });
5777
6109
  await deleteAccessLog(kv, mem.id);
6110
+ getSearchIndex().remove(mem.id);
6111
+ vectorIndexRemove(mem.id);
5778
6112
  }
5779
6113
  }
5780
6114
  }
@@ -5852,10 +6186,13 @@ function registerAutoForgetFunction(sdk, kv) {
5852
6186
  sessionId: sessions[i].id,
5853
6187
  timestamp: obs.timestamp
5854
6188
  });
6189
+ getSearchIndex().remove(obs.id);
6190
+ vectorIndexRemove(obs.id);
5855
6191
  }
5856
6192
  }
5857
6193
  }
5858
6194
  }
6195
+ if (!dryRun && (result.ttlExpired.length > 0 || result.lowValueObs.length > 0)) await flushIndexSave();
5859
6196
  logger.info("Auto-forget complete", {
5860
6197
  ttlExpired: result.ttlExpired.length,
5861
6198
  contradictions: result.contradictions.length,
@@ -6002,7 +6339,9 @@ function registerExportImportFunction(sdk, kv) {
6002
6339
  "0.9.17",
6003
6340
  "0.9.18",
6004
6341
  "0.9.19",
6005
- "0.9.20"
6342
+ "0.9.20",
6343
+ "0.9.21",
6344
+ "0.9.22"
6006
6345
  ]).has(importData.version)) return {
6007
6346
  success: false,
6008
6347
  error: `Unsupported export version: ${importData.version}`
@@ -6528,12 +6867,12 @@ function parseGraphXml(xml, observationIds) {
6528
6867
  const nodes = [];
6529
6868
  const edges = [];
6530
6869
  const now = (/* @__PURE__ */ new Date()).toISOString();
6531
- const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/entity>/g;
6870
+ const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*?(?:\/>|>([\s\S]*?)<\/entity>)/g;
6532
6871
  let match;
6533
6872
  while ((match = entityRegex.exec(xml)) !== null) {
6534
6873
  const type = match[1];
6535
6874
  const name = match[2];
6536
- const propsBlock = match[3];
6875
+ const propsBlock = match[3] ?? "";
6537
6876
  const properties = {};
6538
6877
  const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
6539
6878
  let propMatch;
@@ -7061,8 +7400,11 @@ function registerGovernanceFunction(sdk, kv) {
7061
7400
  for (const id of data.memoryIds) if (await kv.get(KV.memories, id)) {
7062
7401
  await kv.delete(KV.memories, id);
7063
7402
  await deleteAccessLog(kv, id);
7403
+ getSearchIndex().remove(id);
7404
+ vectorIndexRemove(id);
7064
7405
  deleted++;
7065
7406
  }
7407
+ if (deleted > 0) await flushIndexSave();
7066
7408
  await recordAudit(kv, "delete", "mem::governance-delete", data.memoryIds, {
7067
7409
  reason: data.reason || "manual deletion",
7068
7410
  deleted
@@ -7115,6 +7457,8 @@ function registerGovernanceFunction(sdk, kv) {
7115
7457
  (await Promise.allSettled(batch.map(async (mem) => {
7116
7458
  await kv.delete(KV.memories, mem.id);
7117
7459
  await deleteAccessLog(kv, mem.id);
7460
+ getSearchIndex().remove(mem.id);
7461
+ vectorIndexRemove(mem.id);
7118
7462
  }))).forEach((result, j) => {
7119
7463
  const mem = batch[j];
7120
7464
  if (result.status === "fulfilled") successfulIds.push(mem.id);
@@ -7130,6 +7474,7 @@ function registerGovernanceFunction(sdk, kv) {
7130
7474
  }
7131
7475
  });
7132
7476
  }
7477
+ if (successfulIds.length > 0) await flushIndexSave();
7133
7478
  await safeAudit(kv, "delete", "mem::governance-bulk", successfulIds, {
7134
7479
  filter: data,
7135
7480
  deleted: successfulIds.length,
@@ -9704,6 +10049,12 @@ const ALL_CATEGORIES = [
9704
10049
  "signals",
9705
10050
  "sessions",
9706
10051
  "memories",
10052
+ "lessons",
10053
+ "summaries",
10054
+ "semantic",
10055
+ "procedural",
10056
+ "crystals",
10057
+ "insights",
9707
10058
  "mesh"
9708
10059
  ];
9709
10060
  const TWENTY_FOUR_HOURS_MS = 1440 * 60 * 1e3;
@@ -9936,6 +10287,133 @@ function registerDiagnosticsFunction(sdk, kv) {
9936
10287
  fixable: false
9937
10288
  });
9938
10289
  }
10290
+ if (categories.includes("lessons")) {
10291
+ const lessons = await kv.list(KV.lessons);
10292
+ const live = lessons.filter((l) => !l.deleted);
10293
+ let lessonIssues = 0;
10294
+ for (const l of live) if (!Number.isFinite(l.confidence) || l.confidence < 0 || l.confidence > 1) {
10295
+ checks.push({
10296
+ name: `lesson-bad-confidence:${l.id}`,
10297
+ category: "lessons",
10298
+ status: "warn",
10299
+ message: `Lesson ${l.id} has confidence ${l.confidence} (expected finite number in 0..1)`,
10300
+ fixable: false
10301
+ });
10302
+ lessonIssues++;
10303
+ }
10304
+ if (lessonIssues === 0) checks.push({
10305
+ name: "lessons-ok",
10306
+ category: "lessons",
10307
+ status: "pass",
10308
+ message: `All ${live.length} lessons are healthy (${lessons.length - live.length} tombstoned)`,
10309
+ fixable: false
10310
+ });
10311
+ }
10312
+ if (categories.includes("summaries")) {
10313
+ const summaries = await kv.list(KV.summaries);
10314
+ let summaryIssues = 0;
10315
+ for (const s of summaries) if (typeof s.title !== "string" || s.title.trim().length === 0) {
10316
+ checks.push({
10317
+ name: `summary-missing-title:${s.sessionId}`,
10318
+ category: "summaries",
10319
+ status: "warn",
10320
+ message: `Summary for session ${s.sessionId} has no title`,
10321
+ fixable: false
10322
+ });
10323
+ summaryIssues++;
10324
+ }
10325
+ if (summaryIssues === 0) checks.push({
10326
+ name: "summaries-ok",
10327
+ category: "summaries",
10328
+ status: "pass",
10329
+ message: `All ${summaries.length} session summaries are consistent`,
10330
+ fixable: false
10331
+ });
10332
+ }
10333
+ if (categories.includes("semantic")) {
10334
+ const semantic = await kv.list(KV.semantic);
10335
+ let semanticIssues = 0;
10336
+ for (const s of semantic) if (!Number.isFinite(s.confidence) || s.confidence < 0 || s.confidence > 1) {
10337
+ checks.push({
10338
+ name: `semantic-bad-confidence:${s.id}`,
10339
+ category: "semantic",
10340
+ status: "warn",
10341
+ message: `Semantic fact ${s.id} has confidence ${s.confidence} (expected finite number in 0..1)`,
10342
+ fixable: false
10343
+ });
10344
+ semanticIssues++;
10345
+ }
10346
+ if (semanticIssues === 0) checks.push({
10347
+ name: "semantic-ok",
10348
+ category: "semantic",
10349
+ status: "pass",
10350
+ message: `All ${semantic.length} semantic memories are consistent`,
10351
+ fixable: false
10352
+ });
10353
+ }
10354
+ if (categories.includes("procedural")) {
10355
+ const procedural = await kv.list(KV.procedural);
10356
+ let proceduralIssues = 0;
10357
+ for (const p of procedural) if (!Array.isArray(p.steps) || p.steps.length === 0) {
10358
+ checks.push({
10359
+ name: `procedural-empty-steps:${p.id}`,
10360
+ category: "procedural",
10361
+ status: "warn",
10362
+ message: `Procedural memory "${p.name}" (${p.id}) has no steps`,
10363
+ fixable: false
10364
+ });
10365
+ proceduralIssues++;
10366
+ }
10367
+ if (proceduralIssues === 0) checks.push({
10368
+ name: "procedural-ok",
10369
+ category: "procedural",
10370
+ status: "pass",
10371
+ message: `All ${procedural.length} procedural memories are consistent`,
10372
+ fixable: false
10373
+ });
10374
+ }
10375
+ if (categories.includes("crystals")) {
10376
+ const crystals = await kv.list(KV.crystals);
10377
+ let crystalIssues = 0;
10378
+ for (const c of crystals) if (typeof c.narrative !== "string" || c.narrative.trim().length === 0) {
10379
+ checks.push({
10380
+ name: `crystal-empty-narrative:${c.id}`,
10381
+ category: "crystals",
10382
+ status: "warn",
10383
+ message: `Crystal ${c.id} has empty narrative`,
10384
+ fixable: false
10385
+ });
10386
+ crystalIssues++;
10387
+ }
10388
+ if (crystalIssues === 0) checks.push({
10389
+ name: "crystals-ok",
10390
+ category: "crystals",
10391
+ status: "pass",
10392
+ message: `All ${crystals.length} crystals are consistent`,
10393
+ fixable: false
10394
+ });
10395
+ }
10396
+ if (categories.includes("insights")) {
10397
+ const insights = await kv.list(KV.insights);
10398
+ let insightIssues = 0;
10399
+ for (const i of insights) if (!Number.isFinite(i.confidence) || i.confidence < 0 || i.confidence > 1) {
10400
+ checks.push({
10401
+ name: `insight-bad-confidence:${i.id}`,
10402
+ category: "insights",
10403
+ status: "warn",
10404
+ message: `Insight ${i.id} has confidence ${i.confidence} (expected finite number in 0..1)`,
10405
+ fixable: false
10406
+ });
10407
+ insightIssues++;
10408
+ }
10409
+ if (insightIssues === 0) checks.push({
10410
+ name: "insights-ok",
10411
+ category: "insights",
10412
+ status: "pass",
10413
+ message: `All ${insights.length} insights are consistent`,
10414
+ fixable: false
10415
+ });
10416
+ }
9939
10417
  if (categories.includes("mesh")) {
9940
10418
  const peers = await kv.list(KV.mesh);
9941
10419
  let meshIssues = 0;
@@ -12359,7 +12837,7 @@ function registerRetentionFunctions(sdk, kv) {
12359
12837
  const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
12360
12838
  const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
12361
12839
  const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
12362
- const { decrementImageRef } = await import("./image-refs-R3tin9MR.mjs");
12840
+ const { decrementImageRef } = await import("./image-refs-CJS5B9Gq.mjs");
12363
12841
  const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
12364
12842
  if (data?.dryRun) return {
12365
12843
  success: true,
@@ -12396,6 +12874,8 @@ function registerRetentionFunctions(sdk, kv) {
12396
12874
  await kv.delete(scope, candidate.memoryId);
12397
12875
  await kv.delete(KV.retentionScores, candidate.memoryId);
12398
12876
  await deleteAccessLog(kv, candidate.memoryId);
12877
+ getSearchIndex().remove(candidate.memoryId);
12878
+ vectorIndexRemove(candidate.memoryId);
12399
12879
  evicted++;
12400
12880
  evictedIds.push(candidate.memoryId);
12401
12881
  if (resolvedSource === "semantic") evictedSemantic++;
@@ -12403,13 +12883,16 @@ function registerRetentionFunctions(sdk, kv) {
12403
12883
  } catch {
12404
12884
  continue;
12405
12885
  }
12406
- if (evicted > 0) await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
12407
- threshold,
12408
- evicted,
12409
- evictedEpisodic,
12410
- evictedSemantic,
12411
- reason: "retention score below threshold"
12412
- });
12886
+ if (evicted > 0) {
12887
+ await flushIndexSave();
12888
+ await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
12889
+ threshold,
12890
+ evicted,
12891
+ evictedEpisodic,
12892
+ evictedSemantic,
12893
+ reason: "retention score below threshold"
12894
+ });
12895
+ }
12413
12896
  logger.info("Retention-based eviction complete", {
12414
12897
  evicted,
12415
12898
  evictedEpisodic,
@@ -13314,38 +13797,253 @@ function renderViewerDocument() {
13314
13797
  }
13315
13798
 
13316
13799
  //#endregion
13317
- //#region src/triggers/api.ts
13318
- function parseOptionalInt(raw) {
13319
- if (raw === void 0 || raw === null || raw === "") return void 0;
13320
- const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
13321
- return Number.isFinite(n) ? n : void 0;
13322
- }
13323
- function checkAuth(req, secret) {
13324
- if (!secret) return null;
13325
- const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
13326
- if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
13327
- status_code: 401,
13328
- body: { error: "unauthorized" }
13329
- };
13800
+ //#region src/viewer/server.ts
13801
+ function loadViewerFavicon() {
13802
+ const base = dirname(fileURLToPath(import.meta.url));
13803
+ const candidates = [
13804
+ join(base, "..", "src", "viewer", "favicon.svg"),
13805
+ join(base, "..", "viewer", "favicon.svg"),
13806
+ join(base, "viewer", "favicon.svg")
13807
+ ];
13808
+ for (const path of candidates) try {
13809
+ return readFileSync(path);
13810
+ } catch {}
13330
13811
  return null;
13331
13812
  }
13332
- function requireConfiguredSecret(secret, feature) {
13333
- if (secret) return null;
13813
+ 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());
13814
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
13815
+ function buildAllowedHosts(origins, listenPort) {
13816
+ const hosts = /* @__PURE__ */ new Set();
13817
+ for (const o of origins) try {
13818
+ const parsed = new URL(o);
13819
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
13820
+ } catch {}
13821
+ hosts.add(`localhost:${listenPort}`);
13822
+ hosts.add(`127.0.0.1:${listenPort}`);
13823
+ hosts.add(`[::1]:${listenPort}`);
13824
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
13825
+ return hosts;
13826
+ }
13827
+ function isHostAllowed(headerHost, allowed) {
13828
+ if (typeof headerHost !== "string") return false;
13829
+ const lower = headerHost.toLowerCase().trim();
13830
+ if (!lower) return false;
13831
+ return allowed.has(lower);
13832
+ }
13833
+ function corsHeaders(req) {
13834
+ const origin = req.headers.origin || "";
13835
+ const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
13334
13836
  return {
13335
- status_code: 503,
13336
- body: { error: `${feature} requires AGENTMEMORY_SECRET` }
13837
+ "Access-Control-Allow-Origin": allowed,
13838
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
13839
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
13840
+ Vary: "Origin"
13337
13841
  };
13338
13842
  }
13339
- function flagDisabledResponse(opts) {
13340
- return {
13341
- status_code: 503,
13342
- body: opts
13843
+ function json(res, status, data, req) {
13844
+ const body = JSON.stringify(data);
13845
+ const cors = req ? corsHeaders(req) : {
13846
+ "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
13847
+ Vary: "Origin"
13343
13848
  };
13849
+ res.writeHead(status, {
13850
+ ...cors,
13851
+ "Content-Type": "application/json"
13852
+ });
13853
+ res.end(body);
13344
13854
  }
13345
- function graphDisabledResponse() {
13346
- return flagDisabledResponse({
13347
- error: "Knowledge graph not enabled",
13348
- flag: "GRAPH_EXTRACTION_ENABLED",
13855
+ function readBody(req) {
13856
+ return new Promise((resolve, reject) => {
13857
+ let data = "";
13858
+ let size = 0;
13859
+ req.on("data", (chunk) => {
13860
+ size += chunk.length;
13861
+ if (size > 1e6) {
13862
+ req.destroy();
13863
+ reject(/* @__PURE__ */ new Error("too large"));
13864
+ return;
13865
+ }
13866
+ data += chunk.toString();
13867
+ });
13868
+ req.on("end", () => resolve(data));
13869
+ req.on("error", reject);
13870
+ });
13871
+ }
13872
+ const MAX_VIEWER_PORT_RETRIES = 10;
13873
+ let boundViewerPort = null;
13874
+ let viewerSkipped = false;
13875
+ function getBoundViewerPort() {
13876
+ return boundViewerPort;
13877
+ }
13878
+ function getViewerSkipped() {
13879
+ return viewerSkipped;
13880
+ }
13881
+ function startViewerServer(port, _kv, _sdk, secret, restPort) {
13882
+ boundViewerPort = null;
13883
+ viewerSkipped = false;
13884
+ const resolvedRestPort = restPort ?? port - 2;
13885
+ const requestedPort = port;
13886
+ let allowedHosts = null;
13887
+ const server = createServer(async (req, res) => {
13888
+ if (!allowedHosts) {
13889
+ const addr = server.address();
13890
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
13891
+ }
13892
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
13893
+ res.writeHead(403, { "Content-Type": "text/plain" });
13894
+ res.end("forbidden host");
13895
+ return;
13896
+ }
13897
+ const raw = req.url || "/";
13898
+ const qIdx = raw.indexOf("?");
13899
+ const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
13900
+ const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
13901
+ const method = req.method || "GET";
13902
+ if (method === "OPTIONS") {
13903
+ res.writeHead(204, {
13904
+ ...corsHeaders(req),
13905
+ "Access-Control-Max-Age": "86400"
13906
+ });
13907
+ res.end();
13908
+ return;
13909
+ }
13910
+ if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
13911
+ const rendered = renderViewerDocument();
13912
+ if (rendered.found) {
13913
+ res.writeHead(200, {
13914
+ "Content-Type": "text/html; charset=utf-8",
13915
+ "Content-Security-Policy": rendered.csp,
13916
+ "Cache-Control": "no-cache"
13917
+ });
13918
+ res.end(rendered.html);
13919
+ return;
13920
+ }
13921
+ res.writeHead(404, { "Content-Type": "text/plain" });
13922
+ res.end("viewer not found");
13923
+ return;
13924
+ }
13925
+ if (method === "GET" && pathname === "/favicon.svg") {
13926
+ const favicon = loadViewerFavicon();
13927
+ if (favicon) {
13928
+ res.writeHead(200, {
13929
+ "Content-Type": "image/svg+xml",
13930
+ "Cache-Control": "public, max-age=3600"
13931
+ });
13932
+ res.end(favicon);
13933
+ return;
13934
+ }
13935
+ res.writeHead(404, { "Content-Type": "text/plain" });
13936
+ res.end("favicon not found");
13937
+ return;
13938
+ }
13939
+ try {
13940
+ await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
13941
+ } catch (err) {
13942
+ console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
13943
+ json(res, 502, { error: "upstream error" }, req);
13944
+ }
13945
+ });
13946
+ let attempt = 0;
13947
+ let currentPort = requestedPort;
13948
+ const tryListen = () => {
13949
+ server.listen(currentPort, "127.0.0.1");
13950
+ };
13951
+ server.on("listening", () => {
13952
+ const addr = server.address();
13953
+ boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
13954
+ viewerSkipped = false;
13955
+ if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
13956
+ else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
13957
+ });
13958
+ server.on("error", (err) => {
13959
+ if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
13960
+ attempt++;
13961
+ currentPort = requestedPort + attempt;
13962
+ setImmediate(tryListen);
13963
+ return;
13964
+ }
13965
+ if (err.code === "EADDRINUSE") {
13966
+ boundViewerPort = null;
13967
+ viewerSkipped = true;
13968
+ console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
13969
+ } else {
13970
+ boundViewerPort = null;
13971
+ viewerSkipped = true;
13972
+ console.error(`[agentmemory] Viewer error:`, err.message);
13973
+ }
13974
+ });
13975
+ tryListen();
13976
+ return server;
13977
+ }
13978
+ async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
13979
+ const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
13980
+ const headers = {};
13981
+ if (secret) headers["Authorization"] = `Bearer ${secret}`;
13982
+ const ct = req.headers["content-type"];
13983
+ if (ct) headers["Content-Type"] = ct;
13984
+ let body;
13985
+ if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
13986
+ const controller = new AbortController();
13987
+ const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
13988
+ let upstream;
13989
+ try {
13990
+ upstream = await fetch(upstreamUrl, {
13991
+ method,
13992
+ headers,
13993
+ body: body || void 0,
13994
+ signal: controller.signal
13995
+ });
13996
+ clearTimeout(fetchTimeout);
13997
+ } catch (err) {
13998
+ clearTimeout(fetchTimeout);
13999
+ if (err instanceof Error && err.name === "AbortError") {
14000
+ json(res, 504, { error: "upstream timeout" }, req);
14001
+ return;
14002
+ }
14003
+ throw err;
14004
+ }
14005
+ const cors = corsHeaders(req);
14006
+ const responseBody = await upstream.text();
14007
+ const responseHeaders = { ...cors };
14008
+ const upstreamCt = upstream.headers.get("content-type");
14009
+ if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
14010
+ res.writeHead(upstream.status, responseHeaders);
14011
+ res.end(responseBody);
14012
+ }
14013
+
14014
+ //#endregion
14015
+ //#region src/triggers/api.ts
14016
+ function parseOptionalInt(raw) {
14017
+ if (raw === void 0 || raw === null || raw === "") return void 0;
14018
+ const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
14019
+ return Number.isFinite(n) ? n : void 0;
14020
+ }
14021
+ function checkAuth(req, secret) {
14022
+ if (!secret) return null;
14023
+ const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
14024
+ if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
14025
+ status_code: 401,
14026
+ body: { error: "unauthorized" }
14027
+ };
14028
+ return null;
14029
+ }
14030
+ function requireConfiguredSecret(secret, feature) {
14031
+ if (secret) return null;
14032
+ return {
14033
+ status_code: 503,
14034
+ body: { error: `${feature} requires AGENTMEMORY_SECRET` }
14035
+ };
14036
+ }
14037
+ function flagDisabledResponse(opts) {
14038
+ return {
14039
+ status_code: 503,
14040
+ body: opts
14041
+ };
14042
+ }
14043
+ function graphDisabledResponse() {
14044
+ return flagDisabledResponse({
14045
+ error: "Knowledge graph not enabled",
14046
+ flag: "GRAPH_EXTRACTION_ENABLED",
13349
14047
  enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and restart. Requires an LLM provider key.",
13350
14048
  docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
13351
14049
  });
@@ -13398,7 +14096,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13398
14096
  status_code: 200,
13399
14097
  body: {
13400
14098
  status: "ok",
13401
- service: "agentmemory"
14099
+ service: "agentmemory",
14100
+ viewerPort: getBoundViewerPort(),
14101
+ viewerSkipped: getViewerSkipped()
13402
14102
  }
13403
14103
  }));
13404
14104
  sdk.registerTrigger({
@@ -13493,7 +14193,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13493
14193
  version: VERSION,
13494
14194
  health: health || null,
13495
14195
  functionMetrics,
13496
- circuitBreaker
14196
+ circuitBreaker,
14197
+ viewerPort: getBoundViewerPort(),
14198
+ viewerSkipped: getViewerSkipped()
13497
14199
  }
13498
14200
  };
13499
14201
  });
@@ -13746,13 +14448,18 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13746
14448
  status_code: 400,
13747
14449
  body: { error: "sessionId, project, and cwd are required non-empty strings" }
13748
14450
  };
14451
+ const title = typeof body.title === "string" ? body.title.trim() : void 0;
14452
+ const agentId = (typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim().slice(0, 128) : void 0) ?? getAgentId();
13749
14453
  const session = {
13750
14454
  id: sessionId,
13751
14455
  project,
13752
14456
  cwd,
13753
14457
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
13754
14458
  status: "active",
13755
- observationCount: 0
14459
+ observationCount: 0,
14460
+ ...title ? { summary: title.slice(0, 200) } : {},
14461
+ ...title ? { firstPrompt: title.slice(0, 200) } : {},
14462
+ ...agentId ? { agentId } : {}
13756
14463
  };
13757
14464
  await kv.set(KV.sessions, sessionId, session);
13758
14465
  return {
@@ -13939,9 +14646,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13939
14646
  sdk.registerFunction("api::sessions", async (req) => {
13940
14647
  const authErr = checkAuth(req, secret);
13941
14648
  if (authErr) return authErr;
14649
+ const sessions = await kv.list(KV.sessions);
14650
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
14651
+ const wildcardAgent = normalizedAgentId === "*";
14652
+ const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
13942
14653
  return {
13943
14654
  status_code: 200,
13944
- body: { sessions: await kv.list(KV.sessions) }
14655
+ body: { sessions: filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions }
13945
14656
  };
13946
14657
  });
13947
14658
  sdk.registerTrigger({
@@ -13960,9 +14671,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13960
14671
  status_code: 400,
13961
14672
  body: { error: "sessionId required" }
13962
14673
  };
14674
+ const observations = await kv.list(KV.observations(sessionId));
14675
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
14676
+ const wildcardAgent = normalizedAgentId === "*";
14677
+ const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
13963
14678
  return {
13964
14679
  status_code: 200,
13965
- body: { observations: await kv.list(KV.observations(sessionId)) }
14680
+ body: { observations: filterAgentId ? observations.filter((o) => o.agentId === filterAgentId) : observations }
13966
14681
  };
13967
14682
  });
13968
14683
  sdk.registerTrigger({
@@ -14238,11 +14953,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14238
14953
  sdk.registerFunction("api::export", async (req) => {
14239
14954
  const authErr = checkAuth(req, secret);
14240
14955
  if (authErr) return authErr;
14956
+ const rawMax = req.query_params?.["maxSessions"];
14957
+ const rawOffset = req.query_params?.["offset"];
14958
+ const payload = {};
14959
+ if (typeof rawMax === "string") {
14960
+ const n = Number(rawMax);
14961
+ if (Number.isInteger(n) && n > 0) payload.maxSessions = n;
14962
+ }
14963
+ if (typeof rawOffset === "string") {
14964
+ const n = Number(rawOffset);
14965
+ if (Number.isInteger(n) && n >= 0) payload.offset = n;
14966
+ }
14241
14967
  return {
14242
14968
  status_code: 200,
14243
14969
  body: await sdk.trigger({
14244
14970
  function_id: "mem::export",
14245
- payload: {}
14971
+ payload
14246
14972
  })
14247
14973
  };
14248
14974
  });
@@ -14728,9 +15454,35 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14728
15454
  const authErr = checkAuth(req, secret);
14729
15455
  if (authErr) return authErr;
14730
15456
  const memories = await kv.list(KV.memories);
15457
+ const latest = req.query_params?.["latest"] === "true";
15458
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
15459
+ const wildcardAgent = normalizedAgentId === "*";
15460
+ const explicitAgentId = normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0;
15461
+ const includeOrphans = req.query_params?.["includeOrphans"] === "true";
15462
+ const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
15463
+ let filtered = latest ? memories.filter((m) => m.isLatest) : memories;
15464
+ if (filterAgentId) filtered = filtered.filter((m) => m.agentId === filterAgentId || includeOrphans && m.agentId === void 0);
15465
+ if (req.query_params?.["count"] === "true") return {
15466
+ status_code: 200,
15467
+ body: {
15468
+ total: filtered.length,
15469
+ latestCount: filtered.filter((m) => m.isLatest).length
15470
+ }
15471
+ };
15472
+ const rawLimit = req.query_params?.["limit"];
15473
+ const rawOffset = req.query_params?.["offset"];
15474
+ const parsedLimit = typeof rawLimit === "string" ? Number(rawLimit) : NaN;
15475
+ const parsedOffset = typeof rawOffset === "string" ? Number(rawOffset) : NaN;
15476
+ const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 5e3) : void 0;
15477
+ const offset = Number.isInteger(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
14731
15478
  return {
14732
15479
  status_code: 200,
14733
- body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
15480
+ body: {
15481
+ memories: limit !== void 0 ? filtered.slice(offset, offset + limit) : filtered,
15482
+ total: filtered.length,
15483
+ offset,
15484
+ limit: limit ?? null
15485
+ }
14734
15486
  };
14735
15487
  });
14736
15488
  sdk.registerTrigger({
@@ -18343,201 +19095,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
18343
19095
  });
18344
19096
  }
18345
19097
 
18346
- //#endregion
18347
- //#region src/viewer/server.ts
18348
- function loadViewerFavicon() {
18349
- const base = dirname(fileURLToPath(import.meta.url));
18350
- const candidates = [
18351
- join(base, "..", "src", "viewer", "favicon.svg"),
18352
- join(base, "..", "viewer", "favicon.svg"),
18353
- join(base, "viewer", "favicon.svg")
18354
- ];
18355
- for (const path of candidates) try {
18356
- return readFileSync(path);
18357
- } catch {}
18358
- return null;
18359
- }
18360
- 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());
18361
- const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
18362
- function buildAllowedHosts(origins, listenPort) {
18363
- const hosts = /* @__PURE__ */ new Set();
18364
- for (const o of origins) try {
18365
- const parsed = new URL(o);
18366
- if (parsed.host) hosts.add(parsed.host.toLowerCase());
18367
- } catch {}
18368
- hosts.add(`localhost:${listenPort}`);
18369
- hosts.add(`127.0.0.1:${listenPort}`);
18370
- hosts.add(`[::1]:${listenPort}`);
18371
- for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
18372
- return hosts;
18373
- }
18374
- function isHostAllowed(headerHost, allowed) {
18375
- if (typeof headerHost !== "string") return false;
18376
- const lower = headerHost.toLowerCase().trim();
18377
- if (!lower) return false;
18378
- return allowed.has(lower);
18379
- }
18380
- function corsHeaders(req) {
18381
- const origin = req.headers.origin || "";
18382
- const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
18383
- return {
18384
- "Access-Control-Allow-Origin": allowed,
18385
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
18386
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
18387
- Vary: "Origin"
18388
- };
18389
- }
18390
- function json(res, status, data, req) {
18391
- const body = JSON.stringify(data);
18392
- const cors = req ? corsHeaders(req) : {
18393
- "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
18394
- Vary: "Origin"
18395
- };
18396
- res.writeHead(status, {
18397
- ...cors,
18398
- "Content-Type": "application/json"
18399
- });
18400
- res.end(body);
18401
- }
18402
- function readBody(req) {
18403
- return new Promise((resolve, reject) => {
18404
- let data = "";
18405
- let size = 0;
18406
- req.on("data", (chunk) => {
18407
- size += chunk.length;
18408
- if (size > 1e6) {
18409
- req.destroy();
18410
- reject(/* @__PURE__ */ new Error("too large"));
18411
- return;
18412
- }
18413
- data += chunk.toString();
18414
- });
18415
- req.on("end", () => resolve(data));
18416
- req.on("error", reject);
18417
- });
18418
- }
18419
- const MAX_VIEWER_PORT_RETRIES = 10;
18420
- function startViewerServer(port, _kv, _sdk, secret, restPort) {
18421
- const resolvedRestPort = restPort ?? port - 2;
18422
- const requestedPort = port;
18423
- let allowedHosts = null;
18424
- const server = createServer(async (req, res) => {
18425
- if (!allowedHosts) {
18426
- const addr = server.address();
18427
- allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
18428
- }
18429
- if (!isHostAllowed(req.headers.host, allowedHosts)) {
18430
- res.writeHead(403, { "Content-Type": "text/plain" });
18431
- res.end("forbidden host");
18432
- return;
18433
- }
18434
- const raw = req.url || "/";
18435
- const qIdx = raw.indexOf("?");
18436
- const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
18437
- const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
18438
- const method = req.method || "GET";
18439
- if (method === "OPTIONS") {
18440
- res.writeHead(204, {
18441
- ...corsHeaders(req),
18442
- "Access-Control-Max-Age": "86400"
18443
- });
18444
- res.end();
18445
- return;
18446
- }
18447
- if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
18448
- const rendered = renderViewerDocument();
18449
- if (rendered.found) {
18450
- res.writeHead(200, {
18451
- "Content-Type": "text/html; charset=utf-8",
18452
- "Content-Security-Policy": rendered.csp,
18453
- "Cache-Control": "no-cache"
18454
- });
18455
- res.end(rendered.html);
18456
- return;
18457
- }
18458
- res.writeHead(404, { "Content-Type": "text/plain" });
18459
- res.end("viewer not found");
18460
- return;
18461
- }
18462
- if (method === "GET" && pathname === "/favicon.svg") {
18463
- const favicon = loadViewerFavicon();
18464
- if (favicon) {
18465
- res.writeHead(200, {
18466
- "Content-Type": "image/svg+xml",
18467
- "Cache-Control": "public, max-age=3600"
18468
- });
18469
- res.end(favicon);
18470
- return;
18471
- }
18472
- res.writeHead(404, { "Content-Type": "text/plain" });
18473
- res.end("favicon not found");
18474
- return;
18475
- }
18476
- try {
18477
- await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
18478
- } catch (err) {
18479
- console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
18480
- json(res, 502, { error: "upstream error" }, req);
18481
- }
18482
- });
18483
- let attempt = 0;
18484
- let currentPort = requestedPort;
18485
- const tryListen = () => {
18486
- server.listen(currentPort, "127.0.0.1");
18487
- };
18488
- server.on("listening", () => {
18489
- if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
18490
- else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
18491
- });
18492
- server.on("error", (err) => {
18493
- if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
18494
- attempt++;
18495
- currentPort = requestedPort + attempt;
18496
- setImmediate(tryListen);
18497
- return;
18498
- }
18499
- if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
18500
- else console.error(`[agentmemory] Viewer error:`, err.message);
18501
- });
18502
- tryListen();
18503
- return server;
18504
- }
18505
- async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
18506
- const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
18507
- const headers = {};
18508
- if (secret) headers["Authorization"] = `Bearer ${secret}`;
18509
- const ct = req.headers["content-type"];
18510
- if (ct) headers["Content-Type"] = ct;
18511
- let body;
18512
- if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
18513
- const controller = new AbortController();
18514
- const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
18515
- let upstream;
18516
- try {
18517
- upstream = await fetch(upstreamUrl, {
18518
- method,
18519
- headers,
18520
- body: body || void 0,
18521
- signal: controller.signal
18522
- });
18523
- clearTimeout(fetchTimeout);
18524
- } catch (err) {
18525
- clearTimeout(fetchTimeout);
18526
- if (err instanceof Error && err.name === "AbortError") {
18527
- json(res, 504, { error: "upstream timeout" }, req);
18528
- return;
18529
- }
18530
- throw err;
18531
- }
18532
- const cors = corsHeaders(req);
18533
- const responseBody = await upstream.text();
18534
- const responseHeaders = { ...cors };
18535
- const upstreamCt = upstream.headers.get("content-type");
18536
- if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
18537
- res.writeHead(upstream.status, responseHeaders);
18538
- res.end(responseBody);
18539
- }
18540
-
18541
19098
  //#endregion
18542
19099
  //#region src/eval/metrics-store.ts
18543
19100
  var MetricsStore = class {
@@ -18675,6 +19232,21 @@ function initMetrics(getMeter) {
18675
19232
 
18676
19233
  //#endregion
18677
19234
  //#region src/index.ts
19235
+ function workerPidfilePath() {
19236
+ return join(homedir(), ".agentmemory", "worker.pid");
19237
+ }
19238
+ function writeWorkerPidfile() {
19239
+ try {
19240
+ const p = workerPidfilePath();
19241
+ mkdirSync(dirname(p), { recursive: true });
19242
+ writeFileSync(p, `${process.pid}\n`, { encoding: "utf-8" });
19243
+ } catch {}
19244
+ }
19245
+ function clearWorkerPidfile() {
19246
+ try {
19247
+ unlinkSync(workerPidfilePath());
19248
+ } catch {}
19249
+ }
18678
19250
  function hasGetMeter(sdk) {
18679
19251
  return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
18680
19252
  }
@@ -18715,6 +19287,7 @@ async function main() {
18715
19287
  framework: "iii-sdk"
18716
19288
  }
18717
19289
  });
19290
+ writeWorkerPidfile();
18718
19291
  const kv = new StateKV(sdk);
18719
19292
  const secret = getEnvVar("AGENTMEMORY_SECRET");
18720
19293
  const metricsStore = new MetricsStore(kv);
@@ -18810,6 +19383,7 @@ async function main() {
18810
19383
  registerMcpEndpoints(sdk, kv, secret);
18811
19384
  const healthMonitor = registerHealthMonitor(sdk, kv);
18812
19385
  const indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
19386
+ setIndexPersistence(indexPersistence);
18813
19387
  const loaded = await indexPersistence.load().catch((err) => {
18814
19388
  console.warn(`[agentmemory] Failed to load persisted index:`, err);
18815
19389
  return null;
@@ -18827,23 +19401,22 @@ async function main() {
18827
19401
  if (mismatches.length > 0) {
18828
19402
  const sample = mismatches.slice(0, 5).map((m) => `${m.obsId} (dim=${m.dim})`).join(", ");
18829
19403
  const distinct = Array.from(seenDimensions).sort((a, b) => a - b).join(", ");
18830
- 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.`);
19404
+ if (isDropStaleIndexEnabled()) 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.`);
18831
19405
  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.`);
18832
19406
  } else {
18833
19407
  vectorIndex.restoreFrom(loaded.vector);
18834
19408
  bootLog(`Loaded persisted vector index (${vectorIndex.size} vectors)`);
18835
19409
  }
18836
19410
  }
18837
- if (bm25Index.size === 0) {
18838
- const indexCount = await rebuildIndex(kv).catch((err) => {
18839
- console.warn(`[agentmemory] Failed to rebuild search index:`, err);
18840
- return 0;
18841
- });
19411
+ if (bm25Index.size === 0) rebuildIndex(kv).then((indexCount) => {
18842
19412
  if (indexCount > 0) {
18843
19413
  bootLog(`Search index rebuilt: ${indexCount} entries`);
18844
19414
  indexPersistence.scheduleSave();
18845
19415
  }
18846
- } else try {
19416
+ }).catch((err) => {
19417
+ console.warn(`[agentmemory] Failed to rebuild search index:`, err);
19418
+ });
19419
+ else try {
18847
19420
  const memories = await kv.list(KV.memories);
18848
19421
  let backfilled = 0;
18849
19422
  for (const memory of memories) {
@@ -18928,6 +19501,7 @@ async function main() {
18928
19501
  console.warn(`[agentmemory] Failed to save index on shutdown:`, err);
18929
19502
  });
18930
19503
  await sdk.shutdown();
19504
+ clearWorkerPidfile();
18931
19505
  process.exit(0);
18932
19506
  };
18933
19507
  process.on("SIGINT", shutdown);
@@ -18940,4 +19514,4 @@ main().catch((err) => {
18940
19514
 
18941
19515
  //#endregion
18942
19516
  export { };
18943
- //# sourceMappingURL=src-DPSaLB5-.mjs.map
19517
+ //# sourceMappingURL=src-gpTAJuBy.mjs.map