@agentmemory/agentmemory 0.9.21 → 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 (47) hide show
  1. package/README.md +150 -7
  2. package/dist/cli.d.mts +5 -1
  3. package/dist/cli.d.mts.map +1 -0
  4. package/dist/cli.mjs +103 -692
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/connect-BQQXpyDS.mjs +763 -0
  7. package/dist/connect-BQQXpyDS.mjs.map +1 -0
  8. package/dist/hooks/post-tool-use.mjs +1 -1
  9. package/dist/hooks/post-tool-use.mjs.map +1 -1
  10. package/dist/hooks/stop.mjs +8 -0
  11. package/dist/hooks/stop.mjs.map +1 -1
  12. package/dist/{image-refs-R3tin9MR.mjs → image-refs-CJS5B9Gq.mjs} +2 -2
  13. package/dist/{image-refs-R3tin9MR.mjs.map → image-refs-CJS5B9Gq.mjs.map} +1 -1
  14. package/dist/{image-store-DyrKZKqZ.mjs → image-store-CdE0amb1.mjs} +1 -1
  15. package/dist/index.mjs +450 -242
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/logger-xlVlvCWX.mjs +43 -0
  18. package/dist/logger-xlVlvCWX.mjs.map +1 -0
  19. package/dist/schema-BkALl7Z_.mjs +74 -0
  20. package/dist/schema-BkALl7Z_.mjs.map +1 -0
  21. package/dist/{src-D5arboxc.mjs → src-gpTAJuBy.mjs} +428 -243
  22. package/dist/src-gpTAJuBy.mjs.map +1 -0
  23. package/dist/{standalone-C7BgzzIN.mjs → standalone-C4i7ktpn.mjs} +18 -6
  24. package/dist/standalone-C4i7ktpn.mjs.map +1 -0
  25. package/dist/standalone.d.mts.map +1 -1
  26. package/dist/standalone.mjs +15 -4
  27. package/dist/standalone.mjs.map +1 -1
  28. package/dist/{tools-registry-CRTWUFw9.mjs → tools-registry-B7Y6nJsr.mjs} +36 -11
  29. package/dist/tools-registry-B7Y6nJsr.mjs.map +1 -0
  30. package/dist/version-DvQMNbEH.mjs +6 -0
  31. package/dist/version-DvQMNbEH.mjs.map +1 -0
  32. package/dist/viewer/index.html +77 -9
  33. package/package.json +6 -4
  34. package/plugin/.claude-plugin/plugin.json +1 -1
  35. package/plugin/.codex-plugin/plugin.json +1 -1
  36. package/plugin/.mcp.json +3 -2
  37. package/plugin/opencode/agentmemory-capture.ts +34 -9
  38. package/plugin/scripts/diagnostics.d.mts +17 -0
  39. package/plugin/scripts/diagnostics.d.mts.map +1 -0
  40. package/plugin/scripts/diagnostics.mjs.map +1 -0
  41. package/plugin/scripts/post-tool-use.mjs +1 -1
  42. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  43. package/plugin/scripts/stop.mjs +8 -0
  44. package/plugin/scripts/stop.mjs.map +1 -1
  45. package/dist/src-D5arboxc.mjs.map +0 -1
  46. package/dist/standalone-C7BgzzIN.mjs.map +0 -1
  47. package/dist/tools-registry-CRTWUFw9.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 {
@@ -175,8 +183,8 @@ function loadClaudeBridgeConfig() {
175
183
  const lineBudget = safeParseInt(env["CLAUDE_MEMORY_LINE_BUDGET"], 200);
176
184
  let memoryFilePath = "";
177
185
  if (enabled && projectPath) {
178
- const safePath = projectPath.replace(/[/\\]/g, "-").replace(/^-/, "");
179
- memoryFilePath = join(homedir(), ".claude", "projects", safePath, "memory", "MEMORY.md");
186
+ const safePath = projectPath.replace(/[/\\]/g, "-");
187
+ memoryFilePath = join(homedir(), ".claude", "projects", safePath, "MEMORY.md");
180
188
  }
181
189
  return {
182
190
  enabled,
@@ -196,6 +204,23 @@ function loadTeamConfig() {
196
204
  mode: env["TEAM_MODE"] === "shared" ? "shared" : "private"
197
205
  };
198
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
+ }
199
224
  function loadSnapshotConfig() {
200
225
  const env = getMergedEnv();
201
226
  return {
@@ -453,13 +478,25 @@ function v1AzureUrl(baseUrl, path) {
453
478
  url.pathname = `${url.pathname.replace(/\/?openai(?:\/v1)?\/?$/, "").replace(/\/+$/, "")}/openai/v1/${route}`;
454
479
  return url.toString();
455
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
+ }
456
493
  function buildChatUrl(baseUrl, isAzure, azureApiVersion) {
457
494
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/chat/completions", azureApiVersion) : v1AzureUrl(baseUrl, "/chat/completions");
458
- return `${baseUrl}/v1/chat/completions`;
495
+ return appendOpenAIRoute(baseUrl, "/chat/completions");
459
496
  }
460
497
  function buildEmbeddingUrl(baseUrl, isAzure, azureApiVersion) {
461
498
  if (isAzure) return azureStyleOf(baseUrl) === "legacy" ? legacyAzureUrl(baseUrl, "/embeddings", azureApiVersion) : v1AzureUrl(baseUrl, "/embeddings");
462
- return `${baseUrl}/v1/embeddings`;
499
+ return appendOpenAIRoute(baseUrl, "/embeddings");
463
500
  }
464
501
  function buildAuthHeaders(apiKey, isAzure) {
465
502
  if (isAzure) return {
@@ -541,6 +578,7 @@ var OpenAIProvider = class {
541
578
  const body = {
542
579
  model: this.model,
543
580
  max_tokens: this.maxTokens,
581
+ stream: false,
544
582
  messages: [{
545
583
  role: "system",
546
584
  content: systemPrompt
@@ -569,7 +607,7 @@ var OpenAIProvider = class {
569
607
  const message = data.choices?.[0]?.message;
570
608
  const content = message?.content;
571
609
  if (content) return content;
572
- const reasoning = message?.reasoning;
610
+ const reasoning = message?.reasoning ?? message?.reasoning_content;
573
611
  if (reasoning) return reasoning;
574
612
  throw new Error(`OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`);
575
613
  }
@@ -2514,6 +2552,24 @@ var SearchIndex = class SearchIndex {
2514
2552
  has(id) {
2515
2553
  return this.entries.has(id);
2516
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
+ }
2517
2573
  search(query, limit = 20) {
2518
2574
  const rawTerms = this.tokenize(query.toLowerCase());
2519
2575
  if (rawTerms.length === 0) return [];
@@ -2866,6 +2922,7 @@ function buildSyntheticCompression(raw) {
2866
2922
  };
2867
2923
  if (raw.modality) result.modality = raw.modality;
2868
2924
  if (raw.imageData) result.imageData = raw.imageData;
2925
+ if (raw.agentId) result.agentId = raw.agentId;
2869
2926
  return result;
2870
2927
  }
2871
2928
 
@@ -2955,6 +3012,16 @@ function setVectorIndex(idx) {
2955
3012
  function setEmbeddingProvider(provider) {
2956
3013
  currentEmbeddingProvider = provider;
2957
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
+ }
2958
3025
  const EMBED_MAX_CHARS = 16e3;
2959
3026
  function clipEmbedInput(text) {
2960
3027
  if (text.length <= EMBED_MAX_CHARS) return text;
@@ -3349,6 +3416,9 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3349
3416
  error: `Session observation limit reached (${maxObservationsPerSession})`
3350
3417
  };
3351
3418
  }
3419
+ const existingSession = await kv.get(KV.sessions, payload.sessionId);
3420
+ const inheritedAgentId = existingSession ? existingSession.agentId : getAgentId();
3421
+ if (inheritedAgentId) raw.agentId = inheritedAgentId;
3352
3422
  if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
3353
3423
  const { saveImageToDisk } = await Promise.resolve().then(() => image_store_exports);
3354
3424
  const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
@@ -3400,7 +3470,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3400
3470
  },
3401
3471
  action: TriggerAction.Void()
3402
3472
  });
3403
- const session = await kv.get(KV.sessions, payload.sessionId);
3473
+ const session = existingSession;
3404
3474
  if (session) {
3405
3475
  const updates = [{
3406
3476
  type: "set",
@@ -3420,6 +3490,20 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
3420
3490
  });
3421
3491
  }
3422
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
+ });
3423
3507
  }
3424
3508
  if (isAutoCompressEnabled()) await sdk.trigger({
3425
3509
  function_id: "mem::compress",
@@ -4679,7 +4763,8 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
4679
4763
  confidence: qualityScore / 100,
4680
4764
  ...hasImage ? { modality: data.raw.modality } : {},
4681
4765
  ...imageDescription ? { imageDescription } : {},
4682
- ...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
4766
+ ...data.raw.imageData ? { imageRef: data.raw.imageData } : {},
4767
+ ...data.raw.agentId ? { agentId: data.raw.agentId } : {}
4683
4768
  };
4684
4769
  await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
4685
4770
  try {
@@ -5650,6 +5735,7 @@ function registerRememberFunction(sdk, kv) {
5650
5735
  break;
5651
5736
  }
5652
5737
  }
5738
+ const callAgentId = typeof data.agentId === "string" && data.agentId.trim().length > 0 ? data.agentId.trim().slice(0, 128) : getAgentId();
5653
5739
  const memory = {
5654
5740
  id: generateId("mem"),
5655
5741
  createdAt: now,
@@ -5665,7 +5751,8 @@ function registerRememberFunction(sdk, kv) {
5665
5751
  parentId: supersededId,
5666
5752
  supersedes: supersededId ? [supersededId] : [],
5667
5753
  sourceObservationIds: (data.sourceObservationIds || []).filter((id) => typeof id === "string" && id.length > 0),
5668
- isLatest: true
5754
+ isLatest: true,
5755
+ ...callAgentId ? { agentId: callAgentId } : {}
5669
5756
  };
5670
5757
  if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) memory.forgetAfter = new Date(Date.now() + data.ttlDays * 864e5).toISOString();
5671
5758
  if (supersededMemory) {
@@ -5711,6 +5798,8 @@ function registerRememberFunction(sdk, kv) {
5711
5798
  await kv.delete(KV.memories, data.memoryId);
5712
5799
  if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
5713
5800
  await deleteAccessLog(kv, data.memoryId);
5801
+ getSearchIndex().remove(data.memoryId);
5802
+ vectorIndexRemove(data.memoryId);
5714
5803
  deletedMemoryIds.push(data.memoryId);
5715
5804
  deleted++;
5716
5805
  }
@@ -5719,6 +5808,8 @@ function registerRememberFunction(sdk, kv) {
5719
5808
  await kv.delete(KV.observations(data.sessionId), obsId);
5720
5809
  if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5721
5810
  if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5811
+ getSearchIndex().remove(obsId);
5812
+ vectorIndexRemove(obsId);
5722
5813
  deletedObservationIds.push(obsId);
5723
5814
  deleted++;
5724
5815
  }
@@ -5728,6 +5819,8 @@ function registerRememberFunction(sdk, kv) {
5728
5819
  await kv.delete(KV.observations(data.sessionId), obs.id);
5729
5820
  if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5730
5821
  if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5822
+ getSearchIndex().remove(obs.id);
5823
+ vectorIndexRemove(obs.id);
5731
5824
  deletedObservationIds.push(obs.id);
5732
5825
  deleted++;
5733
5826
  }
@@ -5736,14 +5829,17 @@ function registerRememberFunction(sdk, kv) {
5736
5829
  deletedSession = true;
5737
5830
  deleted += 2;
5738
5831
  }
5739
- if (deleted > 0) await recordAudit(kv, "forget", "mem::forget", [...deletedMemoryIds, ...deletedObservationIds], {
5740
- sessionId: data.sessionId,
5741
- deleted,
5742
- memoriesDeleted: deletedMemoryIds.length,
5743
- observationsDeleted: deletedObservationIds.length,
5744
- sessionDeleted: deletedSession,
5745
- reason: "user-initiated forget"
5746
- });
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
+ }
5747
5843
  logger.info("Memory forgotten", { deleted });
5748
5844
  return {
5749
5845
  success: true,
@@ -6258,6 +6354,9 @@ async function findByKeyword(kv, keyword, project) {
6258
6354
  const LESSON_CONTENT_PREVIEW_CHARS = 240;
6259
6355
  function registerSmartSearchFunction(sdk, kv, searchFn) {
6260
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);
6261
6360
  if (data.expandIds && data.expandIds.length > 0) {
6262
6361
  const raw = data.expandIds.slice(0, 20);
6263
6362
  const items = raw.map((entry) => {
@@ -6278,17 +6377,19 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6278
6377
  observation: obs
6279
6378
  } : null)));
6280
6379
  for (const r of results) if (r) expanded.push(r);
6281
- 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));
6282
6382
  const truncated = data.expandIds.length > raw.length;
6283
6383
  logger.info("Smart search expanded", {
6284
6384
  requested: data.expandIds.length,
6285
6385
  attempted: raw.length,
6286
- returned: expanded.length,
6386
+ returned: scoped.length,
6387
+ filteredOutOfScope: expanded.length - scoped.length,
6287
6388
  truncated
6288
6389
  });
6289
6390
  return {
6290
6391
  mode: "expanded",
6291
- results: expanded,
6392
+ results: scoped,
6292
6393
  truncated
6293
6394
  };
6294
6395
  }
@@ -6300,8 +6401,9 @@ function registerSmartSearchFunction(sdk, kv, searchFn) {
6300
6401
  const limit = Math.max(1, Math.min(data.limit ?? 20, 100));
6301
6402
  const lessonLimit = Math.min(limit, 10);
6302
6403
  const includeLessons = data.includeLessons !== false;
6303
- const [hybridResults, lessons] = await Promise.all([searchFn(data.query, limit), includeLessons ? recallLessons(sdk, data.query, lessonLimit, data.project) : Promise.resolve([])]);
6304
- const compact = hybridResults.map((r) => ({
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) => ({
6305
6407
  obsId: r.observation.id,
6306
6408
  sessionId: r.sessionId,
6307
6409
  title: r.observation.title,
@@ -6480,6 +6582,8 @@ function registerAutoForgetFunction(sdk, kv) {
6480
6582
  timestamp: mem.forgetAfter
6481
6583
  });
6482
6584
  await deleteAccessLog(kv, mem.id);
6585
+ getSearchIndex().remove(mem.id);
6586
+ vectorIndexRemove(mem.id);
6483
6587
  }
6484
6588
  }
6485
6589
  }
@@ -6557,10 +6661,13 @@ function registerAutoForgetFunction(sdk, kv) {
6557
6661
  sessionId: sessions[i].id,
6558
6662
  timestamp: obs.timestamp
6559
6663
  });
6664
+ getSearchIndex().remove(obs.id);
6665
+ vectorIndexRemove(obs.id);
6560
6666
  }
6561
6667
  }
6562
6668
  }
6563
6669
  }
6670
+ if (!dryRun && (result.ttlExpired.length > 0 || result.lowValueObs.length > 0)) await flushIndexSave();
6564
6671
  logger.info("Auto-forget complete", {
6565
6672
  ttlExpired: result.ttlExpired.length,
6566
6673
  contradictions: result.contradictions.length,
@@ -6573,7 +6680,7 @@ function registerAutoForgetFunction(sdk, kv) {
6573
6680
 
6574
6681
  //#endregion
6575
6682
  //#region src/version.ts
6576
- const VERSION = "0.9.21";
6683
+ const VERSION = "0.9.22";
6577
6684
 
6578
6685
  //#endregion
6579
6686
  //#region src/functions/export-import.ts
@@ -6712,7 +6819,8 @@ function registerExportImportFunction(sdk, kv) {
6712
6819
  "0.9.18",
6713
6820
  "0.9.19",
6714
6821
  "0.9.20",
6715
- "0.9.21"
6822
+ "0.9.21",
6823
+ "0.9.22"
6716
6824
  ]).has(importData.version)) return {
6717
6825
  success: false,
6718
6826
  error: `Unsupported export version: ${importData.version}`
@@ -7238,12 +7346,12 @@ function parseGraphXml(xml, observationIds) {
7238
7346
  const nodes = [];
7239
7347
  const edges = [];
7240
7348
  const now = (/* @__PURE__ */ new Date()).toISOString();
7241
- const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/entity>/g;
7349
+ const entityRegex = /<entity\s+type="([^"]+)"\s+name="([^"]+)"[^>]*?(?:\/>|>([\s\S]*?)<\/entity>)/g;
7242
7350
  let match;
7243
7351
  while ((match = entityRegex.exec(xml)) !== null) {
7244
7352
  const type = match[1];
7245
7353
  const name = match[2];
7246
- const propsBlock = match[3];
7354
+ const propsBlock = match[3] ?? "";
7247
7355
  const properties = {};
7248
7356
  const propRegex = /<property\s+key="([^"]+)">([^<]*)<\/property>/g;
7249
7357
  let propMatch;
@@ -7771,8 +7879,11 @@ function registerGovernanceFunction(sdk, kv) {
7771
7879
  for (const id of data.memoryIds) if (await kv.get(KV.memories, id)) {
7772
7880
  await kv.delete(KV.memories, id);
7773
7881
  await deleteAccessLog(kv, id);
7882
+ getSearchIndex().remove(id);
7883
+ vectorIndexRemove(id);
7774
7884
  deleted++;
7775
7885
  }
7886
+ if (deleted > 0) await flushIndexSave();
7776
7887
  await recordAudit(kv, "delete", "mem::governance-delete", data.memoryIds, {
7777
7888
  reason: data.reason || "manual deletion",
7778
7889
  deleted
@@ -7825,6 +7936,8 @@ function registerGovernanceFunction(sdk, kv) {
7825
7936
  (await Promise.allSettled(batch.map(async (mem) => {
7826
7937
  await kv.delete(KV.memories, mem.id);
7827
7938
  await deleteAccessLog(kv, mem.id);
7939
+ getSearchIndex().remove(mem.id);
7940
+ vectorIndexRemove(mem.id);
7828
7941
  }))).forEach((result, j) => {
7829
7942
  const mem = batch[j];
7830
7943
  if (result.status === "fulfilled") successfulIds.push(mem.id);
@@ -7840,6 +7953,7 @@ function registerGovernanceFunction(sdk, kv) {
7840
7953
  }
7841
7954
  });
7842
7955
  }
7956
+ if (successfulIds.length > 0) await flushIndexSave();
7843
7957
  await safeAudit(kv, "delete", "mem::governance-bulk", successfulIds, {
7844
7958
  filter: data,
7845
7959
  deleted: successfulIds.length,
@@ -13239,6 +13353,8 @@ function registerRetentionFunctions(sdk, kv) {
13239
13353
  await kv.delete(scope, candidate.memoryId);
13240
13354
  await kv.delete(KV.retentionScores, candidate.memoryId);
13241
13355
  await deleteAccessLog(kv, candidate.memoryId);
13356
+ getSearchIndex().remove(candidate.memoryId);
13357
+ vectorIndexRemove(candidate.memoryId);
13242
13358
  evicted++;
13243
13359
  evictedIds.push(candidate.memoryId);
13244
13360
  if (resolvedSource === "semantic") evictedSemantic++;
@@ -13246,13 +13362,16 @@ function registerRetentionFunctions(sdk, kv) {
13246
13362
  } catch {
13247
13363
  continue;
13248
13364
  }
13249
- if (evicted > 0) await recordAudit(kv, "delete", "mem::retention-evict", evictedIds, {
13250
- threshold,
13251
- evicted,
13252
- evictedEpisodic,
13253
- evictedSemantic,
13254
- reason: "retention score below threshold"
13255
- });
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
+ }
13256
13375
  logger.info("Retention-based eviction complete", {
13257
13376
  evicted,
13258
13377
  evictedEpisodic,
@@ -14156,6 +14275,221 @@ function renderViewerDocument() {
14156
14275
  };
14157
14276
  }
14158
14277
 
14278
+ //#endregion
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 {}
14290
+ return null;
14291
+ }
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];
14315
+ return {
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"
14320
+ };
14321
+ }
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
+
14159
14493
  //#endregion
14160
14494
  //#region src/triggers/api.ts
14161
14495
  function parseOptionalInt(raw) {
@@ -14241,7 +14575,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14241
14575
  status_code: 200,
14242
14576
  body: {
14243
14577
  status: "ok",
14244
- service: "agentmemory"
14578
+ service: "agentmemory",
14579
+ viewerPort: getBoundViewerPort(),
14580
+ viewerSkipped: getViewerSkipped()
14245
14581
  }
14246
14582
  }));
14247
14583
  sdk.registerTrigger({
@@ -14336,7 +14672,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14336
14672
  version: VERSION,
14337
14673
  health: health || null,
14338
14674
  functionMetrics,
14339
- circuitBreaker
14675
+ circuitBreaker,
14676
+ viewerPort: getBoundViewerPort(),
14677
+ viewerSkipped: getViewerSkipped()
14340
14678
  }
14341
14679
  };
14342
14680
  });
@@ -14590,6 +14928,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14590
14928
  body: { error: "sessionId, project, and cwd are required non-empty strings" }
14591
14929
  };
14592
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();
14593
14932
  const session = {
14594
14933
  id: sessionId,
14595
14934
  project,
@@ -14598,7 +14937,8 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14598
14937
  status: "active",
14599
14938
  observationCount: 0,
14600
14939
  ...title ? { summary: title.slice(0, 200) } : {},
14601
- ...title ? { firstPrompt: title.slice(0, 200) } : {}
14940
+ ...title ? { firstPrompt: title.slice(0, 200) } : {},
14941
+ ...agentId ? { agentId } : {}
14602
14942
  };
14603
14943
  await kv.set(KV.sessions, sessionId, session);
14604
14944
  return {
@@ -14785,9 +15125,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14785
15125
  sdk.registerFunction("api::sessions", async (req) => {
14786
15126
  const authErr = checkAuth(req, secret);
14787
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);
14788
15132
  return {
14789
15133
  status_code: 200,
14790
- body: { sessions: await kv.list(KV.sessions) }
15134
+ body: { sessions: filterAgentId ? sessions.filter((s) => s.agentId === filterAgentId) : sessions }
14791
15135
  };
14792
15136
  });
14793
15137
  sdk.registerTrigger({
@@ -14806,9 +15150,13 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
14806
15150
  status_code: 400,
14807
15151
  body: { error: "sessionId required" }
14808
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);
14809
15157
  return {
14810
15158
  status_code: 200,
14811
- body: { observations: await kv.list(KV.observations(sessionId)) }
15159
+ body: { observations: filterAgentId ? observations.filter((o) => o.agentId === filterAgentId) : observations }
14812
15160
  };
14813
15161
  });
14814
15162
  sdk.registerTrigger({
@@ -15084,11 +15432,22 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15084
15432
  sdk.registerFunction("api::export", async (req) => {
15085
15433
  const authErr = checkAuth(req, secret);
15086
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
+ }
15087
15446
  return {
15088
15447
  status_code: 200,
15089
15448
  body: await sdk.trigger({
15090
15449
  function_id: "mem::export",
15091
- payload: {}
15450
+ payload
15092
15451
  })
15093
15452
  };
15094
15453
  });
@@ -15574,9 +15933,35 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
15574
15933
  const authErr = checkAuth(req, secret);
15575
15934
  if (authErr) return authErr;
15576
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;
15577
15957
  return {
15578
15958
  status_code: 200,
15579
- 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
+ }
15580
15965
  };
15581
15966
  });
15582
15967
  sdk.registerTrigger({
@@ -18656,8 +19041,8 @@ function getAllTools() {
18656
19041
  ];
18657
19042
  }
18658
19043
  function getVisibleTools() {
18659
- if ((process.env["AGENTMEMORY_TOOLS"] || "core") === "all") return getAllTools();
18660
- 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();
18661
19046
  }
18662
19047
 
18663
19048
  //#endregion
@@ -20295,201 +20680,6 @@ function registerMcpEndpoints(sdk, kv, secret) {
20295
20680
  });
20296
20681
  }
20297
20682
 
20298
- //#endregion
20299
- //#region src/viewer/server.ts
20300
- function loadViewerFavicon() {
20301
- const base = dirname(fileURLToPath(import.meta.url));
20302
- const candidates = [
20303
- join(base, "..", "src", "viewer", "favicon.svg"),
20304
- join(base, "..", "viewer", "favicon.svg"),
20305
- join(base, "viewer", "favicon.svg")
20306
- ];
20307
- for (const path of candidates) try {
20308
- return readFileSync(path);
20309
- } catch {}
20310
- return null;
20311
- }
20312
- 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());
20313
- const ALLOWED_HOSTS_OVERRIDE = (process.env.VIEWER_ALLOWED_HOSTS || "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
20314
- function buildAllowedHosts(origins, listenPort) {
20315
- const hosts = /* @__PURE__ */ new Set();
20316
- for (const o of origins) try {
20317
- const parsed = new URL(o);
20318
- if (parsed.host) hosts.add(parsed.host.toLowerCase());
20319
- } catch {}
20320
- hosts.add(`localhost:${listenPort}`);
20321
- hosts.add(`127.0.0.1:${listenPort}`);
20322
- hosts.add(`[::1]:${listenPort}`);
20323
- for (const h of ALLOWED_HOSTS_OVERRIDE) hosts.add(h);
20324
- return hosts;
20325
- }
20326
- function isHostAllowed(headerHost, allowed) {
20327
- if (typeof headerHost !== "string") return false;
20328
- const lower = headerHost.toLowerCase().trim();
20329
- if (!lower) return false;
20330
- return allowed.has(lower);
20331
- }
20332
- function corsHeaders(req) {
20333
- const origin = req.headers.origin || "";
20334
- const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
20335
- return {
20336
- "Access-Control-Allow-Origin": allowed,
20337
- "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
20338
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
20339
- Vary: "Origin"
20340
- };
20341
- }
20342
- function json(res, status, data, req) {
20343
- const body = JSON.stringify(data);
20344
- const cors = req ? corsHeaders(req) : {
20345
- "Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
20346
- Vary: "Origin"
20347
- };
20348
- res.writeHead(status, {
20349
- ...cors,
20350
- "Content-Type": "application/json"
20351
- });
20352
- res.end(body);
20353
- }
20354
- function readBody(req) {
20355
- return new Promise((resolve, reject) => {
20356
- let data = "";
20357
- let size = 0;
20358
- req.on("data", (chunk) => {
20359
- size += chunk.length;
20360
- if (size > 1e6) {
20361
- req.destroy();
20362
- reject(/* @__PURE__ */ new Error("too large"));
20363
- return;
20364
- }
20365
- data += chunk.toString();
20366
- });
20367
- req.on("end", () => resolve(data));
20368
- req.on("error", reject);
20369
- });
20370
- }
20371
- const MAX_VIEWER_PORT_RETRIES = 10;
20372
- function startViewerServer(port, _kv, _sdk, secret, restPort) {
20373
- const resolvedRestPort = restPort ?? port - 2;
20374
- const requestedPort = port;
20375
- let allowedHosts = null;
20376
- const server = createServer(async (req, res) => {
20377
- if (!allowedHosts) {
20378
- const addr = server.address();
20379
- allowedHosts = buildAllowedHosts(ALLOWED_ORIGINS, addr && typeof addr === "object" && "port" in addr ? addr.port : port);
20380
- }
20381
- if (!isHostAllowed(req.headers.host, allowedHosts)) {
20382
- res.writeHead(403, { "Content-Type": "text/plain" });
20383
- res.end("forbidden host");
20384
- return;
20385
- }
20386
- const raw = req.url || "/";
20387
- const qIdx = raw.indexOf("?");
20388
- const pathname = qIdx >= 0 ? raw.slice(0, qIdx) : raw;
20389
- const qs = qIdx >= 0 ? raw.slice(qIdx + 1) : "";
20390
- const method = req.method || "GET";
20391
- if (method === "OPTIONS") {
20392
- res.writeHead(204, {
20393
- ...corsHeaders(req),
20394
- "Access-Control-Max-Age": "86400"
20395
- });
20396
- res.end();
20397
- return;
20398
- }
20399
- if (method === "GET" && (pathname === "/" || pathname === "/viewer" || pathname === "/agentmemory/viewer")) {
20400
- const rendered = renderViewerDocument();
20401
- if (rendered.found) {
20402
- res.writeHead(200, {
20403
- "Content-Type": "text/html; charset=utf-8",
20404
- "Content-Security-Policy": rendered.csp,
20405
- "Cache-Control": "no-cache"
20406
- });
20407
- res.end(rendered.html);
20408
- return;
20409
- }
20410
- res.writeHead(404, { "Content-Type": "text/plain" });
20411
- res.end("viewer not found");
20412
- return;
20413
- }
20414
- if (method === "GET" && pathname === "/favicon.svg") {
20415
- const favicon = loadViewerFavicon();
20416
- if (favicon) {
20417
- res.writeHead(200, {
20418
- "Content-Type": "image/svg+xml",
20419
- "Cache-Control": "public, max-age=3600"
20420
- });
20421
- res.end(favicon);
20422
- return;
20423
- }
20424
- res.writeHead(404, { "Content-Type": "text/plain" });
20425
- res.end("favicon not found");
20426
- return;
20427
- }
20428
- try {
20429
- await proxyToRestApi(resolvedRestPort, pathname, qs, method, req, res, secret);
20430
- } catch (err) {
20431
- console.error(`[viewer] proxy error on ${method} ${pathname}:`, err);
20432
- json(res, 502, { error: "upstream error" }, req);
20433
- }
20434
- });
20435
- let attempt = 0;
20436
- let currentPort = requestedPort;
20437
- const tryListen = () => {
20438
- server.listen(currentPort, "127.0.0.1");
20439
- };
20440
- server.on("listening", () => {
20441
- if (currentPort === requestedPort) console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
20442
- else console.log(`[agentmemory] Viewer started on http://localhost:${currentPort} (fallback from ${requestedPort})`);
20443
- });
20444
- server.on("error", (err) => {
20445
- if (err.code === "EADDRINUSE" && attempt < MAX_VIEWER_PORT_RETRIES) {
20446
- attempt++;
20447
- currentPort = requestedPort + attempt;
20448
- setImmediate(tryListen);
20449
- return;
20450
- }
20451
- if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`);
20452
- else console.error(`[agentmemory] Viewer error:`, err.message);
20453
- });
20454
- tryListen();
20455
- return server;
20456
- }
20457
- async function proxyToRestApi(restPort, pathname, qs, method, req, res, secret) {
20458
- const upstreamUrl = `http://127.0.0.1:${restPort}${pathname.startsWith("/agentmemory/") ? pathname : `/agentmemory${pathname.startsWith("/") ? pathname : "/" + pathname}`}${qs ? "?" + qs : ""}`;
20459
- const headers = {};
20460
- if (secret) headers["Authorization"] = `Bearer ${secret}`;
20461
- const ct = req.headers["content-type"];
20462
- if (ct) headers["Content-Type"] = ct;
20463
- let body;
20464
- if (method === "POST" || method === "PUT" || method === "DELETE" || method === "PATCH") body = await readBody(req);
20465
- const controller = new AbortController();
20466
- const fetchTimeout = setTimeout(() => controller.abort(), 1e4);
20467
- let upstream;
20468
- try {
20469
- upstream = await fetch(upstreamUrl, {
20470
- method,
20471
- headers,
20472
- body: body || void 0,
20473
- signal: controller.signal
20474
- });
20475
- clearTimeout(fetchTimeout);
20476
- } catch (err) {
20477
- clearTimeout(fetchTimeout);
20478
- if (err instanceof Error && err.name === "AbortError") {
20479
- json(res, 504, { error: "upstream timeout" }, req);
20480
- return;
20481
- }
20482
- throw err;
20483
- }
20484
- const cors = corsHeaders(req);
20485
- const responseBody = await upstream.text();
20486
- const responseHeaders = { ...cors };
20487
- const upstreamCt = upstream.headers.get("content-type");
20488
- if (upstreamCt) responseHeaders["Content-Type"] = upstreamCt;
20489
- res.writeHead(upstream.status, responseHeaders);
20490
- res.end(responseBody);
20491
- }
20492
-
20493
20683
  //#endregion
20494
20684
  //#region src/eval/metrics-store.ts
20495
20685
  var MetricsStore = class {
@@ -20627,6 +20817,21 @@ function initMetrics(getMeter) {
20627
20817
 
20628
20818
  //#endregion
20629
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
+ }
20630
20835
  function hasGetMeter(sdk) {
20631
20836
  return typeof sdk === "object" && sdk !== null && "getMeter" in sdk && typeof sdk.getMeter === "function";
20632
20837
  }
@@ -20667,6 +20872,7 @@ async function main() {
20667
20872
  framework: "iii-sdk"
20668
20873
  }
20669
20874
  });
20875
+ writeWorkerPidfile();
20670
20876
  const kv = new StateKV(sdk);
20671
20877
  const secret = getEnvVar("AGENTMEMORY_SECRET");
20672
20878
  const metricsStore = new MetricsStore(kv);
@@ -20762,6 +20968,7 @@ async function main() {
20762
20968
  registerMcpEndpoints(sdk, kv, secret);
20763
20969
  const healthMonitor = registerHealthMonitor(sdk, kv);
20764
20970
  const indexPersistence = new IndexPersistence(kv, bm25Index, vectorIndex);
20971
+ setIndexPersistence(indexPersistence);
20765
20972
  const loaded = await indexPersistence.load().catch((err) => {
20766
20973
  console.warn(`[agentmemory] Failed to load persisted index:`, err);
20767
20974
  return null;
@@ -20879,6 +21086,7 @@ async function main() {
20879
21086
  console.warn(`[agentmemory] Failed to save index on shutdown:`, err);
20880
21087
  });
20881
21088
  await sdk.shutdown();
21089
+ clearWorkerPidfile();
20882
21090
  process.exit(0);
20883
21091
  };
20884
21092
  process.on("SIGINT", shutdown);