@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
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { TriggerAction, registerWorker } from "iii-sdk";
4
- import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import Anthropic from "@anthropic-ai/sdk";
@@ -40,6 +40,7 @@ function safeParseInt(value, fallback) {
40
40
  }
41
41
  const DATA_DIR = join(homedir(), ".agentmemory");
42
42
  const ENV_FILE = join(DATA_DIR, ".env");
43
+ let warnPremiumModelShown = false;
43
44
  function loadEnvFile() {
44
45
  if (!existsSync(ENV_FILE)) return {};
45
46
  const content = readFileSync(ENV_FILE, "utf-8");
@@ -93,11 +94,18 @@ function detectProvider(env) {
93
94
  maxTokens
94
95
  };
95
96
  }
96
- if (hasRealValue(env["OPENROUTER_API_KEY"])) return {
97
- provider: "openrouter",
98
- model: env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-4-20250514",
99
- maxTokens
100
- };
97
+ if (hasRealValue(env["OPENROUTER_API_KEY"])) {
98
+ const model = env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-4-20250514";
99
+ if (!warnPremiumModelShown && /sonnet|opus|gpt-4o(?!.*mini)|gpt-4-turbo/i.test(model) && env["AGENTMEMORY_SUPPRESS_COST_WARNING"] !== "1" && env["AGENTMEMORY_SUPPRESS_COST_WARNING"] !== "true") {
100
+ warnPremiumModelShown = true;
101
+ process.stderr.write(`[agentmemory] OPENROUTER_MODEL=${model} is in the premium tier. Background compression on this model can cost $5+/day under active use. Cheaper alternatives with comparable quality for memory compression: deepseek/deepseek-v4-pro, deepseek/deepseek-chat, qwen/qwen3-coder. See README "Cost-aware model selection" for the full table. Set AGENTMEMORY_SUPPRESS_COST_WARNING=1 to silence.\n`);
102
+ }
103
+ return {
104
+ provider: "openrouter",
105
+ model,
106
+ maxTokens
107
+ };
108
+ }
101
109
  if (!(env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true")) {
102
110
  process.stderr.write("[agentmemory] No LLM provider key found (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY, OPENAI_API_KEY). LLM-backed compression and summarization are DISABLED — using no-op provider. This is the safe default: the agent-sdk fallback used to spawn Claude Agent SDK child sessions which inherit Claude Code's plugin hooks and cause infinite Stop-hook recursion (#149 follow-up). To opt in to the agent-sdk fallback anyway, set both AGENTMEMORY_AUTO_COMPRESS=true AND AGENTMEMORY_ALLOW_AGENT_SDK=true — but be aware it will burn your Claude Pro allocation and may still recurse if you use it from inside Claude Code itself.\n");
103
111
  return {
@@ -137,6 +145,9 @@ function getMergedEnv(overrides) {
137
145
  function getEnvVar(key) {
138
146
  return getMergedEnv()[key];
139
147
  }
148
+ function isDropStaleIndexEnabled() {
149
+ return getMergedEnv()["AGENTMEMORY_DROP_STALE_INDEX"] === "true";
150
+ }
140
151
  function detectLlmProviderKind() {
141
152
  const env = getMergedEnv();
142
153
  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"]) || hasRealValue(env["OPENAI_API_KEY"]) && env["OPENAI_API_KEY_FOR_LLM"] !== "false") return "llm";
@@ -172,8 +183,8 @@ function loadClaudeBridgeConfig() {
172
183
  const lineBudget = safeParseInt(env["CLAUDE_MEMORY_LINE_BUDGET"], 200);
173
184
  let memoryFilePath = "";
174
185
  if (enabled && projectPath) {
175
- const safePath = projectPath.replace(/[/\\]/g, "-").replace(/^-/, "");
176
- memoryFilePath = join(homedir(), ".claude", "projects", safePath, "memory", "MEMORY.md");
186
+ const safePath = projectPath.replace(/[/\\]/g, "-");
187
+ memoryFilePath = join(homedir(), ".claude", "projects", safePath, "MEMORY.md");
177
188
  }
178
189
  return {
179
190
  enabled,
@@ -193,6 +204,23 @@ function loadTeamConfig() {
193
204
  mode: env["TEAM_MODE"] === "shared" ? "shared" : "private"
194
205
  };
195
206
  }
207
+ function loadAgentScope() {
208
+ const env = getMergedEnv();
209
+ const raw = env["AGENT_ID"];
210
+ if (!raw) return null;
211
+ const agentId = raw.trim().slice(0, 128);
212
+ if (!agentId) return null;
213
+ return {
214
+ agentId,
215
+ mode: env["AGENTMEMORY_AGENT_SCOPE"] === "isolated" ? "isolated" : "shared"
216
+ };
217
+ }
218
+ function getAgentId() {
219
+ return loadAgentScope()?.agentId;
220
+ }
221
+ function isAgentScopeIsolated() {
222
+ return loadAgentScope()?.mode === "isolated";
223
+ }
196
224
  function loadSnapshotConfig() {
197
225
  const env = getMergedEnv();
198
226
  return {
@@ -450,13 +478,25 @@ function v1AzureUrl(baseUrl, path) {
450
478
  url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
451
479
  return url.toString();
452
480
  }
481
+ function appendOpenAIRoute(baseUrl, route) {
482
+ const trimmedBase = baseUrl.replace(/\/+$/, "");
483
+ const cleanRoute = route.startsWith("/") ? route : `/${route}`;
484
+ let pathname;
485
+ try {
486
+ pathname = new URL(trimmedBase).pathname.replace(/\/+$/, "");
487
+ } catch {
488
+ return `${trimmedBase}/v1${cleanRoute}`;
489
+ }
490
+ if (pathname === "" || pathname === "/") return `${trimmedBase}/v1${cleanRoute}`;
491
+ return `${trimmedBase}${cleanRoute}`;
492
+ }
453
493
  function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
454
494
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
455
- return `${baseUrl}/v1/chat/completions`;
495
+ return appendOpenAIRoute(baseUrl, "/chat/completions");
456
496
  }
457
497
  function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
458
498
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
459
- return `${baseUrl}/v1/embeddings`;
499
+ return appendOpenAIRoute(baseUrl, "/embeddings");
460
500
  }
461
501
  function buildAuthHeaders(apiKey, isAzure) {
462
502
  if (isAzure) return {
@@ -538,6 +578,7 @@ var OpenAIProvider = class {
538
578
  const body = {
539
579
  model: this.model,
540
580
  max_tokens: this.maxTokens,
581
+ stream: false,
541
582
  messages: [{
542
583
  role: "system",
543
584
  content: systemPrompt
@@ -566,7 +607,7 @@ var OpenAIProvider = class {
566
607
  const message = data.choices?.[0]?.message;
567
608
  const content = message?.content;
568
609
  if (content) return content;
569
- const reasoning = message?.reasoning;
610
+ const reasoning = message?.reasoning ?? message?.reasoning_content;
570
611
  if (reasoning) return reasoning;
571
612
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
572
613
  }
@@ -2511,6 +2552,24 @@ var SearchIndex = class SearchIndex {
2511
2552
  has(id) {
2512
2553
  return this.entries.has(id);
2513
2554
  }
2555
+ remove(id) {
2556
+ const entry = this.entries.get(id);
2557
+ if (!entry) return;
2558
+ const termFreq = this.docTermCounts.get(id);
2559
+ if (termFreq) {
2560
+ for (const term of termFreq.keys()) {
2561
+ const postingList = this.invertedIndex.get(term);
2562
+ if (postingList) {
2563
+ postingList.delete(id);
2564
+ if (postingList.size === 0) this.invertedIndex.delete(term);
2565
+ }
2566
+ }
2567
+ this.docTermCounts.delete(id);
2568
+ }
2569
+ this.totalDocLength = Math.max(0, this.totalDocLength - entry.termCount);
2570
+ this.entries.delete(id);
2571
+ this.sortedTerms = null;
2572
+ }
2514
2573
  search(query, limit = 20) {
2515
2574
  const rawTerms = this.tokenize(query.toLowerCase());
2516
2575
  if (rawTerms.length === 0) return [];
@@ -2863,6 +2922,7 @@ function buildSyntheticCompression(raw) {
2863
2922
  };
2864
2923
  if (raw.modality) result.modality = raw.modality;
2865
2924
  if (raw.imageData) result.imageData = raw.imageData;
2925
+ if (raw.agentId) result.agentId = raw.agentId;
2866
2926
  return result;
2867
2927
  }
2868
2928
 
@@ -2952,6 +3012,16 @@ function setVectorIndex(idx) {
2952
3012
  function setEmbeddingProvider(provider) {
2953
3013
  currentEmbeddingProvider = provider;
2954
3014
  }
3015
+ function vectorIndexRemove(id) {
3016
+ vectorIndex?.remove(id);
3017
+ }
3018
+ let indexPersistence = null;
3019
+ function setIndexPersistence(p) {
3020
+ indexPersistence = p;
3021
+ }
3022
+ async function flushIndexSave() {
3023
+ await indexPersistence?.save();
3024
+ }
2955
3025
  const EMBED_MAX_CHARS = 16e3;
2956
3026
  function clipEmbedInput(text) {
2957
3027
  if (text.length <= EMBED_MAX_CHARS) return text;
@@ -2985,20 +3055,108 @@ async function vectorIndexAddGuarded(id, sessionId, text, context) {
2985
3055
  return false;
2986
3056
  }
2987
3057
  }
3058
+ async function vectorIndexAddBatchGuarded(items) {
3059
+ const vi = vectorIndex;
3060
+ const ep = currentEmbeddingProvider;
3061
+ if (!vi || !ep || items.length === 0) return {
3062
+ ok: 0,
3063
+ fail: 0
3064
+ };
3065
+ let embeddings;
3066
+ try {
3067
+ embeddings = await ep.embedBatch(items.map((i) => clipEmbedInput(i.text)));
3068
+ } catch (err) {
3069
+ logger.warn("vector-index add batch: embed failed — skipping batch", {
3070
+ batchSize: items.length,
3071
+ provider: ep.name,
3072
+ error: err instanceof Error ? err.message : String(err)
3073
+ });
3074
+ return {
3075
+ ok: 0,
3076
+ fail: items.length
3077
+ };
3078
+ }
3079
+ if (embeddings.length !== items.length) {
3080
+ logger.warn("vector-index add batch: provider returned wrong length — skipping batch", {
3081
+ batchSize: items.length,
3082
+ returned: embeddings.length,
3083
+ provider: ep.name
3084
+ });
3085
+ return {
3086
+ ok: 0,
3087
+ fail: items.length
3088
+ };
3089
+ }
3090
+ let ok = 0;
3091
+ let fail = 0;
3092
+ for (let i = 0; i < items.length; i++) {
3093
+ const item = items[i];
3094
+ const embedding = embeddings[i];
3095
+ if (embedding.length !== ep.dimensions) {
3096
+ logger.warn("vector-index add batch: dimension mismatch — skipping item", {
3097
+ kind: item.context.kind,
3098
+ id: item.context.logId,
3099
+ provider: ep.name,
3100
+ expected: ep.dimensions,
3101
+ received: embedding.length
3102
+ });
3103
+ fail++;
3104
+ continue;
3105
+ }
3106
+ try {
3107
+ vi.add(item.id, item.sessionId, embedding);
3108
+ ok++;
3109
+ } catch (err) {
3110
+ logger.warn("vector-index add batch: index write failed — skipping item", {
3111
+ kind: item.context.kind,
3112
+ id: item.context.logId,
3113
+ error: err instanceof Error ? err.message : String(err)
3114
+ });
3115
+ fail++;
3116
+ }
3117
+ }
3118
+ return {
3119
+ ok,
3120
+ fail
3121
+ };
3122
+ }
3123
+ const DEFAULT_REBUILD_EMBED_BATCH = 32;
3124
+ function getRebuildEmbedBatchSize() {
3125
+ const raw = process.env.REBUILD_EMBED_BATCH_SIZE;
3126
+ if (!raw) return DEFAULT_REBUILD_EMBED_BATCH;
3127
+ const n = parseInt(raw, 10);
3128
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_REBUILD_EMBED_BATCH;
3129
+ }
2988
3130
  async function rebuildIndex(kv) {
2989
3131
  const idx = getSearchIndex();
2990
3132
  idx.clear();
2991
3133
  vectorIndex?.clear();
3134
+ const batchSize = getRebuildEmbedBatchSize();
3135
+ const pending = [];
2992
3136
  let count = 0;
3137
+ const flush = async () => {
3138
+ if (pending.length === 0) return;
3139
+ await vectorIndexAddBatchGuarded(pending);
3140
+ pending.length = 0;
3141
+ };
3142
+ const enqueue = async (job) => {
3143
+ pending.push(job);
3144
+ if (pending.length >= batchSize) await flush();
3145
+ };
2993
3146
  try {
2994
3147
  const memories = await kv.list(KV.memories);
2995
3148
  for (const memory of memories) {
2996
3149
  if (memory.isLatest === false) continue;
2997
3150
  if (!memory.title || !memory.content) continue;
2998
3151
  idx.add(memoryToObservation(memory));
2999
- await vectorIndexAddGuarded(memory.id, memory.sessionIds[0] ?? "memory", memory.title + " " + memory.content, {
3000
- kind: "memory",
3001
- logId: memory.id
3152
+ await enqueue({
3153
+ id: memory.id,
3154
+ sessionId: memory.sessionIds[0] ?? "memory",
3155
+ text: memory.title + " " + memory.content,
3156
+ context: {
3157
+ kind: "memory",
3158
+ logId: memory.id
3159
+ }
3002
3160
  });
3003
3161
  count++;
3004
3162
  }
@@ -3006,7 +3164,10 @@ async function rebuildIndex(kv) {
3006
3164
  logger.warn("rebuildIndex: failed to load memories", { error: err instanceof Error ? err.message : String(err) });
3007
3165
  }
3008
3166
  const sessions = await kv.list(KV.sessions);
3009
- if (!sessions.length) return count;
3167
+ if (!sessions.length) {
3168
+ await flush();
3169
+ return count;
3170
+ }
3010
3171
  const obsPerSession = [];
3011
3172
  const failedSessions = [];
3012
3173
  for (let batch = 0; batch < sessions.length; batch += 10) {
@@ -3024,12 +3185,18 @@ async function rebuildIndex(kv) {
3024
3185
  if (failedSessions.length > 0) logger.warn("rebuildIndex: failed to load observations for sessions", { failedSessions });
3025
3186
  for (const observations of obsPerSession) for (const obs of observations) if (obs.title && obs.narrative) {
3026
3187
  idx.add(obs);
3027
- await vectorIndexAddGuarded(obs.id, obs.sessionId, obs.title + " " + obs.narrative, {
3028
- kind: "observation",
3029
- logId: obs.id
3188
+ await enqueue({
3189
+ id: obs.id,
3190
+ sessionId: obs.sessionId,
3191
+ text: obs.title + " " + obs.narrative,
3192
+ context: {
3193
+ kind: "observation",
3194
+ logId: obs.id
3195
+ }
3030
3196
  });
3031
3197
  count++;
3032
3198
  }
3199
+ await flush();
3033
3200
  return count;
3034
3201
  }
3035
3202
  function registerSearchFunction(sdk, kv) {
@@ -3249,6 +3416,9 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3249
3416
  error: `Session observation limit reached (${maxObservationsPerSession})`
3250
3417
  };
3251
3418
  }
3419
+ const existingSession = await kv.get(KV.sessions, payload.sessionId);
3420
+ const inheritedAgentId = existingSession ? existingSession.agentId : getAgentId();
3421
+ if (inheritedAgentId) raw.agentId = inheritedAgentId;
3252
3422
  if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
3253
3423
  const { saveImageToDisk } = await Promise.resolve().then(() => image_store_exports);
3254
3424
  const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
@@ -3300,7 +3470,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3300
3470
  },
3301
3471
  action: TriggerAction.Void()
3302
3472
  });
3303
- const session = await kv.get(KV.sessions, payload.sessionId);
3473
+ const session = existingSession;
3304
3474
  if (session) {
3305
3475
  const updates = [{
3306
3476
  type: "set",
@@ -3320,6 +3490,20 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3320
3490
  });
3321
3491
  }
3322
3492
  await kv.update(KV.sessions, payload.sessionId, updates);
3493
+ } else if (typeof payload.project === "string" && payload.project.trim().length > 0 && typeof payload.cwd === "string" && payload.cwd.trim().length > 0) {
3494
+ const trimmedPrompt = typeof raw.userPrompt === "string" ? raw.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
3495
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3496
+ await kv.set(KV.sessions, payload.sessionId, {
3497
+ id: payload.sessionId,
3498
+ project: payload.project,
3499
+ cwd: payload.cwd,
3500
+ startedAt: payload.timestamp ?? ts,
3501
+ updatedAt: ts,
3502
+ status: "active",
3503
+ observationCount: 1,
3504
+ ...inheritedAgentId ? { agentId: inheritedAgentId } : {},
3505
+ ...trimmedPrompt && trimmedPrompt.length > 0 ? { firstPrompt: trimmedPrompt } : {}
3506
+ });
3323
3507
  }
3324
3508
  if (isAutoCompressEnabled()) await sdk.trigger({
3325
3509
  function_id: "mem::compress",
@@ -4579,7 +4763,8 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
4579
4763
  confidence: qualityScore / 100,
4580
4764
  ...hasImage ? { modality: data.raw.modality } : {},
4581
4765
  ...imageDescription ? { imageDescription } : {},
4582
- ...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
4766
+ ...data.raw.imageData ? { imageRef: data.raw.imageData } : {},
4767
+ ...data.raw.agentId ? { agentId: data.raw.agentId } : {}
4583
4768
  };
4584
4769
  await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
4585
4770
  try {
@@ -4814,9 +4999,134 @@ function buildSummaryPrompt(observations) {
4814
4999
  });
4815
5000
  return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
4816
5001
  }
5002
+ 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.
5003
+
5004
+ Output EXACTLY this XML format with no additional text:
5005
+
5006
+ <summary>
5007
+ <title>Short session title (max 100 chars)</title>
5008
+ <narrative>3-5 sentence narrative covering the whole session</narrative>
5009
+ <decisions>
5010
+ <decision>Key technical decision made</decision>
5011
+ </decisions>
5012
+ <files>
5013
+ <file>path/to/modified/file</file>
5014
+ </files>
5015
+ <concepts>
5016
+ <concept>key concept from session</concept>
5017
+ </concepts>
5018
+ </summary>
5019
+
5020
+ Rules:
5021
+ - Synthesize a single narrative that reflects the whole arc, not a chunk-by-chunk recap
5022
+ - Preserve every distinct decision across chunks
5023
+ - Union (deduplicate) all files and concepts
5024
+ - Title should capture the session's overall outcome`;
5025
+ function buildReducePrompt(partials) {
5026
+ const sections = partials.map((p, i) => {
5027
+ const decisions = p.keyDecisions.map((d) => ` - ${d}`).join("\n");
5028
+ const files = p.filesModified.map((f) => ` - ${f}`).join("\n");
5029
+ const concepts = p.concepts.join(", ");
5030
+ return `[Chunk ${i + 1} of ${partials.length} — obs ${p.obsRangeStart}-${p.obsRangeEnd}]
5031
+ Title: ${p.title}
5032
+ Narrative: ${p.narrative}
5033
+ Decisions:
5034
+ ${decisions}
5035
+ Files:
5036
+ ${files}
5037
+ Concepts: ${concepts}`;
5038
+ });
5039
+ return `Partial summaries (${partials.length} chunks of one session, chronological):\n\n${sections.join("\n\n---\n\n")}`;
5040
+ }
4817
5041
 
4818
5042
  //#endregion
4819
5043
  //#region src/functions/summarize.ts
5044
+ const CHUNK_SIZE_DEFAULT = 400;
5045
+ const CHUNK_CONCURRENCY_DEFAULT = 6;
5046
+ const MAX_SKIP_RATIO = .5;
5047
+ function getChunkSize() {
5048
+ const raw = process.env.SUMMARIZE_CHUNK_SIZE;
5049
+ if (!raw) return CHUNK_SIZE_DEFAULT;
5050
+ const n = parseInt(raw, 10);
5051
+ return Number.isFinite(n) && n > 0 ? n : CHUNK_SIZE_DEFAULT;
5052
+ }
5053
+ function getChunkConcurrency() {
5054
+ const raw = process.env.SUMMARIZE_CHUNK_CONCURRENCY;
5055
+ if (!raw) return CHUNK_CONCURRENCY_DEFAULT;
5056
+ const n = parseInt(raw, 10);
5057
+ return Number.isFinite(n) && n > 0 ? n : CHUNK_CONCURRENCY_DEFAULT;
5058
+ }
5059
+ async function summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, total) {
5060
+ for (let attempt = 1; attempt <= 2; attempt++) try {
5061
+ const parsed = parseSummaryXml(await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(chunk)), sessionId, project, chunk.length);
5062
+ if (parsed) return parsed;
5063
+ logger.warn("Summarize chunk parse failed", {
5064
+ sessionId,
5065
+ chunk: `${idx + 1}/${total}`,
5066
+ attempt
5067
+ });
5068
+ } catch (err) {
5069
+ logger.warn("Summarize chunk LLM call failed", {
5070
+ sessionId,
5071
+ chunk: `${idx + 1}/${total}`,
5072
+ attempt,
5073
+ error: err instanceof Error ? err.message : String(err)
5074
+ });
5075
+ }
5076
+ return null;
5077
+ }
5078
+ async function produceSummaryXml(provider, compressed, sessionId, project) {
5079
+ const chunkSize = getChunkSize();
5080
+ if (compressed.length <= chunkSize) return {
5081
+ response: await provider.summarize(SUMMARY_SYSTEM, buildSummaryPrompt(compressed)),
5082
+ mode: "single",
5083
+ chunks: 1
5084
+ };
5085
+ const chunks = [];
5086
+ for (let i = 0; i < compressed.length; i += chunkSize) chunks.push(compressed.slice(i, i + chunkSize));
5087
+ const concurrency = getChunkConcurrency();
5088
+ logger.info("Summarize chunking session", {
5089
+ sessionId,
5090
+ chunks: chunks.length,
5091
+ chunkSize,
5092
+ concurrency,
5093
+ totalObservations: compressed.length
5094
+ });
5095
+ const partialByIdx = new Array(chunks.length).fill(null);
5096
+ for (let batchStart = 0; batchStart < chunks.length; batchStart += concurrency) {
5097
+ const batch = chunks.slice(batchStart, batchStart + concurrency);
5098
+ await Promise.all(batch.map(async (chunk, j) => {
5099
+ const idx = batchStart + j;
5100
+ partialByIdx[idx] = await summarizeChunkWithRetry(provider, chunk, sessionId, project, idx, chunks.length);
5101
+ }));
5102
+ }
5103
+ const skipped = partialByIdx.filter((p) => p === null).length;
5104
+ const partials = partialByIdx.filter((p) => p !== null);
5105
+ 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`);
5106
+ if (skipped > 0) logger.warn("Summarize chunks partially skipped", {
5107
+ sessionId,
5108
+ skipped,
5109
+ total: chunks.length
5110
+ });
5111
+ const reduceInput = partials.map((p) => {
5112
+ const originalIdx = partialByIdx.indexOf(p);
5113
+ return {
5114
+ title: p.title,
5115
+ narrative: p.narrative,
5116
+ keyDecisions: p.keyDecisions,
5117
+ filesModified: p.filesModified,
5118
+ concepts: p.concepts,
5119
+ obsRangeStart: originalIdx * chunkSize + 1,
5120
+ obsRangeEnd: Math.min((originalIdx + 1) * chunkSize, compressed.length)
5121
+ };
5122
+ });
5123
+ return {
5124
+ response: await provider.summarize(REDUCE_SYSTEM, buildReducePrompt(reduceInput)),
5125
+ mode: "chunked",
5126
+ chunks: chunks.length,
5127
+ skipped
5128
+ };
5129
+ }
4820
5130
  function parseSummaryXml(xml, sessionId, project, obsCount) {
4821
5131
  const title = getXmlTag(xml, "title");
4822
5132
  if (!title) return null;
@@ -4865,16 +5175,15 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
4865
5175
  };
4866
5176
  }
4867
5177
  try {
4868
- const prompt = buildSummaryPrompt(compressed);
4869
- const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
5178
+ const { response, mode, chunks } = await produceSummaryXml(provider, compressed, sessionId, session.project);
4870
5179
  if (!response || !response.trim()) {
4871
5180
  const latencyMs = Date.now() - startMs;
4872
5181
  if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
4873
5182
  logger.warn("Empty provider response on summarize", {
4874
5183
  sessionId,
4875
5184
  provider: provider.name,
4876
- promptBytes: prompt.length,
4877
- systemBytes: SUMMARY_SYSTEM.length,
5185
+ mode,
5186
+ chunks,
4878
5187
  observationCount: compressed.length
4879
5188
  });
4880
5189
  return {
@@ -5426,6 +5735,7 @@ function registerRememberFunction(sdk, kv) {
5426
5735
  break;
5427
5736
  }
5428
5737
  }
5738
+ const callAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim().slice(0, 128) : getAgentId();
5429
5739
  const memory = {
5430
5740
  id: generateId("mem"),
5431
5741
  createdAt: now,
@@ -5441,7 +5751,8 @@ function registerRememberFunction(sdk, kv) {
5441
5751
  parentId: supersededId,
5442
5752
  supersedes: supersededId ? [supersededId] : [],
5443
5753
  sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
5444
- isLatest: true
5754
+ isLatest: true,
5755
+ ...callAgentId ? { agentId: callAgentId } : {}
5445
5756
  };
5446
5757
  if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
5447
5758
  if (supersededMemory) {
@@ -5487,6 +5798,8 @@ function registerRememberFunction(sdk, kv) {
5487
5798
  await kv.delete(KV.memories, data.memoryId);
5488
5799
  if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
5489
5800
  await deleteAccessLog(kv, data.memoryId);
5801
+ getSearchIndex().remove(data.memoryId);
5802
+ vectorIndexRemove(data.memoryId);
5490
5803
  deletedMemoryIds.push(data.memoryId);
5491
5804
  deleted++;
5492
5805
  }
@@ -5495,6 +5808,8 @@ function registerRememberFunction(sdk, kv) {
5495
5808
  await kv.delete(KV.observations(data.sessionId), obsId);
5496
5809
  if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5497
5810
  if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5811
+ getSearchIndex().remove(obsId);
5812
+ vectorIndexRemove(obsId);
5498
5813
  deletedObservationIds.push(obsId);
5499
5814
  deleted++;
5500
5815
  }
@@ -5504,6 +5819,8 @@ function registerRememberFunction(sdk, kv) {
5504
5819
  await kv.delete(KV.observations(data.sessionId), obs.id);
5505
5820
  if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5506
5821
  if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5822
+ getSearchIndex().remove(obs.id);
5823
+ vectorIndexRemove(obs.id);
5507
5824
  deletedObservationIds.push(obs.id);
5508
5825
  deleted++;
5509
5826
  }
@@ -5512,14 +5829,17 @@ function registerRememberFunction(sdk, kv) {
5512
5829
  deletedSession = true;
5513
5830
  deleted += 2;
5514
5831
  }
5515
- if (deleted > 0) await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5516
- sessionId: data.sessionId,
5517
- deleted,
5518
- memoriesDeleted: deletedMemoryIds.length,
5519
- observationsDeleted: deletedObservationIds.length,
5520
- sessionDeleted: deletedSession,
5521
- reason: "user-initiated forget"
5522
- });
5832
+ if (deleted > 0) {
5833
+ await flushIndexSave();
5834
+ await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5835
+ sessionId: data.sessionId,
5836
+ deleted,
5837
+ memoriesDeleted: deletedMemoryIds.length,
5838
+ observationsDeleted: deletedObservationIds.length,
5839
+ sessionDeleted: deletedSession,
5840
+ reason: "user-initiated forget"
5841
+ });
5842
+ }
5523
5843
  logger.info("Memory forgotten", { deleted });
5524
5844
  return {
5525
5845
  success: true,
@@ -6031,8 +6351,12 @@ async function findByKeyword(kv, keyword, project) {
6031
6351
 
6032
6352
  //#endregion
6033
6353
  //#region src/functions/smart-search.ts
6354
+ const LESSON_CONTENT_PREVIEW_CHARS = 240;
6034
6355
  function registerSmartSearchFunction(sdk, kv, searchFn) {
6035
6356
  sdk.registerFunction("mem::smart-search", async (data) => {
6357
+ const isolated = isAgentScopeIsolated();
6358
+ const explicitAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim() : void 0;
6359
+ const filterAgentId = explicitAgentId === "*" ? void 0 : explicitAgentId ?? (isolated ? getAgentId() : void 0);
6036
6360
  if (data.expandIds && data.expandIds.length > 0) {
6037
6361
  const raw = data.expandIds.slice(0, 20);
6038
6362
  const items = raw.map((entry) => {
@@ -6053,17 +6377,19 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6053
6377
  observation: obs
6054
6378
  } : null)));
6055
6379
  for (const r of results) if (r) expanded.push(r);
6056
- recordAccessBatch(kv, expanded.map((e) => e.observation.id));
6380
+ const scoped = filterAgentId ? expanded.filter((e) => e.observation.agentId === filterAgentId) : expanded;
6381
+ recordAccessBatch(kv, scoped.map((e) => e.observation.id));
6057
6382
  const truncated = data.expandIds.length > raw.length;
6058
6383
  logger.info("Smart search expanded", {
6059
6384
  requested: data.expandIds.length,
6060
6385
  attempted: raw.length,
6061
- returned: expanded.length,
6386
+ returned: scoped.length,
6387
+ filteredOutOfScope: expanded.length - scoped.length,
6062
6388
  truncated
6063
6389
  });
6064
6390
  return {
6065
6391
  mode: "expanded",
6066
- results: expanded,
6392
+ results: scoped,
6067
6393
  truncated
6068
6394
  };
6069
6395
  }
@@ -6073,7 +6399,11 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6073
6399
  error: "query is required"
6074
6400
  };
6075
6401
  const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
6076
- const compact = (await searchFn(data.query, limit)).map((r) => ({
6402
+ const lessonLimit = Math.min(limit, 10);
6403
+ const includeLessons = data.includeLessons !== false;
6404
+ const overFetchLimit = filterAgentId ? Math.min(limit * 3, 300) : limit;
6405
+ const [hybridResults, lessons] = await Promise.all([searchFn(data.query, overFetchLimit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
6406
+ const compact = (filterAgentId ? hybridResults.filter((r) => r.observation.agentId === filterAgentId).slice(0, limit) : hybridResults.slice(0, limit)).map((r) => ({
6077
6407
  obsId: r.observation.id,
6078
6408
  sessionId: r.sessionId,
6079
6409
  title: r.observation.title,
@@ -6084,14 +6414,42 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6084
6414
  recordAccessBatch(kv, compact.map((r) => r.obsId));
6085
6415
  logger.info("Smart search compact", {
6086
6416
  query: data.query,
6087
- results: compact.length
6417
+ results: compact.length,
6418
+ lessons: lessons.length
6088
6419
  });
6089
- return {
6420
+ const response = {
6090
6421
  mode: "compact",
6091
6422
  results: compact
6092
6423
  };
6424
+ if (includeLessons) response.lessons = lessons;
6425
+ return response;
6093
6426
  });
6094
6427
  }
6428
+ async function recallLessons(sdk, query, limit, project) {
6429
+ try {
6430
+ const result = await sdk.trigger({
6431
+ function_id: "mem::lesson-recall",
6432
+ payload: {
6433
+ query,
6434
+ limit,
6435
+ project
6436
+ }
6437
+ });
6438
+ if (!result?.success || !Array.isArray(result.lessons)) return [];
6439
+ return result.lessons.map((l) => ({
6440
+ lessonId: l.id,
6441
+ content: l.content.length > LESSON_CONTENT_PREVIEW_CHARS ? l.content.slice(0, LESSON_CONTENT_PREVIEW_CHARS) + "…" : l.content,
6442
+ confidence: l.confidence,
6443
+ score: l.score ?? l.confidence,
6444
+ createdAt: l.createdAt,
6445
+ project: l.project,
6446
+ tags: l.tags ?? []
6447
+ }));
6448
+ } catch (err) {
6449
+ logger.warn("Smart search: mem::lesson-recall failed; returning empty lesson list", { error: err instanceof Error ? err.message : String(err) });
6450
+ return [];
6451
+ }
6452
+ }
6095
6453
  async function findObservation$1(kv, obsId, sessionIdHint) {
6096
6454
  if (sessionIdHint) {
6097
6455
  const obs = await kv.get(KV.observations(sessionIdHint), obsId).catch(() => null);
@@ -6224,6 +6582,8 @@ function registerAutoForgetFunction(sdk, kv) {
6224
6582
  timestamp: mem.forgetAfter
6225
6583
  });
6226
6584
  await deleteAccessLog(kv, mem.id);
6585
+ getSearchIndex().remove(mem.id);
6586
+ vectorIndexRemove(mem.id);
6227
6587
  }
6228
6588
  }
6229
6589
  }
@@ -6301,10 +6661,13 @@ function registerAutoForgetFunction(sdk, kv) {
6301
6661
  sessionId: sessions[i].id,
6302
6662
  timestamp: obs.timestamp
6303
6663
  });
6664
+ getSearchIndex().remove(obs.id);
6665
+ vectorIndexRemove(obs.id);
6304
6666
  }
6305
6667
  }
6306
6668
  }
6307
6669
  }
6670
+ if (!dryRun && (result.ttlExpired.length > 0 || result.lowValueObs.length > 0)) await flushIndexSave();
6308
6671
  logger.info("Auto-forget complete", {
6309
6672
  ttlExpired: result.ttlExpired.length,
6310
6673
  contradictions: result.contradictions.length,
@@ -6317,7 +6680,7 @@ function registerAutoForgetFunction(sdk, kv) {
6317
6680
 
6318
6681
  //#endregion
6319
6682
  //#region src/version.ts
6320
- const VERSION = "0.9.20";
6683
+ const VERSION = "0.9.22";
6321
6684
 
6322
6685
  //#endregion
6323
6686
  //#region src/functions/export-import.ts
@@ -6455,7 +6818,9 @@ function registerExportImportFunction(sdk, kv) {
6455
6818
  "0.9.17",
6456
6819
  "0.9.18",
6457
6820
  "0.9.19",
6458
- "0.9.20"
6821
+ "0.9.20",
6822
+ "0.9.21",
6823
+ "0.9.22"
6459
6824
  ]).has(importData.version)) return {
6460
6825
  success: false,
6461
6826
  error: `Unsupported export version: ${importData.version}`
@@ -6981,12 +7346,12 @@ function parseGraphXml(xml, observationIds) {
6981
7346
  const nodes = [];
6982
7347
  const edges = [];
6983
7348
  const now = (/* @__PURE__ */ new Date()).toISOString();
6984
- const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/entity>/g;
7349
+ const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*?(?:\/>|>([\s\S]*?)<\/entity>)/g;
6985
7350
  let match;
6986
7351
  while ((match = entityRegex.exec(xml)) !== null) {
6987
7352
  const type = match[1];
6988
7353
  const name = match[2];
6989
- const propsBlock = match[3];
7354
+ const propsBlock = match[3] ?? "";
6990
7355
  const properties = {};
6991
7356
  const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
6992
7357
  let propMatch;
@@ -7514,8 +7879,11 @@ function registerGovernanceFunction(sdk, kv) {
7514
7879
  for (const id of data.memoryIds) if (await kv.get(KV.memories, id)) {
7515
7880
  await kv.delete(KV.memories, id);
7516
7881
  await deleteAccessLog(kv, id);
7882
+ getSearchIndex().remove(id);
7883
+ vectorIndexRemove(id);
7517
7884
  deleted++;
7518
7885
  }
7886
+ if (deleted > 0) await flushIndexSave();
7519
7887
  await recordAudit(kv, "delete", "mem::governance-delete", data.memoryIds, {
7520
7888
  reason: data.reason || "manual deletion",
7521
7889
  deleted
@@ -7568,6 +7936,8 @@ function registerGovernanceFunction(sdk, kv) {
7568
7936
  (await Promise.allSettled(batch.map(async (mem) => {
7569
7937
  await kv.delete(KV.memories, mem.id);
7570
7938
  await deleteAccessLog(kv, mem.id);
7939
+ getSearchIndex().remove(mem.id);
7940
+ vectorIndexRemove(mem.id);
7571
7941
  }))).forEach((result, j) => {
7572
7942
  const mem = batch[j];
7573
7943
  if (result.status === "fulfilled") successfulIds.push(mem.id);
@@ -7583,6 +7953,7 @@ function registerGovernanceFunction(sdk, kv) {
7583
7953
  }
7584
7954
  });
7585
7955
  }
7956
+ if (successfulIds.length > 0) await flushIndexSave();
7586
7957
  await safeAudit(kv, "delete", "mem::governance-bulk", successfulIds, {
7587
7958
  filter: data,
7588
7959
  deleted: successfulIds.length,
@@ -10157,6 +10528,12 @@ const ALL_CATEGORIES = [
10157
10528
  "signals",
10158
10529
  "sessions",
10159
10530
  "memories",
10531
+ "lessons",
10532
+ "summaries",
10533
+ "semantic",
10534
+ "procedural",
10535
+ "crystals",
10536
+ "insights",
10160
10537
  "mesh"
10161
10538
  ];
10162
10539
  const TWENTY_FOUR_HOURS_MS = 1440 * 60 * 1e3;
@@ -10389,6 +10766,133 @@ function registerDiagnosticsFunction(sdk, kv) {
10389
10766
  fixable: false
10390
10767
  });
10391
10768
  }
10769
+ if (categories.includes("lessons")) {
10770
+ const lessons = await kv.list(KV.lessons);
10771
+ const live = lessons.filter((l) => !l.deleted);
10772
+ let lessonIssues = 0;
10773
+ for (const l of live) if (!Number.isFinite(l.confidence) || l.confidence < 0 || l.confidence > 1) {
10774
+ checks.push({
10775
+ name: `lesson-bad-confidence:${l.id}`,
10776
+ category: "lessons",
10777
+ status: "warn",
10778
+ message: `Lesson ${l.id} has confidence ${l.confidence} (expected finite number in 0..1)`,
10779
+ fixable: false
10780
+ });
10781
+ lessonIssues++;
10782
+ }
10783
+ if (lessonIssues === 0) checks.push({
10784
+ name: "lessons-ok",
10785
+ category: "lessons",
10786
+ status: "pass",
10787
+ message: `All ${live.length} lessons are healthy (${lessons.length - live.length} tombstoned)`,
10788
+ fixable: false
10789
+ });
10790
+ }
10791
+ if (categories.includes("summaries")) {
10792
+ const summaries = await kv.list(KV.summaries);
10793
+ let summaryIssues = 0;
10794
+ for (const s of summaries) if (typeof s.title !== "string" || s.title.trim().length === 0) {
10795
+ checks.push({
10796
+ name: `summary-missing-title:${s.sessionId}`,
10797
+ category: "summaries",
10798
+ status: "warn",
10799
+ message: `Summary for session ${s.sessionId} has no title`,
10800
+ fixable: false
10801
+ });
10802
+ summaryIssues++;
10803
+ }
10804
+ if (summaryIssues === 0) checks.push({
10805
+ name: "summaries-ok",
10806
+ category: "summaries",
10807
+ status: "pass",
10808
+ message: `All ${summaries.length} session summaries are consistent`,
10809
+ fixable: false
10810
+ });
10811
+ }
10812
+ if (categories.includes("semantic")) {
10813
+ const semantic = await kv.list(KV.semantic);
10814
+ let semanticIssues = 0;
10815
+ for (const s of semantic) if (!Number.isFinite(s.confidence) || s.confidence < 0 || s.confidence > 1) {
10816
+ checks.push({
10817
+ name: `semantic-bad-confidence:${s.id}`,
10818
+ category: "semantic",
10819
+ status: "warn",
10820
+ message: `Semantic fact ${s.id} has confidence ${s.confidence} (expected finite number in 0..1)`,
10821
+ fixable: false
10822
+ });
10823
+ semanticIssues++;
10824
+ }
10825
+ if (semanticIssues === 0) checks.push({
10826
+ name: "semantic-ok",
10827
+ category: "semantic",
10828
+ status: "pass",
10829
+ message: `All ${semantic.length} semantic memories are consistent`,
10830
+ fixable: false
10831
+ });
10832
+ }
10833
+ if (categories.includes("procedural")) {
10834
+ const procedural = await kv.list(KV.procedural);
10835
+ let proceduralIssues = 0;
10836
+ for (const p of procedural) if (!Array.isArray(p.steps) || p.steps.length === 0) {
10837
+ checks.push({
10838
+ name: `procedural-empty-steps:${p.id}`,
10839
+ category: "procedural",
10840
+ status: "warn",
10841
+ message: `Procedural memory "${p.name}" (${p.id}) has no steps`,
10842
+ fixable: false
10843
+ });
10844
+ proceduralIssues++;
10845
+ }
10846
+ if (proceduralIssues === 0) checks.push({
10847
+ name: "procedural-ok",
10848
+ category: "procedural",
10849
+ status: "pass",
10850
+ message: `All ${procedural.length} procedural memories are consistent`,
10851
+ fixable: false
10852
+ });
10853
+ }
10854
+ if (categories.includes("crystals")) {
10855
+ const crystals = await kv.list(KV.crystals);
10856
+ let crystalIssues = 0;
10857
+ for (const c of crystals) if (typeof c.narrative !== "string" || c.narrative.trim().length === 0) {
10858
+ checks.push({
10859
+ name: `crystal-empty-narrative:${c.id}`,
10860
+ category: "crystals",
10861
+ status: "warn",
10862
+ message: `Crystal ${c.id} has empty narrative`,
10863
+ fixable: false
10864
+ });
10865
+ crystalIssues++;
10866
+ }
10867
+ if (crystalIssues === 0) checks.push({
10868
+ name: "crystals-ok",
10869
+ category: "crystals",
10870
+ status: "pass",
10871
+ message: `All ${crystals.length} crystals are consistent`,
10872
+ fixable: false
10873
+ });
10874
+ }
10875
+ if (categories.includes("insights")) {
10876
+ const insights = await kv.list(KV.insights);
10877
+ let insightIssues = 0;
10878
+ for (const i of insights) if (!Number.isFinite(i.confidence) || i.confidence < 0 || i.confidence > 1) {
10879
+ checks.push({
10880
+ name: `insight-bad-confidence:${i.id}`,
10881
+ category: "insights",
10882
+ status: "warn",
10883
+ message: `Insight ${i.id} has confidence ${i.confidence} (expected finite number in 0..1)`,
10884
+ fixable: false
10885
+ });
10886
+ insightIssues++;
10887
+ }
10888
+ if (insightIssues === 0) checks.push({
10889
+ name: "insights-ok",
10890
+ category: "insights",
10891
+ status: "pass",
10892
+ message: `All ${insights.length} insights are consistent`,
10893
+ fixable: false
10894
+ });
10895
+ }
10392
10896
  if (categories.includes("mesh")) {
10393
10897
  const peers = await kv.list(KV.mesh);
10394
10898
  let meshIssues = 0;
@@ -12849,6 +13353,8 @@ function registerRetentionFunctions(sdk, kv) {
12849
13353
  await kv.delete(scope, candidate.memoryId);
12850
13354
  await kv.delete(KV.retentionScores, candidate.memoryId);
12851
13355
  await deleteAccessLog(kv, candidate.memoryId);
13356
+ getSearchIndex().remove(candidate.memoryId);
13357
+ vectorIndexRemove(candidate.memoryId);
12852
13358
  evicted++;
12853
13359
  evictedIds.push(candidate.memoryId);
12854
13360
  if (resolvedSource === "semantic") evictedSemantic++;
@@ -12856,13 +13362,16 @@ function registerRetentionFunctions(sdk, kv) {
12856
13362
  } catch {
12857
13363
  continue;
12858
13364
  }
12859
- if (evicted > 0) await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
12860
- threshold,
12861
- evicted,
12862
- evictedEpisodic,
12863
- evictedSemantic,
12864
- reason: "retention score below threshold"
12865
- });
13365
+ if (evicted > 0) {
13366
+ await flushIndexSave();
13367
+ await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
13368
+ threshold,
13369
+ evicted,
13370
+ evictedEpisodic,
13371
+ evictedSemantic,
13372
+ reason: "retention score below threshold"
13373
+ });
13374
+ }
12866
13375
  logger.info("Retention-based eviction complete", {
12867
13376
  evicted,
12868
13377
  evictedEpisodic,
@@ -13767,31 +14276,246 @@ function renderViewerDocument() {
13767
14276
  }
13768
14277
 
13769
14278
  //#endregion
13770
- //#region src/triggers/api.ts
13771
- function parseOptionalInt(raw) {
13772
- if (raw === void 0 || raw === null || raw === "") return void 0;
13773
- const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
13774
- return Number.isFinite(n) ? n : void 0;
13775
- }
13776
- function checkAuth(req, secret) {
13777
- if (!secret) return null;
13778
- const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
13779
- if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
13780
- status_code: 401,
13781
- body: { error: "unauthorized" }
13782
- };
14279
+ //#region src/viewer/server.ts
14280
+ function loadViewerFavicon() {
14281
+ const base = dirname(fileURLToPath(import.meta.url));
14282
+ const candidates = [
14283
+ join(base, "..", "src", "viewer", "favicon.svg"),
14284
+ join(base, "..", "viewer", "favicon.svg"),
14285
+ join(base, "viewer", "favicon.svg")
14286
+ ];
14287
+ for (const path of candidates) try {
14288
+ return readFileSync(path);
14289
+ } catch {}
13783
14290
  return null;
13784
14291
  }
13785
- function requireConfiguredSecret(secret, feature) {
13786
- if (secret) return null;
14292
+ 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());
14293
+ const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
14294
+ function buildAllowedHosts(origins, listenPort) {
14295
+ const hosts = /* @__PURE__ */ new Set();
14296
+ for (const o of origins) try {
14297
+ const parsed = new URL(o);
14298
+ if (parsed.host) hosts.add(parsed.host.toLowerCase());
14299
+ } catch {}
14300
+ hosts.add(`localhost:${listenPort}`);
14301
+ hosts.add(`127.0.0.1:${listenPort}`);
14302
+ hosts.add(`[::1]:${listenPort}`);
14303
+ for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
14304
+ return hosts;
14305
+ }
14306
+ function isHostAllowed(headerHost, allowed) {
14307
+ if (typeof headerHost !== "string") return false;
14308
+ const lower = headerHost.toLowerCase().trim();
14309
+ if (!lower) return false;
14310
+ return allowed.has(lower);
14311
+ }
14312
+ function corsHeaders(req) {
14313
+ const origin = req.headers.origin || "";
14314
+ const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
13787
14315
  return {
13788
- status_code: 503,
13789
- body: { error: `${feature} requires AGENTMEMORY_SECRET` }
14316
+ "Access-Control-Allow-Origin": allowed,
14317
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
14318
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
14319
+ Vary: "Origin"
13790
14320
  };
13791
14321
  }
13792
- function flagDisabledResponse(opts) {
13793
- return {
13794
- status_code: 503,
14322
+ function json(res, status, data, req) {
14323
+ const body = JSON.stringify(data);
14324
+ const cors = req ? corsHeaders(req) : {
14325
+ "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
14326
+ Vary: "Origin"
14327
+ };
14328
+ res.writeHead(status, {
14329
+ ...cors,
14330
+ "Content-Type": "application/json"
14331
+ });
14332
+ res.end(body);
14333
+ }
14334
+ function readBody(req) {
14335
+ return new Promise((resolve, reject) => {
14336
+ let data = "";
14337
+ let size = 0;
14338
+ req.on("data", (chunk) => {
14339
+ size += chunk.length;
14340
+ if (size > 1e6) {
14341
+ req.destroy();
14342
+ reject(/* @__PURE__ */ new Error("too large"));
14343
+ return;
14344
+ }
14345
+ data += chunk.toString();
14346
+ });
14347
+ req.on("end", () => resolve(data));
14348
+ req.on("error", reject);
14349
+ });
14350
+ }
14351
+ const MAX_VIEWER_PORT_RETRIES = 10;
14352
+ let boundViewerPort = null;
14353
+ let viewerSkipped = false;
14354
+ function getBoundViewerPort() {
14355
+ return boundViewerPort;
14356
+ }
14357
+ function getViewerSkipped() {
14358
+ return viewerSkipped;
14359
+ }
14360
+ function startViewerServer(port, _kv, _sdk, secret, restPort) {
14361
+ boundViewerPort = null;
14362
+ viewerSkipped = false;
14363
+ const resolvedRestPort = restPort ?? port - 2;
14364
+ const requestedPort = port;
14365
+ let allowedHosts = null;
14366
+ const server = createServer(async (req, res) => {
14367
+ if (!allowedHosts) {
14368
+ const addr = server.address();
14369
+ allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
14370
+ }
14371
+ if (!isHostAllowed(req.headers.host, allowedHosts)) {
14372
+ res.writeHead(403, { "Content-Type": "text/plain" });
14373
+ res.end("forbidden host");
14374
+ return;
14375
+ }
14376
+ const raw = req.url || "/";
14377
+ const qIdx = raw.indexOf("?");
14378
+ const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
14379
+ const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
14380
+ const method = req.method || "GET";
14381
+ if (method === "OPTIONS") {
14382
+ res.writeHead(204, {
14383
+ ...corsHeaders(req),
14384
+ "Access-Control-Max-Age": "86400"
14385
+ });
14386
+ res.end();
14387
+ return;
14388
+ }
14389
+ if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
14390
+ const rendered = renderViewerDocument();
14391
+ if (rendered.found) {
14392
+ res.writeHead(200, {
14393
+ "Content-Type": "text/html; charset=utf-8",
14394
+ "Content-Security-Policy": rendered.csp,
14395
+ "Cache-Control": "no-cache"
14396
+ });
14397
+ res.end(rendered.html);
14398
+ return;
14399
+ }
14400
+ res.writeHead(404, { "Content-Type": "text/plain" });
14401
+ res.end("viewer not found");
14402
+ return;
14403
+ }
14404
+ if (method === "GET" && pathname === "/favicon.svg") {
14405
+ const favicon = loadViewerFavicon();
14406
+ if (favicon) {
14407
+ res.writeHead(200, {
14408
+ "Content-Type": "image/svg+xml",
14409
+ "Cache-Control": "public, max-age=3600"
14410
+ });
14411
+ res.end(favicon);
14412
+ return;
14413
+ }
14414
+ res.writeHead(404, { "Content-Type": "text/plain" });
14415
+ res.end("favicon not found");
14416
+ return;
14417
+ }
14418
+ try {
14419
+ await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
14420
+ } catch (err) {
14421
+ console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
14422
+ json(res, 502, { error: "upstream error" }, req);
14423
+ }
14424
+ });
14425
+ let attempt = 0;
14426
+ let currentPort = requestedPort;
14427
+ const tryListen = () => {
14428
+ server.listen(currentPort, "127.0.0.1");
14429
+ };
14430
+ server.on("listening", () => {
14431
+ const addr = server.address();
14432
+ boundViewerPort = addr && typeof addr === "object" && "port" in addr ? addr.port : currentPort;
14433
+ viewerSkipped = false;
14434
+ if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
14435
+ else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
14436
+ });
14437
+ server.on("error", (err) => {
14438
+ if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
14439
+ attempt++;
14440
+ currentPort = requestedPort + attempt;
14441
+ setImmediate(tryListen);
14442
+ return;
14443
+ }
14444
+ if (err.code === "EADDRINUSE") {
14445
+ boundViewerPort = null;
14446
+ viewerSkipped = true;
14447
+ console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
14448
+ } else {
14449
+ boundViewerPort = null;
14450
+ viewerSkipped = true;
14451
+ console.error(`[agentmemory] Viewer error:`, err.message);
14452
+ }
14453
+ });
14454
+ tryListen();
14455
+ return server;
14456
+ }
14457
+ async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
14458
+ const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
14459
+ const headers = {};
14460
+ if (secret) headers["Authorization"] = `Bearer ${secret}`;
14461
+ const ct = req.headers["content-type"];
14462
+ if (ct) headers["Content-Type"] = ct;
14463
+ let body;
14464
+ if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
14465
+ const controller = new AbortController();
14466
+ const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
14467
+ let upstream;
14468
+ try {
14469
+ upstream = await fetch(upstreamUrl, {
14470
+ method,
14471
+ headers,
14472
+ body: body || void 0,
14473
+ signal: controller.signal
14474
+ });
14475
+ clearTimeout(fetchTimeout);
14476
+ } catch (err) {
14477
+ clearTimeout(fetchTimeout);
14478
+ if (err instanceof Error && err.name === "AbortError") {
14479
+ json(res, 504, { error: "upstream timeout" }, req);
14480
+ return;
14481
+ }
14482
+ throw err;
14483
+ }
14484
+ const cors = corsHeaders(req);
14485
+ const responseBody = await upstream.text();
14486
+ const responseHeaders = { ...cors };
14487
+ const upstreamCt = upstream.headers.get("content-type");
14488
+ if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
14489
+ res.writeHead(upstream.status, responseHeaders);
14490
+ res.end(responseBody);
14491
+ }
14492
+
14493
+ //#endregion
14494
+ //#region src/triggers/api.ts
14495
+ function parseOptionalInt(raw) {
14496
+ if (raw === void 0 || raw === null || raw === "") return void 0;
14497
+ const n = typeof raw === "number" ? raw : parseInt(String(raw), 10);
14498
+ return Number.isFinite(n) ? n : void 0;
14499
+ }
14500
+ function checkAuth(req, secret) {
14501
+ if (!secret) return null;
14502
+ const auth = req.headers?.["authorization"] || req.headers?.["Authorization"];
14503
+ if (typeof auth !== "string" || !timingSafeCompare(auth, `Bearer ${secret}`)) return {
14504
+ status_code: 401,
14505
+ body: { error: "unauthorized" }
14506
+ };
14507
+ return null;
14508
+ }
14509
+ function requireConfiguredSecret(secret, feature) {
14510
+ if (secret) return null;
14511
+ return {
14512
+ status_code: 503,
14513
+ body: { error: `${feature} requires AGENTMEMORY_SECRET` }
14514
+ };
14515
+ }
14516
+ function flagDisabledResponse(opts) {
14517
+ return {
14518
+ status_code: 503,
13795
14519
  body: opts
13796
14520
  };
13797
14521
  }
@@ -13851,7 +14575,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13851
14575
  status_code: 200,
13852
14576
  body: {
13853
14577
  status: "ok",
13854
- service: "agentmemory"
14578
+ service: "agentmemory",
14579
+ viewerPort: getBoundViewerPort(),
14580
+ viewerSkipped: getViewerSkipped()
13855
14581
  }
13856
14582
  }));
13857
14583
  sdk.registerTrigger({
@@ -13946,7 +14672,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13946
14672
  version: VERSION,
13947
14673
  health: health || null,
13948
14674
  functionMetrics,
13949
- circuitBreaker
14675
+ circuitBreaker,
14676
+ viewerPort: getBoundViewerPort(),
14677
+ viewerSkipped: getViewerSkipped()
13950
14678
  }
13951
14679
  };
13952
14680
  });
@@ -14199,13 +14927,18 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14199
14927
  status_code: 400,
14200
14928
  body: { error: "sessionId, project, and cwd are required non-empty strings" }
14201
14929
  };
14930
+ const title = typeof body.title === "string" ? body.title.trim() : void 0;
14931
+ const agentId = (typeof body.agentId === "string" && body.agentId.trim().length > 0 ? body.agentId.trim().slice(0, 128) : void 0) ?? getAgentId();
14202
14932
  const session = {
14203
14933
  id: sessionId,
14204
14934
  project,
14205
14935
  cwd,
14206
14936
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
14207
14937
  status: "active",
14208
- observationCount: 0
14938
+ observationCount: 0,
14939
+ ...title ? { summary: title.slice(0, 200) } : {},
14940
+ ...title ? { firstPrompt: title.slice(0, 200) } : {},
14941
+ ...agentId ? { agentId } : {}
14209
14942
  };
14210
14943
  await kv.set(KV.sessions, sessionId, session);
14211
14944
  return {
@@ -14392,9 +15125,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14392
15125
  sdk.registerFunction("api::sessions", async (req) => {
14393
15126
  const authErr = checkAuth(req, secret);
14394
15127
  if (authErr) return authErr;
15128
+ const sessions = await kv.list(KV.sessions);
15129
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
15130
+ const wildcardAgent = normalizedAgentId === "*";
15131
+ const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
14395
15132
  return {
14396
15133
  status_code: 200,
14397
- body: { sessions: await kv.list(KV.sessions) }
15134
+ body: { sessions: filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions }
14398
15135
  };
14399
15136
  });
14400
15137
  sdk.registerTrigger({
@@ -14413,9 +15150,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14413
15150
  status_code: 400,
14414
15151
  body: { error: "sessionId required" }
14415
15152
  };
15153
+ const observations = await kv.list(KV.observations(sessionId));
15154
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
15155
+ const wildcardAgent = normalizedAgentId === "*";
15156
+ const filterAgentId = wildcardAgent ? void 0 : (normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0) ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
14416
15157
  return {
14417
15158
  status_code: 200,
14418
- body: { observations: await kv.list(KV.observations(sessionId)) }
15159
+ body: { observations: filterAgentId ? observations.filter((o) => o.agentId === filterAgentId) : observations }
14419
15160
  };
14420
15161
  });
14421
15162
  sdk.registerTrigger({
@@ -14691,11 +15432,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14691
15432
  sdk.registerFunction("api::export", async (req) => {
14692
15433
  const authErr = checkAuth(req, secret);
14693
15434
  if (authErr) return authErr;
15435
+ const rawMax = req.query_params?.["maxSessions"];
15436
+ const rawOffset = req.query_params?.["offset"];
15437
+ const payload = {};
15438
+ if (typeof rawMax === "string") {
15439
+ const n = Number(rawMax);
15440
+ if (Number.isInteger(n) && n > 0) payload.maxSessions = n;
15441
+ }
15442
+ if (typeof rawOffset === "string") {
15443
+ const n = Number(rawOffset);
15444
+ if (Number.isInteger(n) && n >= 0) payload.offset = n;
15445
+ }
14694
15446
  return {
14695
15447
  status_code: 200,
14696
15448
  body: await sdk.trigger({
14697
15449
  function_id: "mem::export",
14698
- payload: {}
15450
+ payload
14699
15451
  })
14700
15452
  };
14701
15453
  });
@@ -15181,9 +15933,35 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15181
15933
  const authErr = checkAuth(req, secret);
15182
15934
  if (authErr) return authErr;
15183
15935
  const memories = await kv.list(KV.memories);
15936
+ const latest = req.query_params?.["latest"] === "true";
15937
+ const normalizedAgentId = typeof req.query_params?.["agentId"] === "string" ? req.query_params["agentId"].trim() : void 0;
15938
+ const wildcardAgent = normalizedAgentId === "*";
15939
+ const explicitAgentId = normalizedAgentId && !wildcardAgent ? normalizedAgentId : void 0;
15940
+ const includeOrphans = req.query_params?.["includeOrphans"] === "true";
15941
+ const filterAgentId = wildcardAgent ? void 0 : explicitAgentId ?? (isAgentScopeIsolated() ? getAgentId() : void 0);
15942
+ let filtered = latest ? memories.filter((m) => m.isLatest) : memories;
15943
+ if (filterAgentId) filtered = filtered.filter((m) => m.agentId === filterAgentId || includeOrphans && m.agentId === void 0);
15944
+ if (req.query_params?.["count"] === "true") return {
15945
+ status_code: 200,
15946
+ body: {
15947
+ total: filtered.length,
15948
+ latestCount: filtered.filter((m) => m.isLatest).length
15949
+ }
15950
+ };
15951
+ const rawLimit = req.query_params?.["limit"];
15952
+ const rawOffset = req.query_params?.["offset"];
15953
+ const parsedLimit = typeof rawLimit === "string" ? Number(rawLimit) : NaN;
15954
+ const parsedOffset = typeof rawOffset === "string" ? Number(rawOffset) : NaN;
15955
+ const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 5e3) : void 0;
15956
+ const offset = Number.isInteger(parsedOffset) && parsedOffset >= 0 ? parsedOffset : 0;
15184
15957
  return {
15185
15958
  status_code: 200,
15186
- body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
15959
+ body: {
15960
+ memories: limit !== void 0 ? filtered.slice(offset, offset + limit) : filtered,
15961
+ total: filtered.length,
15962
+ offset,
15963
+ limit: limit ?? null
15964
+ }
15187
15965
  };
15188
15966
  });
15189
15967
  sdk.registerTrigger({
@@ -18263,8 +19041,8 @@ function getAllTools() {
18263
19041
  ];
18264
19042
  }
18265
19043
  function getVisibleTools() {
18266
- if ((process.env["AGENTMEMORY_TOOLS"] || "core") === "all") return getAllTools();
18267
- return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
19044
+ if ((process.env["AGENTMEMORY_TOOLS"] || "all") === "core") return getAllTools().filter((t) => ESSENTIAL_TOOLS.has(t.name));
19045
+ return getAllTools();
18268
19046
  }
18269
19047
 
18270
19048
  //#endregion
@@ -19902,201 +20680,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
19902
20680
  });
19903
20681
  }
19904
20682
 
19905
- //#endregion
19906
- //#region src/viewer/server.ts
19907
- function loadViewerFavicon() {
19908
- const base = dirname(fileURLToPath(import.meta.url));
19909
- const candidates = [
19910
- join(base, "..", "src", "viewer", "favicon.svg"),
19911
- join(base, "..", "viewer", "favicon.svg"),
19912
- join(base, "viewer", "favicon.svg")
19913
- ];
19914
- for (const path of candidates) try {
19915
- return readFileSync(path);
19916
- } catch {}
19917
- return null;
19918
- }
19919
- 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());
19920
- const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
19921
- function buildAllowedHosts(origins, listenPort) {
19922
- const hosts = /* @__PURE__ */ new Set();
19923
- for (const o of origins) try {
19924
- const parsed = new URL(o);
19925
- if (parsed.host) hosts.add(parsed.host.toLowerCase());
19926
- } catch {}
19927
- hosts.add(`localhost:${listenPort}`);
19928
- hosts.add(`127.0.0.1:${listenPort}`);
19929
- hosts.add(`[::1]:${listenPort}`);
19930
- for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
19931
- return hosts;
19932
- }
19933
- function isHostAllowed(headerHost, allowed) {
19934
- if (typeof headerHost !== "string") return false;
19935
- const lower = headerHost.toLowerCase().trim();
19936
- if (!lower) return false;
19937
- return allowed.has(lower);
19938
- }
19939
- function corsHeaders(req) {
19940
- const origin = req.headers.origin || "";
19941
- const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
19942
- return {
19943
- "Access-Control-Allow-Origin": allowed,
19944
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
19945
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
19946
- Vary: "Origin"
19947
- };
19948
- }
19949
- function json(res, status, data, req) {
19950
- const body = JSON.stringify(data);
19951
- const cors = req ? corsHeaders(req) : {
19952
- "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
19953
- Vary: "Origin"
19954
- };
19955
- res.writeHead(status, {
19956
- ...cors,
19957
- "Content-Type": "application/json"
19958
- });
19959
- res.end(body);
19960
- }
19961
- function readBody(req) {
19962
- return new Promise((resolve, reject) => {
19963
- let data = "";
19964
- let size = 0;
19965
- req.on("data", (chunk) => {
19966
- size += chunk.length;
19967
- if (size > 1e6) {
19968
- req.destroy();
19969
- reject(/* @__PURE__ */ new Error("too large"));
19970
- return;
19971
- }
19972
- data += chunk.toString();
19973
- });
19974
- req.on("end", () => resolve(data));
19975
- req.on("error", reject);
19976
- });
19977
- }
19978
- const MAX_VIEWER_PORT_RETRIES = 10;
19979
- function startViewerServer(port, _kv, _sdk, secret, restPort) {
19980
- const resolvedRestPort = restPort ?? port - 2;
19981
- const requestedPort = port;
19982
- let allowedHosts = null;
19983
- const server = createServer(async (req, res) => {
19984
- if (!allowedHosts) {
19985
- const addr = server.address();
19986
- allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
19987
- }
19988
- if (!isHostAllowed(req.headers.host, allowedHosts)) {
19989
- res.writeHead(403, { "Content-Type": "text/plain" });
19990
- res.end("forbidden host");
19991
- return;
19992
- }
19993
- const raw = req.url || "/";
19994
- const qIdx = raw.indexOf("?");
19995
- const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
19996
- const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
19997
- const method = req.method || "GET";
19998
- if (method === "OPTIONS") {
19999
- res.writeHead(204, {
20000
- ...corsHeaders(req),
20001
- "Access-Control-Max-Age": "86400"
20002
- });
20003
- res.end();
20004
- return;
20005
- }
20006
- if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
20007
- const rendered = renderViewerDocument();
20008
- if (rendered.found) {
20009
- res.writeHead(200, {
20010
- "Content-Type": "text/html; charset=utf-8",
20011
- "Content-Security-Policy": rendered.csp,
20012
- "Cache-Control": "no-cache"
20013
- });
20014
- res.end(rendered.html);
20015
- return;
20016
- }
20017
- res.writeHead(404, { "Content-Type": "text/plain" });
20018
- res.end("viewer not found");
20019
- return;
20020
- }
20021
- if (method === "GET" && pathname === "/favicon.svg") {
20022
- const favicon = loadViewerFavicon();
20023
- if (favicon) {
20024
- res.writeHead(200, {
20025
- "Content-Type": "image/svg+xml",
20026
- "Cache-Control": "public, max-age=3600"
20027
- });
20028
- res.end(favicon);
20029
- return;
20030
- }
20031
- res.writeHead(404, { "Content-Type": "text/plain" });
20032
- res.end("favicon not found");
20033
- return;
20034
- }
20035
- try {
20036
- await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
20037
- } catch (err) {
20038
- console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
20039
- json(res, 502, { error: "upstream error" }, req);
20040
- }
20041
- });
20042
- let attempt = 0;
20043
- let currentPort = requestedPort;
20044
- const tryListen = () => {
20045
- server.listen(currentPort, "127.0.0.1");
20046
- };
20047
- server.on("listening", () => {
20048
- if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
20049
- else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
20050
- });
20051
- server.on("error", (err) => {
20052
- if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
20053
- attempt++;
20054
- currentPort = requestedPort + attempt;
20055
- setImmediate(tryListen);
20056
- return;
20057
- }
20058
- if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
20059
- else console.error(`[agentmemory] Viewer error:`, err.message);
20060
- });
20061
- tryListen();
20062
- return server;
20063
- }
20064
- async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
20065
- const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
20066
- const headers = {};
20067
- if (secret) headers["Authorization"] = `Bearer ${secret}`;
20068
- const ct = req.headers["content-type"];
20069
- if (ct) headers["Content-Type"] = ct;
20070
- let body;
20071
- if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
20072
- const controller = new AbortController();
20073
- const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
20074
- let upstream;
20075
- try {
20076
- upstream = await fetch(upstreamUrl, {
20077
- method,
20078
- headers,
20079
- body: body || void 0,
20080
- signal: controller.signal
20081
- });
20082
- clearTimeout(fetchTimeout);
20083
- } catch (err) {
20084
- clearTimeout(fetchTimeout);
20085
- if (err instanceof Error && err.name === "AbortError") {
20086
- json(res, 504, { error: "upstream timeout" }, req);
20087
- return;
20088
- }
20089
- throw err;
20090
- }
20091
- const cors = corsHeaders(req);
20092
- const responseBody = await upstream.text();
20093
- const responseHeaders = { ...cors };
20094
- const upstreamCt = upstream.headers.get("content-type");
20095
- if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
20096
- res.writeHead(upstream.status, responseHeaders);
20097
- res.end(responseBody);
20098
- }
20099
-
20100
20683
  //#endregion
20101
20684
  //#region src/eval/metrics-store.ts
20102
20685
  var MetricsStore = class {
@@ -20234,6 +20817,21 @@ function initMetrics(getMeter) {
20234
20817
 
20235
20818
  //#endregion
20236
20819
  //#region src/index.ts
20820
+ function workerPidfilePath() {
20821
+ return join(homedir(), ".agentmemory", "worker.pid");
20822
+ }
20823
+ function writeWorkerPidfile() {
20824
+ try {
20825
+ const p = workerPidfilePath();
20826
+ mkdirSync(dirname(p), { recursive: true });
20827
+ writeFileSync(p, `${process.pid}\n`, { encoding: "utf-8" });
20828
+ } catch {}
20829
+ }
20830
+ function clearWorkerPidfile() {
20831
+ try {
20832
+ unlinkSync(workerPidfilePath());
20833
+ } catch {}
20834
+ }
20237
20835
  function hasGetMeter(sdk) {
20238
20836
  return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
20239
20837
  }
@@ -20274,6 +20872,7 @@ async function main() {
20274
20872
  framework: "iii-sdk"
20275
20873
  }
20276
20874
  });
20875
+ writeWorkerPidfile();
20277
20876
  const kv = new StateKV(sdk);
20278
20877
  const secret = getEnvVar("AGENTMEMORY_SECRET");
20279
20878
  const metricsStore = new MetricsStore(kv);
@@ -20369,6 +20968,7 @@ async function main() {
20369
20968
  registerMcpEndpoints(sdk, kv, secret);
20370
20969
  const healthMonitor = registerHealthMonitor(sdk, kv);
20371
20970
  const indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
20971
+ setIndexPersistence(indexPersistence);
20372
20972
  const loaded = await indexPersistence.load().catch((err) => {
20373
20973
  console.warn(`[agentmemory] Failed to load persisted index:`, err);
20374
20974
  return null;
@@ -20386,23 +20986,22 @@ async function main() {
20386
20986
  if (mismatches.length > 0) {
20387
20987
  const sample = mismatches.slice(0, 5).map((m) => `${m.obsId} (dim=${m.dim})`).join(", ");
20388
20988
  const distinct = Array.from(seenDimensions).sort((a, b) => a - b).join(", ");
20389
- 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.`);
20989
+ 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.`);
20390
20990
  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.`);
20391
20991
  } else {
20392
20992
  vectorIndex.restoreFrom(loaded.vector);
20393
20993
  bootLog(`Loaded persisted vector index (${vectorIndex.size} vectors)`);
20394
20994
  }
20395
20995
  }
20396
- if (bm25Index.size === 0) {
20397
- const indexCount = await rebuildIndex(kv).catch((err) => {
20398
- console.warn(`[agentmemory] Failed to rebuild search index:`, err);
20399
- return 0;
20400
- });
20996
+ if (bm25Index.size === 0) rebuildIndex(kv).then((indexCount) => {
20401
20997
  if (indexCount > 0) {
20402
20998
  bootLog(`Search index rebuilt: ${indexCount} entries`);
20403
20999
  indexPersistence.scheduleSave();
20404
21000
  }
20405
- } else try {
21001
+ }).catch((err) => {
21002
+ console.warn(`[agentmemory] Failed to rebuild search index:`, err);
21003
+ });
21004
+ else try {
20406
21005
  const memories = await kv.list(KV.memories);
20407
21006
  let backfilled = 0;
20408
21007
  for (const memory of memories) {
@@ -20487,6 +21086,7 @@ async function main() {
20487
21086
  console.warn(`[agentmemory] Failed to save index on shutdown:`, err);
20488
21087
  });
20489
21088
  await sdk.shutdown();
21089
+ clearWorkerPidfile();
20490
21090
  process.exit(0);
20491
21091
  };
20492
21092
  process.on("SIGINT", shutdown);