@agentmemory/agentmemory 0.9.1 → 0.9.3

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 (72) hide show
  1. package/README.md +40 -13
  2. package/dist/cli.mjs +147 -26
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/hooks/notification.mjs +6 -0
  5. package/dist/hooks/notification.mjs.map +1 -1
  6. package/dist/hooks/post-tool-failure.mjs +6 -0
  7. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  8. package/dist/hooks/post-tool-use.mjs +35 -1
  9. package/dist/hooks/post-tool-use.mjs.map +1 -1
  10. package/dist/hooks/pre-compact.mjs +6 -0
  11. package/dist/hooks/pre-compact.mjs.map +1 -1
  12. package/dist/hooks/pre-tool-use.mjs +6 -0
  13. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  14. package/dist/hooks/prompt-submit.mjs +6 -0
  15. package/dist/hooks/prompt-submit.mjs.map +1 -1
  16. package/dist/hooks/session-end.mjs +6 -0
  17. package/dist/hooks/session-end.mjs.map +1 -1
  18. package/dist/hooks/session-start.mjs +6 -0
  19. package/dist/hooks/session-start.mjs.map +1 -1
  20. package/dist/hooks/stop.mjs +6 -0
  21. package/dist/hooks/stop.mjs.map +1 -1
  22. package/dist/hooks/subagent-start.mjs +6 -0
  23. package/dist/hooks/subagent-start.mjs.map +1 -1
  24. package/dist/hooks/subagent-stop.mjs +6 -0
  25. package/dist/hooks/subagent-stop.mjs.map +1 -1
  26. package/dist/hooks/task-completed.mjs +6 -0
  27. package/dist/hooks/task-completed.mjs.map +1 -1
  28. package/dist/image-refs-CESf9ndJ.mjs +3 -0
  29. package/dist/image-store-DGvZMMrI.mjs +3 -0
  30. package/dist/index.mjs +2100 -157
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/{src-Dw_gJcCy.mjs → src-3Snd7D3T.mjs} +2021 -267
  33. package/dist/src-3Snd7D3T.mjs.map +1 -0
  34. package/dist/{standalone-BEWvWM5P.mjs → standalone-BG9uPsDK.mjs} +2 -2
  35. package/dist/{standalone-BEWvWM5P.mjs.map → standalone-BG9uPsDK.mjs.map} +1 -1
  36. package/dist/standalone.mjs +136 -2
  37. package/dist/standalone.mjs.map +1 -1
  38. package/dist/{tools-registry-BvWNlj6u.mjs → tools-registry-m8Ofn9vV.mjs} +166 -12
  39. package/dist/tools-registry-m8Ofn9vV.mjs.map +1 -0
  40. package/dist/viewer/index.html +528 -68
  41. package/package.json +5 -3
  42. package/plugin/.claude-plugin/plugin.json +2 -2
  43. package/plugin/scripts/notification.mjs +6 -0
  44. package/plugin/scripts/notification.mjs.map +1 -1
  45. package/plugin/scripts/post-tool-failure.mjs +6 -0
  46. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  47. package/plugin/scripts/post-tool-use.mjs +35 -1
  48. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  49. package/plugin/scripts/pre-compact.mjs +6 -0
  50. package/plugin/scripts/pre-compact.mjs.map +1 -1
  51. package/plugin/scripts/pre-tool-use.mjs +6 -0
  52. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  53. package/plugin/scripts/prompt-submit.mjs +6 -0
  54. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  55. package/plugin/scripts/session-end.mjs +6 -0
  56. package/plugin/scripts/session-end.mjs.map +1 -1
  57. package/plugin/scripts/session-start.mjs +6 -0
  58. package/plugin/scripts/session-start.mjs.map +1 -1
  59. package/plugin/scripts/stop.mjs +6 -0
  60. package/plugin/scripts/stop.mjs.map +1 -1
  61. package/plugin/scripts/subagent-start.mjs +6 -0
  62. package/plugin/scripts/subagent-start.mjs.map +1 -1
  63. package/plugin/scripts/subagent-stop.mjs +6 -0
  64. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  65. package/plugin/scripts/task-completed.mjs +6 -0
  66. package/plugin/scripts/task-completed.mjs.map +1 -1
  67. package/dist/src-Dw_gJcCy.mjs.map +0 -1
  68. package/dist/tools-registry-BvWNlj6u.mjs.map +0 -1
  69. package/dist/transformers-BX_tgxdO.mjs +0 -38684
  70. package/dist/transformers-BX_tgxdO.mjs.map +0 -1
  71. package/dist/transformers-KMm1i9no.mjs +0 -38683
  72. package/dist/transformers-KMm1i9no.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -4,16 +4,33 @@ import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
4
4
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import Anthropic from "@anthropic-ai/sdk";
7
+ import { lstat, mkdir, open, readFile, readdir, stat, unlink, utimes, writeFile } from "node:fs/promises";
7
8
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
8
9
  import { z } from "zod";
9
10
  import { execFile } from "node:child_process";
10
11
  import { promisify } from "node:util";
11
12
  import { lookup } from "node:dns/promises";
12
13
  import { isIP } from "node:net";
13
- import { lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { createServer } from "node:http";
16
16
 
17
+ //#region \0rolldown/runtime.js
18
+ var __defProp = Object.defineProperty;
19
+ var __exportAll = (all, no_symbols) => {
20
+ let target = {};
21
+ for (var name in all) {
22
+ __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true
25
+ });
26
+ }
27
+ if (!no_symbols) {
28
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
29
+ }
30
+ return target;
31
+ };
32
+
33
+ //#endregion
17
34
  //#region src/config.ts
18
35
  function safeParseInt(value, fallback) {
19
36
  if (!value) return fallback;
@@ -38,37 +55,48 @@ function loadEnvFile() {
38
55
  }
39
56
  return vars;
40
57
  }
58
+ function hasRealValue(v) {
59
+ return typeof v === "string" && v.trim().length > 0;
60
+ }
41
61
  function detectProvider(env) {
42
62
  const maxTokens = parseInt(env["MAX_TOKENS"] || "4096", 10);
43
- if (env["MINIMAX_API_KEY"]) return {
63
+ if (hasRealValue(env["MINIMAX_API_KEY"])) return {
44
64
  provider: "minimax",
45
65
  model: env["MINIMAX_MODEL"] || "MiniMax-M2.7",
46
66
  maxTokens
47
67
  };
48
- if (env["ANTHROPIC_API_KEY"]) return {
68
+ if (hasRealValue(env["ANTHROPIC_API_KEY"])) return {
49
69
  provider: "anthropic",
50
70
  model: env["ANTHROPIC_MODEL"] || "claude-sonnet-4-20250514",
51
71
  maxTokens,
52
72
  baseURL: env["ANTHROPIC_BASE_URL"]
53
73
  };
54
- if (env["GEMINI_API_KEY"] || env["GOOGLE_API_KEY"]) {
55
- if (!env["GEMINI_API_KEY"] && env["GOOGLE_API_KEY"]) process.stderr.write("[agentmemory] GOOGLE_API_KEY detected — treating as GEMINI_API_KEY. Set GEMINI_API_KEY in ~/.agentmemory/.env to silence this warning.\n");
74
+ if (hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"])) {
75
+ if (!hasRealValue(env["GEMINI_API_KEY"]) && hasRealValue(env["GOOGLE_API_KEY"])) process.stderr.write("[agentmemory] GOOGLE_API_KEY detected — treating as GEMINI_API_KEY. Set GEMINI_API_KEY in ~/.agentmemory/.env to silence this warning.\n");
56
76
  return {
57
77
  provider: "gemini",
58
78
  model: env["GEMINI_MODEL"] || "gemini-2.0-flash",
59
79
  maxTokens
60
80
  };
61
81
  }
62
- if (env["OPENROUTER_API_KEY"]) return {
82
+ if (hasRealValue(env["OPENROUTER_API_KEY"])) return {
63
83
  provider: "openrouter",
64
84
  model: env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-4-20250514",
65
85
  maxTokens
66
86
  };
67
- if (env["AGENTMEMORY_AUTO_COMPRESS"] === "true") process.stderr.write("[agentmemory] WARNING: AGENTMEMORY_AUTO_COMPRESS=true but no LLM provider key found (GEMINI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY). Falling back to agent-sdk which shares Claude Code's API quota — this can exhaust a Pro subscription during heavy sessions. Set an API key in ~/.agentmemory/.env to avoid rate limits (#149).\n");
87
+ if (!(env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true")) {
88
+ process.stderr.write("[agentmemory] No LLM provider key found (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_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");
89
+ return {
90
+ provider: "noop",
91
+ model: "noop",
92
+ maxTokens
93
+ };
94
+ }
95
+ process.stderr.write("[agentmemory] WARNING: agent-sdk fallback enabled via AGENTMEMORY_ALLOW_AGENT_SDK=true. This spawns @anthropic-ai/claude-agent-sdk child sessions that can trigger the Stop-hook recursion loop (#149 follow-up). A SDK-child env marker is set to block re-entry, but prefer setting a real API key in ~/.agentmemory/.env instead.\n");
68
96
  return {
69
97
  provider: "agent-sdk",
70
98
  model: "claude-sonnet-4-20250514",
71
- maxTokens: 4096
99
+ maxTokens
72
100
  };
73
101
  }
74
102
  function loadConfig() {
@@ -177,7 +205,16 @@ const VALID_PROVIDERS = new Set([
177
205
  "minimax"
178
206
  ]);
179
207
  function loadFallbackConfig() {
180
- return { providers: (getMergedEnv()["FALLBACK_PROVIDERS"] || "").split(",").map((p) => p.trim()).filter((p) => Boolean(p) && VALID_PROVIDERS.has(p)) };
208
+ const env = getMergedEnv();
209
+ const raw = env["FALLBACK_PROVIDERS"] || "";
210
+ const allowAgentSdk = env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true";
211
+ return { providers: raw.split(",").map((p) => p.trim()).filter((p) => Boolean(p) && VALID_PROVIDERS.has(p)).filter((p) => {
212
+ if (p === "agent-sdk" && !allowAgentSdk) {
213
+ process.stderr.write("[agentmemory] Ignoring FALLBACK_PROVIDERS entry 'agent-sdk' (AGENTMEMORY_ALLOW_AGENT_SDK is not 'true'). The agent-sdk fallback can spawn Claude Agent SDK child sessions that trigger the Stop-hook recursion loop (#149 follow-up). Opt in explicitly with AGENTMEMORY_ALLOW_AGENT_SDK=true if this is intentional.\n");
214
+ return false;
215
+ }
216
+ return true;
217
+ }) };
181
218
  }
182
219
 
183
220
  //#endregion
@@ -191,18 +228,26 @@ var AgentSDKProvider = class {
191
228
  return this.query(systemPrompt, userPrompt);
192
229
  }
193
230
  async query(systemPrompt, userPrompt) {
194
- const { query } = await import("@anthropic-ai/claude-agent-sdk");
195
- const messages = query({
196
- prompt: userPrompt,
197
- options: {
198
- systemPrompt,
199
- maxTurns: 1,
200
- allowedTools: []
201
- }
202
- });
203
- let result = "";
204
- for await (const msg of messages) if (msg.type === "result") result = typeof msg.result === "string" ? msg.result : JSON.stringify(msg.result);
205
- return result;
231
+ if (process.env.AGENTMEMORY_SDK_CHILD === "1") return "";
232
+ const prev = process.env.AGENTMEMORY_SDK_CHILD;
233
+ process.env.AGENTMEMORY_SDK_CHILD = "1";
234
+ try {
235
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
236
+ const messages = query({
237
+ prompt: userPrompt,
238
+ options: {
239
+ systemPrompt,
240
+ maxTurns: 1,
241
+ allowedTools: []
242
+ }
243
+ });
244
+ let result = "";
245
+ for await (const msg of messages) if (msg.type === "result") result = msg.result ?? "";
246
+ return result;
247
+ } finally {
248
+ if (prev === void 0) delete process.env.AGENTMEMORY_SDK_CHILD;
249
+ else process.env.AGENTMEMORY_SDK_CHILD = prev;
250
+ }
206
251
  }
207
252
  };
208
253
 
@@ -227,6 +272,26 @@ var AnthropicProvider = class {
227
272
  async summarize(systemPrompt, userPrompt) {
228
273
  return this.call(systemPrompt, userPrompt);
229
274
  }
275
+ async describeImage(imageData, mimeType, prompt) {
276
+ return (await this.client.messages.create({
277
+ model: this.model,
278
+ max_tokens: this.maxTokens,
279
+ messages: [{
280
+ role: "user",
281
+ content: [{
282
+ type: "image",
283
+ source: {
284
+ type: "base64",
285
+ media_type: mimeType,
286
+ data: imageData
287
+ }
288
+ }, {
289
+ type: "text",
290
+ text: prompt
291
+ }]
292
+ }]
293
+ })).content.find((b) => b.type === "text")?.text ?? "";
294
+ }
230
295
  async call(systemPrompt, userPrompt) {
231
296
  return (await this.client.messages.create({
232
297
  model: this.model,
@@ -301,6 +366,25 @@ var MinimaxProvider = class {
301
366
  }
302
367
  };
303
368
 
369
+ //#endregion
370
+ //#region src/providers/noop.ts
371
+ /**
372
+ * Returns empty strings for every call. Used when no LLM API key is set
373
+ * AND the user has not opted into the agent-sdk fallback via
374
+ * AGENTMEMORY_ALLOW_AGENT_SDK=true. Callers (compress, summarize) must
375
+ * detect the empty result and short-circuit instead of spawning a
376
+ * provider session (#149 / Stop-hook recursion loop fix).
377
+ */
378
+ var NoopProvider = class {
379
+ name = "noop";
380
+ async compress() {
381
+ return "";
382
+ }
383
+ async summarize() {
384
+ return "";
385
+ }
386
+ };
387
+
304
388
  //#endregion
305
389
  //#region src/providers/openrouter.ts
306
390
  var OpenRouterProvider = class {
@@ -512,28 +596,67 @@ var GeminiEmbeddingProvider = class {
512
596
 
513
597
  //#endregion
514
598
  //#region src/providers/embedding/openai.ts
515
- const API_URL$3 = "https://api.openai.com/v1/embeddings";
599
+ const DEFAULT_BASE_URL = "https://api.openai.com";
600
+ const DEFAULT_MODEL$1 = "text-embedding-3-small";
601
+ /**
602
+ * Known OpenAI embedding model dimensions. Extend as new models ship.
603
+ * Override in any case via OPENAI_EMBEDDING_DIMENSIONS for custom or
604
+ * self-hosted OpenAI-compatible endpoints returning non-standard sizes.
605
+ */
606
+ const MODEL_DIMENSIONS = {
607
+ "text-embedding-3-small": 1536,
608
+ "text-embedding-3-large": 3072,
609
+ "text-embedding-ada-002": 1536
610
+ };
611
+ const DEFAULT_DIMENSIONS = MODEL_DIMENSIONS[DEFAULT_MODEL$1] ?? 1536;
612
+ function resolveDimensions(model, override) {
613
+ if (override !== void 0 && override.trim().length > 0) {
614
+ const parsed = parseInt(override, 10);
615
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`OPENAI_EMBEDDING_DIMENSIONS must be a positive integer, got: ${override}`);
616
+ return parsed;
617
+ }
618
+ return MODEL_DIMENSIONS[model] ?? DEFAULT_DIMENSIONS;
619
+ }
620
+ /**
621
+ * OpenAI-compatible embedding provider.
622
+ *
623
+ * Required env vars:
624
+ * OPENAI_API_KEY — API key
625
+ *
626
+ * Optional:
627
+ * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com)
628
+ * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
629
+ * OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
630
+ * custom / self-hosted models not in the
631
+ * MODEL_DIMENSIONS table above)
632
+ */
516
633
  var OpenAIEmbeddingProvider = class {
517
634
  name = "openai";
518
- dimensions = 1536;
635
+ dimensions;
519
636
  apiKey;
637
+ baseUrl;
638
+ model;
520
639
  constructor(apiKey) {
521
640
  this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
522
641
  if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
642
+ this.baseUrl = getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL;
643
+ this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
644
+ this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
523
645
  }
524
646
  async embed(text) {
525
647
  const [result] = await this.embedBatch([text]);
526
648
  return result;
527
649
  }
528
650
  async embedBatch(texts) {
529
- const response = await fetch(API_URL$3, {
651
+ const url = `${this.baseUrl}/v1/embeddings`;
652
+ const response = await fetch(url, {
530
653
  method: "POST",
531
654
  headers: {
532
655
  Authorization: `Bearer ${this.apiKey}`,
533
656
  "Content-Type": "application/json"
534
657
  },
535
658
  body: JSON.stringify({
536
- model: "text-embedding-3-small",
659
+ model: this.model,
537
660
  input: texts
538
661
  })
539
662
  });
@@ -674,7 +797,7 @@ var LocalEmbeddingProvider = class {
674
797
  if (this.extractor) return this.extractor;
675
798
  let transformers;
676
799
  try {
677
- transformers = await import("./transformers-BX_tgxdO.mjs");
800
+ transformers = await import("@xenova/transformers");
678
801
  } catch {
679
802
  throw new Error("Install @xenova/transformers for local embeddings: npm install @xenova/transformers");
680
803
  }
@@ -683,8 +806,86 @@ var LocalEmbeddingProvider = class {
683
806
  }
684
807
  };
685
808
 
809
+ //#endregion
810
+ //#region src/providers/embedding/clip.ts
811
+ const DEFAULT_MODEL = "Xenova/clip-vit-base-patch32";
812
+ const DIMENSIONS = 512;
813
+ var ClipEmbeddingProvider = class {
814
+ name = "clip";
815
+ dimensions = DIMENSIONS;
816
+ textExtractor = null;
817
+ imageExtractor = null;
818
+ transformers = null;
819
+ modelId;
820
+ constructor(modelId = DEFAULT_MODEL) {
821
+ this.modelId = modelId;
822
+ }
823
+ async embed(text) {
824
+ const [vec] = await this.embedBatch([text]);
825
+ return vec;
826
+ }
827
+ async embedBatch(texts) {
828
+ return (await (await this.getTextExtractor())(texts, {
829
+ pooling: "mean",
830
+ normalize: true
831
+ })).tolist().map((v) => new Float32Array(v));
832
+ }
833
+ async embedImage(src) {
834
+ const image = await loadImage(await this.getTransformers(), src);
835
+ const output = await (await this.getImageExtractor())(image);
836
+ return normalize(output.data ?? new Float32Array(output.tolist()[0] || []));
837
+ }
838
+ async getTransformers() {
839
+ if (this.transformers) return this.transformers;
840
+ try {
841
+ this.transformers = await import("@xenova/transformers");
842
+ } catch {
843
+ throw new Error("Install @xenova/transformers for CLIP image embeddings: npm install @xenova/transformers");
844
+ }
845
+ return this.transformers;
846
+ }
847
+ async getTextExtractor() {
848
+ if (this.textExtractor) return this.textExtractor;
849
+ this.textExtractor = await (await this.getTransformers()).pipeline("feature-extraction", this.modelId);
850
+ return this.textExtractor;
851
+ }
852
+ async getImageExtractor() {
853
+ if (this.imageExtractor) return this.imageExtractor;
854
+ this.imageExtractor = await (await this.getTransformers()).pipeline("image-feature-extraction", this.modelId);
855
+ return this.imageExtractor;
856
+ }
857
+ };
858
+ async function loadImage(t, src) {
859
+ if (src.startsWith("data:")) {
860
+ const comma = src.indexOf(",");
861
+ const b64 = comma >= 0 ? src.slice(comma + 1) : src;
862
+ const buf = Buffer.from(b64, "base64");
863
+ const blob = new Blob([buf]);
864
+ return t.RawImage.fromBlob(blob);
865
+ }
866
+ const data = await readFile(src);
867
+ const blob = new Blob([data]);
868
+ return t.RawImage.fromBlob(blob);
869
+ }
870
+ function normalize(vec) {
871
+ let sum = 0;
872
+ for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i];
873
+ const norm = Math.sqrt(sum);
874
+ if (norm === 0) return vec;
875
+ const out = new Float32Array(vec.length);
876
+ for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;
877
+ return out;
878
+ }
879
+
686
880
  //#endregion
687
881
  //#region src/providers/embedding/index.ts
882
+ let imageEmbeddingProvider = null;
883
+ function createImageEmbeddingProvider() {
884
+ if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] !== "true") return null;
885
+ if (imageEmbeddingProvider) return imageEmbeddingProvider;
886
+ imageEmbeddingProvider = new ClipEmbeddingProvider();
887
+ return imageEmbeddingProvider;
888
+ }
688
889
  function createEmbeddingProvider() {
689
890
  const detected = detectEmbeddingProvider();
690
891
  if (!detected) return null;
@@ -736,6 +937,7 @@ function createBaseProvider(config) {
736
937
  return new OpenRouterProvider(geminiKey, config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
737
938
  }
738
939
  case "openrouter": return new OpenRouterProvider(requireEnvVar("OPENROUTER_API_KEY"), config.model, config.maxTokens, "https://openrouter.ai/api/v1/chat/completions");
940
+ case "noop": return new NoopProvider();
739
941
  default: return new AgentSDKProvider();
740
942
  }
741
943
  }
@@ -939,7 +1141,12 @@ const KV = {
939
1141
  enrichedChunks: (sessionId) => `mem:enriched:${sessionId}`,
940
1142
  latentEmbeddings: (obsId) => `mem:latent:${obsId}`,
941
1143
  retentionScores: "mem:retention",
942
- accessLog: "mem:access"
1144
+ accessLog: "mem:access",
1145
+ imageRefs: "mem:image-refs",
1146
+ imageEmbeddings: "mem:image-embeddings",
1147
+ slots: "mem:slots",
1148
+ globalSlots: "mem:slots:global",
1149
+ state: "mem:state"
943
1150
  };
944
1151
  const STREAM = {
945
1152
  name: "mem-live",
@@ -1335,7 +1542,7 @@ async function loadPipeline() {
1335
1542
  if (pipelineLoading) return pipelineLoading;
1336
1543
  pipelineLoading = (async () => {
1337
1544
  try {
1338
- const { pipeline: createPipeline } = await import("./transformers-BX_tgxdO.mjs");
1545
+ const { pipeline: createPipeline } = await import("@xenova/transformers");
1339
1546
  pipeline = await createPipeline("text-classification", "Xenova/ms-marco-MiniLM-L-6-v2", { quantized: true });
1340
1547
  return pipeline;
1341
1548
  } catch {
@@ -2141,7 +2348,7 @@ function buildSyntheticCompression(raw) {
2141
2348
  inputStr,
2142
2349
  outputStr
2143
2350
  ].filter((s) => s.length > 0);
2144
- return {
2351
+ const result = {
2145
2352
  id: raw.id,
2146
2353
  sessionId: raw.sessionId,
2147
2354
  timestamp: raw.timestamp,
@@ -2155,6 +2362,9 @@ function buildSyntheticCompression(raw) {
2155
2362
  importance: 5,
2156
2363
  confidence: .3
2157
2364
  };
2365
+ if (raw.modality) result.modality = raw.modality;
2366
+ if (raw.imageData) result.imageData = raw.imageData;
2367
+ return result;
2158
2368
  }
2159
2369
 
2160
2370
  //#endregion
@@ -2402,6 +2612,24 @@ function registerSearchFunction(sdk, kv) {
2402
2612
 
2403
2613
  //#endregion
2404
2614
  //#region src/functions/observe.ts
2615
+ function extractImage(d) {
2616
+ if (!d) return void 0;
2617
+ if (typeof d === "string") {
2618
+ if (d.startsWith("data:image/") || d.startsWith("iVBORw0KGgo") || d.startsWith("/9j/")) return d;
2619
+ return;
2620
+ }
2621
+ if (typeof d === "object" && d !== null) {
2622
+ const obj = d;
2623
+ if (typeof obj["image_data"] === "string") return obj["image_data"];
2624
+ if (typeof obj["image_path"] === "string") return obj["image_path"];
2625
+ if (typeof obj["imageBase64"] === "string") return obj["imageBase64"];
2626
+ if (typeof obj["imagePath"] === "string") return obj["imagePath"];
2627
+ for (const key of Object.keys(obj)) {
2628
+ const match = extractImage(obj[key]);
2629
+ if (match) return match;
2630
+ }
2631
+ }
2632
+ }
2405
2633
  function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2406
2634
  sdk.registerFunction("mem::observe", async (payload) => {
2407
2635
  if (!payload?.sessionId || typeof payload.sessionId !== "string" || !payload.hookType || typeof payload.hookType !== "string" || !payload.timestamp || typeof payload.timestamp !== "string") return {
@@ -2433,6 +2661,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2433
2661
  hookType: payload.hookType,
2434
2662
  raw: sanitizedRaw
2435
2663
  };
2664
+ let extractedImage;
2436
2665
  if (typeof sanitizedRaw === "object" && sanitizedRaw !== null) {
2437
2666
  const d = sanitizedRaw;
2438
2667
  if (payload.hookType === "post_tool_use" || payload.hookType === "post_tool_failure") {
@@ -2441,7 +2670,13 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2441
2670
  raw.toolOutput = d["tool_output"] || d["error"];
2442
2671
  }
2443
2672
  if (payload.hookType === "prompt_submit") raw.userPrompt = d["prompt"];
2673
+ extractedImage = extractImage(sanitizedRaw);
2674
+ if (extractedImage) raw.modality = raw.toolInput || raw.toolOutput || raw.userPrompt ? "mixed" : "image";
2675
+ } else if (typeof sanitizedRaw === "string") {
2676
+ extractedImage = extractImage(sanitizedRaw);
2677
+ if (extractedImage) raw.modality = "image";
2444
2678
  }
2679
+ const pendingImageData = extractedImage;
2445
2680
  return withKeyedLock(`obs:${payload.sessionId}`, async () => {
2446
2681
  if (maxObservationsPerSession && maxObservationsPerSession > 0) {
2447
2682
  if ((await kv.list(KV.observations(payload.sessionId))).length >= maxObservationsPerSession) return {
@@ -2449,7 +2684,29 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2449
2684
  error: `Session observation limit reached (${maxObservationsPerSession})`
2450
2685
  };
2451
2686
  }
2452
- await kv.set(KV.observations(payload.sessionId), obsId, raw);
2687
+ if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
2688
+ const { saveImageToDisk } = await Promise.resolve().then(() => image_store_exports);
2689
+ const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
2690
+ raw.imageData = filePath;
2691
+ const { incrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
2692
+ await incrementImageRef(kv, filePath);
2693
+ sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: bytesWritten });
2694
+ if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.triggerVoid("mem::vision-embed", {
2695
+ imageRef: filePath,
2696
+ sessionId: payload.sessionId,
2697
+ observationId: obsId
2698
+ });
2699
+ }
2700
+ try {
2701
+ await kv.set(KV.observations(payload.sessionId), obsId, raw);
2702
+ } catch (error) {
2703
+ if (raw.imageData) {
2704
+ const { deleteImage } = await Promise.resolve().then(() => image_store_exports);
2705
+ const { deletedBytes } = await deleteImage(raw.imageData);
2706
+ if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
2707
+ }
2708
+ throw error;
2709
+ }
2453
2710
  if (dedupMap && dedupHash) dedupMap.record(dedupHash);
2454
2711
  await sdk.trigger({
2455
2712
  function_id: "stream::set",
@@ -2479,15 +2736,26 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2479
2736
  action: TriggerAction.Void()
2480
2737
  });
2481
2738
  const session = await kv.get(KV.sessions, payload.sessionId);
2482
- if (session) await kv.update(KV.sessions, payload.sessionId, [{
2483
- type: "set",
2484
- path: "updatedAt",
2485
- value: (/* @__PURE__ */ new Date()).toISOString()
2486
- }, {
2487
- type: "set",
2488
- path: "observationCount",
2489
- value: (session.observationCount || 0) + 1
2490
- }]);
2739
+ if (session) {
2740
+ const updates = [{
2741
+ type: "set",
2742
+ path: "updatedAt",
2743
+ value: (/* @__PURE__ */ new Date()).toISOString()
2744
+ }, {
2745
+ type: "set",
2746
+ path: "observationCount",
2747
+ value: (session.observationCount || 0) + 1
2748
+ }];
2749
+ if (!session.firstPrompt && typeof raw.userPrompt === "string") {
2750
+ const trimmed = raw.userPrompt.replace(/\s+/g, " ").trim();
2751
+ if (trimmed.length > 0) updates.push({
2752
+ type: "set",
2753
+ path: "firstPrompt",
2754
+ value: trimmed.slice(0, 200)
2755
+ });
2756
+ }
2757
+ await kv.update(KV.sessions, payload.sessionId, updates);
2758
+ }
2491
2759
  if (isAutoCompressEnabled()) await sdk.trigger({
2492
2760
  function_id: "mem::compress",
2493
2761
  payload: {
@@ -2538,6 +2806,815 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2538
2806
  });
2539
2807
  }
2540
2808
 
2809
+ //#endregion
2810
+ //#region src/utils/image-store.ts
2811
+ var image_store_exports = /* @__PURE__ */ __exportAll({
2812
+ IMAGES_DIR: () => IMAGES_DIR,
2813
+ deleteImage: () => deleteImage,
2814
+ getMaxBytes: () => getMaxBytes,
2815
+ isManagedImagePath: () => isManagedImagePath,
2816
+ saveImageToDisk: () => saveImageToDisk,
2817
+ touchImage: () => touchImage
2818
+ });
2819
+ const IMAGES_DIR = join(homedir(), ".agentmemory", "images");
2820
+ const DEFAULT_MAX_BYTES = 500 * 1024 * 1024;
2821
+ function getMaxBytes() {
2822
+ return Number(process.env.AGENTMEMORY_IMAGE_STORE_MAX_BYTES) || DEFAULT_MAX_BYTES;
2823
+ }
2824
+ function isManagedImagePath(filePath) {
2825
+ const resolved = resolve(filePath);
2826
+ const normalizedImagesDir = resolve(IMAGES_DIR);
2827
+ return resolved.startsWith(normalizedImagesDir + sep) || resolved === normalizedImagesDir;
2828
+ }
2829
+ function contentHash(data) {
2830
+ return createHash("sha256").update(data).digest("hex");
2831
+ }
2832
+ async function saveImageToDisk(base64Data) {
2833
+ if (!base64Data) return {
2834
+ filePath: "",
2835
+ bytesWritten: 0
2836
+ };
2837
+ if (!existsSync(IMAGES_DIR)) await mkdir(IMAGES_DIR, { recursive: true });
2838
+ let cleanBase64 = base64Data;
2839
+ let ext = "png";
2840
+ if (base64Data.startsWith("data:image/")) {
2841
+ const commaIdx = base64Data.indexOf(",");
2842
+ if (commaIdx !== -1) {
2843
+ const meta = base64Data.substring(0, commaIdx);
2844
+ if (meta.includes("jpeg") || meta.includes("jpg")) ext = "jpg";
2845
+ else if (meta.includes("webp")) ext = "webp";
2846
+ else if (meta.includes("gif")) ext = "gif";
2847
+ cleanBase64 = base64Data.substring(commaIdx + 1);
2848
+ }
2849
+ } else if (base64Data.startsWith("/9j/")) ext = "jpg";
2850
+ const filePath = join(IMAGES_DIR, `${contentHash(cleanBase64)}.${ext}`);
2851
+ if (existsSync(filePath)) return {
2852
+ filePath,
2853
+ bytesWritten: 0
2854
+ };
2855
+ await writeFile(filePath, Buffer.from(cleanBase64, "base64"));
2856
+ return {
2857
+ filePath,
2858
+ bytesWritten: (await stat(filePath)).size
2859
+ };
2860
+ }
2861
+ async function deleteImage(filePath) {
2862
+ if (!filePath) return { deletedBytes: 0 };
2863
+ if (!isManagedImagePath(filePath)) return { deletedBytes: 0 };
2864
+ try {
2865
+ if (existsSync(filePath)) {
2866
+ const size = (await stat(filePath)).size;
2867
+ await unlink(filePath);
2868
+ return { deletedBytes: size };
2869
+ }
2870
+ } catch (err) {
2871
+ console.error("[agentmemory] Failed to delete image context:", err);
2872
+ }
2873
+ return { deletedBytes: 0 };
2874
+ }
2875
+ /** Touch an image file to update its mtime (marking it as recently used for LRU eviction) */
2876
+ async function touchImage(filePath) {
2877
+ if (!filePath || !isManagedImagePath(filePath)) return;
2878
+ try {
2879
+ if (existsSync(filePath)) {
2880
+ const now = /* @__PURE__ */ new Date();
2881
+ await utimes(filePath, now, now);
2882
+ }
2883
+ } catch (err) {}
2884
+ }
2885
+
2886
+ //#endregion
2887
+ //#region src/functions/image-refs.ts
2888
+ var image_refs_exports = /* @__PURE__ */ __exportAll({
2889
+ decrementImageRef: () => decrementImageRef,
2890
+ getImageRefCount: () => getImageRefCount,
2891
+ incrementImageRef: () => incrementImageRef
2892
+ });
2893
+ async function getImageRefCount(kv, filePath) {
2894
+ const count = await kv.get(KV.imageRefs, filePath);
2895
+ return count ? Number(count) : 0;
2896
+ }
2897
+ async function incrementImageRef(kv, filePath) {
2898
+ return withKeyedLock(`imgRef:${filePath}`, async () => {
2899
+ const current = await getImageRefCount(kv, filePath);
2900
+ await kv.set(KV.imageRefs, filePath, current + 1);
2901
+ await touchImage(filePath);
2902
+ });
2903
+ }
2904
+ async function decrementImageRef(kv, sdk, filePath) {
2905
+ return withKeyedLock(`imgRef:${filePath}`, async () => {
2906
+ const current = await getImageRefCount(kv, filePath);
2907
+ if (current <= 1) {
2908
+ await kv.delete(KV.imageEmbeddings, filePath);
2909
+ await kv.delete(KV.imageRefs, filePath);
2910
+ const { deletedBytes } = await deleteImage(filePath);
2911
+ if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
2912
+ } else await kv.set(KV.imageRefs, filePath, current - 1);
2913
+ });
2914
+ }
2915
+
2916
+ //#endregion
2917
+ //#region src/functions/image-quota-cleanup.ts
2918
+ const GRACE_PERIOD_MS = 3e4;
2919
+ function registerImageQuotaCleanup(sdk, kv) {
2920
+ sdk.registerFunction("mem::image-quota-cleanup", async () => {
2921
+ const now = Date.now();
2922
+ return withKeyedLock("system:cleanupLock", async () => {
2923
+ let totalSize = 0;
2924
+ const fileStats = [];
2925
+ try {
2926
+ const files = await readdir(IMAGES_DIR);
2927
+ for (const file of files) {
2928
+ if (file.startsWith(".")) continue;
2929
+ const filePath = join(IMAGES_DIR, file);
2930
+ const s = await stat(filePath);
2931
+ if (s.isFile()) {
2932
+ fileStats.push({
2933
+ filePath,
2934
+ size: s.size,
2935
+ mtimeMs: s.mtimeMs
2936
+ });
2937
+ totalSize += s.size;
2938
+ }
2939
+ }
2940
+ } catch {
2941
+ return {
2942
+ success: true,
2943
+ evicted: 0,
2944
+ freedBytes: 0
2945
+ };
2946
+ }
2947
+ const limit = getMaxBytes();
2948
+ if (totalSize <= limit) return {
2949
+ success: true,
2950
+ evicted: 0,
2951
+ freedBytes: 0,
2952
+ underQuota: true
2953
+ };
2954
+ fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
2955
+ let totalToFree = totalSize - limit;
2956
+ let evicted = 0;
2957
+ let freedBytes = 0;
2958
+ for (const f of fileStats) {
2959
+ if (totalToFree <= 0) break;
2960
+ if (now - f.mtimeMs < GRACE_PERIOD_MS) continue;
2961
+ await withKeyedLock(`imgRef:${f.filePath}`, async () => {
2962
+ let refCount;
2963
+ try {
2964
+ refCount = await getImageRefCount(kv, f.filePath);
2965
+ } catch (err) {
2966
+ logger.error("Failed to read refCount; skipping eviction", {
2967
+ filePath: f.filePath,
2968
+ error: err instanceof Error ? err.message : String(err)
2969
+ });
2970
+ return;
2971
+ }
2972
+ if (refCount > 0) return;
2973
+ const { deletedBytes } = await deleteImage(f.filePath);
2974
+ if (deletedBytes > 0) {
2975
+ sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
2976
+ totalToFree -= deletedBytes;
2977
+ freedBytes += deletedBytes;
2978
+ evicted++;
2979
+ }
2980
+ });
2981
+ }
2982
+ if (evicted > 0) {
2983
+ const freedMb = (freedBytes / (1024 * 1024)).toFixed(1);
2984
+ logger.info("Image quota cleanup complete", {
2985
+ evicted,
2986
+ freedMb
2987
+ });
2988
+ }
2989
+ return {
2990
+ success: true,
2991
+ evicted,
2992
+ freedBytes
2993
+ };
2994
+ });
2995
+ });
2996
+ }
2997
+
2998
+ //#endregion
2999
+ //#region src/functions/audit.ts
3000
+ async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
3001
+ const entry = {
3002
+ id: generateId("aud"),
3003
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3004
+ operation,
3005
+ userId,
3006
+ functionId,
3007
+ targetIds,
3008
+ details,
3009
+ qualityScore
3010
+ };
3011
+ await kv.set(KV.audit, entry.id, entry);
3012
+ return entry;
3013
+ }
3014
+ async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
3015
+ try {
3016
+ await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
3017
+ } catch (err) {
3018
+ try {
3019
+ logger.warn("audit write failed", {
3020
+ functionId,
3021
+ operation,
3022
+ targetIds,
3023
+ error: err instanceof Error ? err.message : String(err)
3024
+ });
3025
+ } catch {}
3026
+ }
3027
+ }
3028
+ async function queryAudit(kv, filter) {
3029
+ let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
3030
+ if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
3031
+ if (filter?.dateFrom) {
3032
+ const from = new Date(filter.dateFrom).getTime();
3033
+ if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
3034
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
3035
+ }
3036
+ if (filter?.dateTo) {
3037
+ const to = new Date(filter.dateTo).getTime();
3038
+ if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
3039
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
3040
+ }
3041
+ return entries.slice(0, filter?.limit || 100);
3042
+ }
3043
+
3044
+ //#endregion
3045
+ //#region src/functions/vision-search.ts
3046
+ function registerVisionSearchFunctions(sdk, kv, imageProvider) {
3047
+ sdk.registerFunction("mem::vision-embed", async (data) => {
3048
+ if (!imageProvider?.embedImage) return {
3049
+ success: false,
3050
+ error: "image embeddings disabled (set AGENTMEMORY_IMAGE_EMBEDDINGS=true)"
3051
+ };
3052
+ if (!data?.imageRef || typeof data.imageRef !== "string") return {
3053
+ success: false,
3054
+ error: "imageRef required"
3055
+ };
3056
+ if (!isManagedImagePath(data.imageRef)) return {
3057
+ success: false,
3058
+ error: "imageRef must point to a file under the managed image store"
3059
+ };
3060
+ const refCount = await kv.get(KV.imageRefs, data.imageRef);
3061
+ if (!refCount || Number(refCount) < 1) return {
3062
+ success: false,
3063
+ error: "imageRef not registered in mem:image-refs"
3064
+ };
3065
+ try {
3066
+ const vec = await imageProvider.embedImage(data.imageRef);
3067
+ const stored = {
3068
+ imageRef: data.imageRef,
3069
+ vector: Array.from(vec),
3070
+ modelName: imageProvider.name,
3071
+ dimensions: imageProvider.dimensions,
3072
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3073
+ sessionId: data.sessionId,
3074
+ observationId: data.observationId
3075
+ };
3076
+ await kv.set(KV.imageEmbeddings, data.imageRef, stored);
3077
+ await recordAudit(kv, "vision_embed", "mem::vision-embed", [data.imageRef], {
3078
+ modelName: imageProvider.name,
3079
+ dimensions: stored.dimensions,
3080
+ sessionId: data.sessionId,
3081
+ observationId: data.observationId
3082
+ });
3083
+ return {
3084
+ success: true,
3085
+ imageRef: data.imageRef,
3086
+ dimensions: stored.dimensions
3087
+ };
3088
+ } catch (err) {
3089
+ const msg = err instanceof Error ? err.message : String(err);
3090
+ logger.warn("vision-embed failed", {
3091
+ imageRef: data.imageRef,
3092
+ error: msg
3093
+ });
3094
+ return {
3095
+ success: false,
3096
+ error: msg
3097
+ };
3098
+ }
3099
+ });
3100
+ sdk.registerFunction("mem::vision-search", async (data) => {
3101
+ if (!imageProvider?.embedImage) return {
3102
+ success: false,
3103
+ error: "image embeddings disabled (set AGENTMEMORY_IMAGE_EMBEDDINGS=true)"
3104
+ };
3105
+ const requestedTopK = typeof data?.topK === "number" && Number.isFinite(data.topK) ? Math.trunc(data.topK) : 10;
3106
+ const topK = Math.min(50, Math.max(1, requestedTopK));
3107
+ let queryVec = null;
3108
+ try {
3109
+ if (data?.queryText) queryVec = await imageProvider.embed(data.queryText);
3110
+ else if (data?.queryImageBase64) {
3111
+ const b64 = data.queryImageBase64.startsWith("data:") ? data.queryImageBase64 : `data:image/png;base64,${data.queryImageBase64}`;
3112
+ queryVec = await imageProvider.embedImage(b64);
3113
+ } else if (data?.queryImageRef) {
3114
+ if (!isManagedImagePath(data.queryImageRef)) return {
3115
+ success: false,
3116
+ error: "queryImageRef must point to a file under the managed image store"
3117
+ };
3118
+ const refCount = await kv.get(KV.imageRefs, data.queryImageRef);
3119
+ if (!refCount || Number(refCount) < 1) return {
3120
+ success: false,
3121
+ error: "queryImageRef not registered in mem:image-refs"
3122
+ };
3123
+ queryVec = await imageProvider.embedImage(data.queryImageRef);
3124
+ } else return {
3125
+ success: false,
3126
+ error: "queryText, queryImageRef, or queryImageBase64 required"
3127
+ };
3128
+ } catch (err) {
3129
+ return {
3130
+ success: false,
3131
+ error: `query embed failed: ${err instanceof Error ? err.message : String(err)}`
3132
+ };
3133
+ }
3134
+ if (!queryVec) return {
3135
+ success: false,
3136
+ error: "failed to build query vector"
3137
+ };
3138
+ const stored = await kv.list(KV.imageEmbeddings);
3139
+ const scored = (data?.sessionId ? stored.filter((s) => s.sessionId === data.sessionId) : stored).map((s) => ({
3140
+ imageRef: s.imageRef,
3141
+ score: cosine(queryVec, s.vector),
3142
+ sessionId: s.sessionId,
3143
+ observationId: s.observationId,
3144
+ updatedAt: s.updatedAt
3145
+ }));
3146
+ scored.sort((a, b) => b.score - a.score);
3147
+ return {
3148
+ success: true,
3149
+ results: scored.slice(0, topK),
3150
+ total: scored.length
3151
+ };
3152
+ });
3153
+ }
3154
+ function cosine(a, b) {
3155
+ if (a.length !== b.length) return 0;
3156
+ let dot = 0;
3157
+ let normA = 0;
3158
+ let normB = 0;
3159
+ for (let i = 0; i < a.length; i++) {
3160
+ dot += a[i] * b[i];
3161
+ normA += a[i] * a[i];
3162
+ normB += b[i] * b[i];
3163
+ }
3164
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
3165
+ return denom === 0 ? 0 : dot / denom;
3166
+ }
3167
+
3168
+ //#endregion
3169
+ //#region src/functions/slots.ts
3170
+ const DEFAULT_SIZE_LIMIT = 2e3;
3171
+ const DEFAULT_SLOTS = [
3172
+ {
3173
+ label: "persona",
3174
+ content: "",
3175
+ sizeLimit: 1e3,
3176
+ description: "How the agent should see itself: role, tone, behavioural guidelines.",
3177
+ pinned: true,
3178
+ readOnly: false,
3179
+ scope: "global"
3180
+ },
3181
+ {
3182
+ label: "user_preferences",
3183
+ content: "",
3184
+ sizeLimit: 2e3,
3185
+ description: "Coding style, tool preferences, naming conventions, and other habits the user wants preserved across sessions.",
3186
+ pinned: true,
3187
+ readOnly: false,
3188
+ scope: "global"
3189
+ },
3190
+ {
3191
+ label: "tool_guidelines",
3192
+ content: "",
3193
+ sizeLimit: 1500,
3194
+ description: "Rules the agent should follow when picking or sequencing tools (e.g. prefer X over Y, never run Z without confirmation).",
3195
+ pinned: true,
3196
+ readOnly: false,
3197
+ scope: "global"
3198
+ },
3199
+ {
3200
+ label: "project_context",
3201
+ content: "",
3202
+ sizeLimit: 3e3,
3203
+ description: "Architecture decisions, codebase conventions, build/test commands, and cross-cutting constraints for the current project.",
3204
+ pinned: true,
3205
+ readOnly: false,
3206
+ scope: "project"
3207
+ },
3208
+ {
3209
+ label: "guidance",
3210
+ content: "",
3211
+ sizeLimit: 1500,
3212
+ description: "Active advice for the next session: what to focus on, what to avoid, open risks.",
3213
+ pinned: true,
3214
+ readOnly: false,
3215
+ scope: "project"
3216
+ },
3217
+ {
3218
+ label: "pending_items",
3219
+ content: "",
3220
+ sizeLimit: 2e3,
3221
+ description: "Unfinished work, explicit TODOs, and promises made but not yet delivered.",
3222
+ pinned: true,
3223
+ readOnly: false,
3224
+ scope: "project"
3225
+ },
3226
+ {
3227
+ label: "session_patterns",
3228
+ content: "",
3229
+ sizeLimit: 1500,
3230
+ description: "Recurring behaviours and common struggles observed across recent sessions.",
3231
+ pinned: false,
3232
+ readOnly: false,
3233
+ scope: "project"
3234
+ },
3235
+ {
3236
+ label: "self_notes",
3237
+ content: "",
3238
+ sizeLimit: 1500,
3239
+ description: "Free-form notes the agent keeps for itself: hypotheses, dead ends, things to revisit.",
3240
+ pinned: false,
3241
+ readOnly: false,
3242
+ scope: "project"
3243
+ }
3244
+ ];
3245
+ function isSlotsEnabled() {
3246
+ return process.env["AGENTMEMORY_SLOTS"] === "true";
3247
+ }
3248
+ function isReflectEnabled() {
3249
+ return process.env["AGENTMEMORY_REFLECT"] === "true";
3250
+ }
3251
+ function scopeKv(scope) {
3252
+ return scope === "global" ? KV.globalSlots : KV.slots;
3253
+ }
3254
+ function nowIso() {
3255
+ return (/* @__PURE__ */ new Date()).toISOString();
3256
+ }
3257
+ function validateLabel(label) {
3258
+ if (typeof label !== "string") return null;
3259
+ const trimmed = label.trim();
3260
+ if (!trimmed || trimmed.length > 64) return null;
3261
+ if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) return null;
3262
+ return trimmed;
3263
+ }
3264
+ async function readSlot(kv, label) {
3265
+ const project = await kv.get(KV.slots, label);
3266
+ if (project) return {
3267
+ slot: project,
3268
+ scope: "project"
3269
+ };
3270
+ const global = await kv.get(KV.globalSlots, label);
3271
+ if (global) return {
3272
+ slot: global,
3273
+ scope: "global"
3274
+ };
3275
+ return {
3276
+ slot: null,
3277
+ scope: "project"
3278
+ };
3279
+ }
3280
+ async function readSlotInScope(kv, label, scope) {
3281
+ return kv.get(scopeKv(scope), label);
3282
+ }
3283
+ function validateScope(raw) {
3284
+ if (raw === void 0 || raw === null) return "project";
3285
+ if (raw === "project" || raw === "global") return raw;
3286
+ return null;
3287
+ }
3288
+ function validateSizeLimit(raw) {
3289
+ if (raw === void 0 || raw === null) return DEFAULT_SIZE_LIMIT;
3290
+ if (typeof raw !== "number") return null;
3291
+ if (!Number.isInteger(raw) || raw < 1 || raw > 2e4) return null;
3292
+ return raw;
3293
+ }
3294
+ async function seedDefaults(kv) {
3295
+ const ts = nowIso();
3296
+ for (const tmpl of DEFAULT_SLOTS) {
3297
+ const target = scopeKv(tmpl.scope);
3298
+ if (await kv.get(target, tmpl.label)) continue;
3299
+ const slot = {
3300
+ ...tmpl,
3301
+ createdAt: ts,
3302
+ updatedAt: ts
3303
+ };
3304
+ await kv.set(target, tmpl.label, slot);
3305
+ }
3306
+ }
3307
+ function registerSlotsFunctions(sdk, kv) {
3308
+ seedDefaults(kv).catch((err) => {
3309
+ logger.warn("slot defaults seed failed", { error: err instanceof Error ? err.message : String(err) });
3310
+ });
3311
+ sdk.registerFunction("mem::slot-list", async () => {
3312
+ const [project, global] = await Promise.all([kv.list(KV.slots), kv.list(KV.globalSlots)]);
3313
+ const merged = /* @__PURE__ */ new Map();
3314
+ for (const s of global) merged.set(s.label, s);
3315
+ for (const s of project) merged.set(s.label, s);
3316
+ return {
3317
+ success: true,
3318
+ slots: Array.from(merged.values()).sort((a, b) => a.label.localeCompare(b.label))
3319
+ };
3320
+ });
3321
+ sdk.registerFunction("mem::slot-get", async (data) => {
3322
+ const label = validateLabel(data?.label);
3323
+ if (!label) return {
3324
+ success: false,
3325
+ error: "label required (lowercase, starts with letter, [a-z0-9_])"
3326
+ };
3327
+ const { slot, scope } = await readSlot(kv, label);
3328
+ if (!slot) return {
3329
+ success: false,
3330
+ error: "slot not found"
3331
+ };
3332
+ return {
3333
+ success: true,
3334
+ slot,
3335
+ scope
3336
+ };
3337
+ });
3338
+ sdk.registerFunction("mem::slot-create", async (data) => {
3339
+ const label = validateLabel(data?.label);
3340
+ if (!label) return {
3341
+ success: false,
3342
+ error: "label required (lowercase, starts with letter, [a-z0-9_])"
3343
+ };
3344
+ const scope = validateScope(data?.scope);
3345
+ if (!scope) return {
3346
+ success: false,
3347
+ error: "scope must be 'project' or 'global'"
3348
+ };
3349
+ const sizeLimit = validateSizeLimit(data?.sizeLimit);
3350
+ if (sizeLimit === null) return {
3351
+ success: false,
3352
+ error: "sizeLimit must be an integer between 1 and 20000"
3353
+ };
3354
+ const content = typeof data?.content === "string" ? data.content : "";
3355
+ if (content.length > sizeLimit) return {
3356
+ success: false,
3357
+ error: `content exceeds sizeLimit (${content.length} > ${sizeLimit})`
3358
+ };
3359
+ const description = typeof data?.description === "string" ? data.description : "";
3360
+ const pinned = typeof data?.pinned === "boolean" ? data.pinned : true;
3361
+ return withKeyedLock(`slot:${label}`, async () => {
3362
+ if (await readSlotInScope(kv, label, scope)) return {
3363
+ success: false,
3364
+ error: `slot already exists in ${scope} scope`
3365
+ };
3366
+ const ts = nowIso();
3367
+ const slot = {
3368
+ label,
3369
+ content,
3370
+ sizeLimit,
3371
+ description,
3372
+ pinned,
3373
+ readOnly: false,
3374
+ scope,
3375
+ createdAt: ts,
3376
+ updatedAt: ts
3377
+ };
3378
+ await kv.set(scopeKv(scope), label, slot);
3379
+ await recordAudit(kv, "slot_create", "mem::slot-create", [label], {
3380
+ scope,
3381
+ sizeLimit: slot.sizeLimit,
3382
+ pinned: slot.pinned
3383
+ });
3384
+ return {
3385
+ success: true,
3386
+ slot
3387
+ };
3388
+ });
3389
+ });
3390
+ sdk.registerFunction("mem::slot-append", async (data) => {
3391
+ const label = validateLabel(data?.label);
3392
+ if (!label) return {
3393
+ success: false,
3394
+ error: "label required"
3395
+ };
3396
+ const text = typeof data?.text === "string" ? data.text : "";
3397
+ if (!text) return {
3398
+ success: false,
3399
+ error: "text required"
3400
+ };
3401
+ return withKeyedLock(`slot:${label}`, async () => {
3402
+ const { slot, scope } = await readSlot(kv, label);
3403
+ if (!slot) return {
3404
+ success: false,
3405
+ error: "slot not found (use mem::slot-create first)"
3406
+ };
3407
+ if (slot.readOnly) return {
3408
+ success: false,
3409
+ error: "slot is read-only"
3410
+ };
3411
+ const sep = slot.content && !slot.content.endsWith("\n") ? "\n" : "";
3412
+ const next = `${slot.content}${sep}${text}`;
3413
+ if (next.length > slot.sizeLimit) return {
3414
+ success: false,
3415
+ error: `append would exceed sizeLimit (${next.length} > ${slot.sizeLimit}). Use mem::slot-replace to compact first.`,
3416
+ currentSize: slot.content.length,
3417
+ sizeLimit: slot.sizeLimit
3418
+ };
3419
+ const updated = {
3420
+ ...slot,
3421
+ content: next,
3422
+ updatedAt: nowIso()
3423
+ };
3424
+ await kv.set(scopeKv(scope), label, updated);
3425
+ await recordAudit(kv, "slot_append", "mem::slot-append", [label], {
3426
+ scope,
3427
+ added: text.length,
3428
+ total: next.length
3429
+ });
3430
+ return {
3431
+ success: true,
3432
+ slot: updated,
3433
+ size: next.length
3434
+ };
3435
+ });
3436
+ });
3437
+ sdk.registerFunction("mem::slot-replace", async (data) => {
3438
+ const label = validateLabel(data?.label);
3439
+ if (!label) return {
3440
+ success: false,
3441
+ error: "label required"
3442
+ };
3443
+ if (typeof data?.content !== "string") return {
3444
+ success: false,
3445
+ error: "content required (string)"
3446
+ };
3447
+ return withKeyedLock(`slot:${label}`, async () => {
3448
+ const { slot, scope } = await readSlot(kv, label);
3449
+ if (!slot) return {
3450
+ success: false,
3451
+ error: "slot not found (use mem::slot-create first)"
3452
+ };
3453
+ if (slot.readOnly) return {
3454
+ success: false,
3455
+ error: "slot is read-only"
3456
+ };
3457
+ if (data.content.length > slot.sizeLimit) return {
3458
+ success: false,
3459
+ error: `content exceeds sizeLimit (${data.content.length} > ${slot.sizeLimit})`,
3460
+ sizeLimit: slot.sizeLimit
3461
+ };
3462
+ const updated = {
3463
+ ...slot,
3464
+ content: data.content,
3465
+ updatedAt: nowIso()
3466
+ };
3467
+ await kv.set(scopeKv(scope), label, updated);
3468
+ await recordAudit(kv, "slot_replace", "mem::slot-replace", [label], {
3469
+ scope,
3470
+ before: slot.content.length,
3471
+ after: data.content.length
3472
+ });
3473
+ return {
3474
+ success: true,
3475
+ slot: updated,
3476
+ size: data.content.length
3477
+ };
3478
+ });
3479
+ });
3480
+ sdk.registerFunction("mem::slot-delete", async (data) => {
3481
+ const label = validateLabel(data?.label);
3482
+ if (!label) return {
3483
+ success: false,
3484
+ error: "label required"
3485
+ };
3486
+ return withKeyedLock(`slot:${label}`, async () => {
3487
+ const { slot, scope } = await readSlot(kv, label);
3488
+ if (!slot) return {
3489
+ success: false,
3490
+ error: "slot not found"
3491
+ };
3492
+ if (slot.readOnly) return {
3493
+ success: false,
3494
+ error: "slot is read-only"
3495
+ };
3496
+ await kv.delete(scopeKv(scope), label);
3497
+ await recordAudit(kv, "slot_delete", "mem::slot-delete", [label], {
3498
+ scope,
3499
+ size: slot.content.length
3500
+ });
3501
+ return { success: true };
3502
+ });
3503
+ });
3504
+ sdk.registerFunction("mem::slot-reflect", async (data) => {
3505
+ if (!data?.sessionId || typeof data.sessionId !== "string") return {
3506
+ success: false,
3507
+ error: "sessionId required"
3508
+ };
3509
+ const max = typeof data.maxObservations === "number" && Number.isInteger(data.maxObservations) && data.maxObservations > 0 ? Math.min(200, data.maxObservations) : 50;
3510
+ const observations = await kv.list(KV.observations(data.sessionId));
3511
+ if (observations.length === 0) return {
3512
+ success: true,
3513
+ applied: 0,
3514
+ reason: "no observations for session"
3515
+ };
3516
+ const recent = observations.slice().sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || "")).slice(0, max);
3517
+ const pendingLines = [];
3518
+ const patternCounts = /* @__PURE__ */ new Map();
3519
+ const files = /* @__PURE__ */ new Set();
3520
+ for (const obs of recent) {
3521
+ const title = (obs.title || "").toLowerCase();
3522
+ if ((obs.narrative || "").toLowerCase().includes("todo") || title.includes("todo")) pendingLines.push(`- ${obs.title || obs.id}`);
3523
+ if (obs.type === "error") patternCounts.set("errors", (patternCounts.get("errors") ?? 0) + 1);
3524
+ if (obs.type === "command_run") patternCounts.set("commands", (patternCounts.get("commands") ?? 0) + 1);
3525
+ if (obs.files) for (const f of obs.files) files.add(f);
3526
+ }
3527
+ let applied = 0;
3528
+ if (pendingLines.length > 0) {
3529
+ if (await withKeyedLock(`slot:pending_items`, async () => {
3530
+ const { slot, scope } = await readSlot(kv, "pending_items");
3531
+ if (!slot) return false;
3532
+ const already = new Set(slot.content.split("\n"));
3533
+ const fresh = pendingLines.filter((line) => !already.has(line));
3534
+ if (fresh.length === 0) return false;
3535
+ const sep = slot.content && !slot.content.endsWith("\n") ? "\n" : "";
3536
+ const next = `${slot.content}${sep}${fresh.join("\n")}`;
3537
+ const truncated = next.length > slot.sizeLimit ? next.slice(next.length - slot.sizeLimit) : next;
3538
+ await kv.set(scopeKv(scope), "pending_items", {
3539
+ ...slot,
3540
+ content: truncated,
3541
+ updatedAt: nowIso()
3542
+ });
3543
+ return true;
3544
+ })) applied++;
3545
+ }
3546
+ if (patternCounts.size > 0) {
3547
+ if (await withKeyedLock(`slot:session_patterns`, async () => {
3548
+ const { slot, scope } = await readSlot(kv, "session_patterns");
3549
+ if (!slot) return false;
3550
+ const summary = [`last reflection: ${nowIso()}`, ...Array.from(patternCounts.entries()).map(([kind, count]) => `- ${kind}: ${count} in last ${recent.length} observations`)].join("\n");
3551
+ const next = summary.length > slot.sizeLimit ? summary.slice(0, slot.sizeLimit) : summary;
3552
+ await kv.set(scopeKv(scope), "session_patterns", {
3553
+ ...slot,
3554
+ content: next,
3555
+ updatedAt: nowIso()
3556
+ });
3557
+ return true;
3558
+ })) applied++;
3559
+ }
3560
+ if (files.size > 0) {
3561
+ if (await withKeyedLock(`slot:project_context`, async () => {
3562
+ const { slot, scope } = await readSlot(kv, "project_context");
3563
+ if (!slot) return false;
3564
+ const already = slot.content;
3565
+ const fresh = Array.from(files).filter((f) => !already.includes(f)).slice(0, 20);
3566
+ if (fresh.length === 0) return false;
3567
+ const header = already.length === 0 ? "Files touched in recent sessions:" : "";
3568
+ const nextRaw = `${already}${already && !already.endsWith("\n") ? "\n" : ""}${header ? header + "\n" : ""}${fresh.map((f) => `- ${f}`).join("\n")}`;
3569
+ const next = nextRaw.length > slot.sizeLimit ? nextRaw.slice(nextRaw.length - slot.sizeLimit) : nextRaw;
3570
+ await kv.set(scopeKv(scope), "project_context", {
3571
+ ...slot,
3572
+ content: next,
3573
+ updatedAt: nowIso()
3574
+ });
3575
+ return true;
3576
+ })) applied++;
3577
+ }
3578
+ if (applied > 0) await recordAudit(kv, "slot_reflect", "mem::slot-reflect", [data.sessionId], {
3579
+ observationCount: recent.length,
3580
+ slotsUpdated: applied
3581
+ });
3582
+ return {
3583
+ success: true,
3584
+ applied,
3585
+ observationsReviewed: recent.length
3586
+ };
3587
+ });
3588
+ }
3589
+
3590
+ //#endregion
3591
+ //#region src/functions/disk-size-manager.ts
3592
+ const DISK_SIZE_KEY = "system:currentDiskSize";
3593
+ function registerDiskSizeManager(sdk, kv) {
3594
+ sdk.registerFunction("mem::disk-size-delta", async (data) => {
3595
+ if (typeof data?.deltaBytes !== "number" || !isFinite(data.deltaBytes)) return {
3596
+ success: false,
3597
+ error: "deltaBytes must be a finite number"
3598
+ };
3599
+ return withKeyedLock(DISK_SIZE_KEY, async () => {
3600
+ let newTotal = (await kv.get(KV.state, DISK_SIZE_KEY) || 0) + data.deltaBytes;
3601
+ if (newTotal < 0) newTotal = 0;
3602
+ await kv.set(KV.state, DISK_SIZE_KEY, newTotal);
3603
+ if (data.deltaBytes > 0 && newTotal > getMaxBytes()) {
3604
+ sdk.triggerVoid("mem::image-quota-cleanup", {});
3605
+ logger.info("Disk quota exceeded, cleanup triggered", {
3606
+ currentBytes: newTotal,
3607
+ maxBytes: getMaxBytes()
3608
+ });
3609
+ }
3610
+ return {
3611
+ success: true,
3612
+ currentTotal: newTotal
3613
+ };
3614
+ });
3615
+ });
3616
+ }
3617
+
2541
3618
  //#endregion
2542
3619
  //#region src/prompts/compression.ts
2543
3620
  const COMPRESSION_SYSTEM = `You are a memory compression engine for an AI coding agent. Your job is to extract the essential information from a tool usage observation and compress it into structured data.
@@ -2586,6 +3663,17 @@ function truncate$1(s, max) {
2586
3663
  return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
2587
3664
  }
2588
3665
 
3666
+ //#endregion
3667
+ //#region src/prompts/vision.ts
3668
+ const VISION_DESCRIPTION_PROMPT = `Describe what this image shows in the context of software development. Extract:
3669
+ - What type of image this is (screenshot, diagram, mockup, terminal output, error, etc.)
3670
+ - Key entities visible (files, components, UI elements, error messages)
3671
+ - Relationships or flow shown
3672
+ - Any decisions, errors, or state visible
3673
+ - Text content visible in the image
3674
+
3675
+ Be concise but preserve all technically relevant details. Output plain text, no XML.`;
3676
+
2589
3677
  //#endregion
2590
3678
  //#region src/prompts/xml.ts
2591
3679
  const VALID_TAG = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
@@ -2819,6 +3907,7 @@ const VALID_TYPES$1 = new Set([
2819
3907
  "subagent",
2820
3908
  "notification",
2821
3909
  "task",
3910
+ "image",
2822
3911
  "other"
2823
3912
  ]);
2824
3913
  function parseCompressionXml(xml) {
@@ -2839,11 +3928,32 @@ function parseCompressionXml(xml) {
2839
3928
  function registerCompressFunction(sdk, kv, provider, metricsStore) {
2840
3929
  sdk.registerFunction("mem::compress", async (data) => {
2841
3930
  const startMs = Date.now();
3931
+ let imageDescription;
3932
+ const hasImage = data.raw.modality === "image" || data.raw.modality === "mixed";
3933
+ if (hasImage && data.raw.imageData && provider.describeImage) try {
3934
+ let base64Data = data.raw.imageData;
3935
+ let mimeType = "image/png";
3936
+ if (!data.raw.imageData.startsWith("/9j/") && !data.raw.imageData.startsWith("iVBOR")) {
3937
+ if (!isManagedImagePath(data.raw.imageData)) throw new Error(`Refusing to read image outside managed store: ${data.raw.imageData}`);
3938
+ base64Data = readFileSync(data.raw.imageData).toString("base64");
3939
+ if (data.raw.imageData.endsWith(".jpg") || data.raw.imageData.endsWith(".jpeg")) mimeType = "image/jpeg";
3940
+ else if (data.raw.imageData.endsWith(".webp")) mimeType = "image/webp";
3941
+ else if (data.raw.imageData.endsWith(".gif")) mimeType = "image/gif";
3942
+ }
3943
+ imageDescription = await provider.describeImage(base64Data, mimeType, VISION_DESCRIPTION_PROMPT);
3944
+ logger.info("Image described by vision model", { obsId: data.observationId });
3945
+ } catch (err) {
3946
+ const msg = err instanceof Error ? err.message : String(err);
3947
+ logger.warn("Vision model call failed, falling back to text-only compression", {
3948
+ obsId: data.observationId,
3949
+ error: msg
3950
+ });
3951
+ }
2842
3952
  const prompt = buildCompressionPrompt({
2843
3953
  hookType: data.raw.hookType,
2844
3954
  toolName: data.raw.toolName,
2845
3955
  toolInput: data.raw.toolInput,
2846
- toolOutput: data.raw.toolOutput,
3956
+ toolOutput: imageDescription ? `[Image Description]: ${imageDescription}\n\n${data.raw.toolOutput ?? ""}` : data.raw.toolOutput,
2847
3957
  userPrompt: data.raw.userPrompt,
2848
3958
  timestamp: data.raw.timestamp
2849
3959
  });
@@ -2880,7 +3990,10 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
2880
3990
  sessionId: data.sessionId,
2881
3991
  timestamp: data.raw.timestamp,
2882
3992
  ...parsed,
2883
- confidence: qualityScore / 100
3993
+ confidence: qualityScore / 100,
3994
+ ...hasImage ? { modality: data.raw.modality } : {},
3995
+ ...imageDescription ? { imageDescription } : {},
3996
+ ...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
2884
3997
  };
2885
3998
  await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
2886
3999
  getSearchIndex().add(compressed);
@@ -3074,52 +4187,6 @@ function buildSummaryPrompt(observations) {
3074
4187
  return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
3075
4188
  }
3076
4189
 
3077
- //#endregion
3078
- //#region src/functions/audit.ts
3079
- async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
3080
- const entry = {
3081
- id: generateId("aud"),
3082
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3083
- operation,
3084
- userId,
3085
- functionId,
3086
- targetIds,
3087
- details,
3088
- qualityScore
3089
- };
3090
- await kv.set(KV.audit, entry.id, entry);
3091
- return entry;
3092
- }
3093
- async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
3094
- try {
3095
- await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
3096
- } catch (err) {
3097
- try {
3098
- logger.warn("audit write failed", {
3099
- functionId,
3100
- operation,
3101
- targetIds,
3102
- error: err instanceof Error ? err.message : String(err)
3103
- });
3104
- } catch {}
3105
- }
3106
- }
3107
- async function queryAudit(kv, filter) {
3108
- let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
3109
- if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
3110
- if (filter?.dateFrom) {
3111
- const from = new Date(filter.dateFrom).getTime();
3112
- if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
3113
- entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
3114
- }
3115
- if (filter?.dateTo) {
3116
- const to = new Date(filter.dateTo).getTime();
3117
- if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
3118
- entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
3119
- }
3120
- return entries.slice(0, filter?.limit || 100);
3121
- }
3122
-
3123
4190
  //#endregion
3124
4191
  //#region src/functions/summarize.ts
3125
4192
  function parseSummaryXml(xml, sessionId, project, obsCount) {
@@ -3158,12 +4225,36 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
3158
4225
  logger.info("No observations to summarize", { sessionId });
3159
4226
  return {
3160
4227
  success: false,
3161
- error: "no_observations"
4228
+ error: "no_observations"
4229
+ };
4230
+ }
4231
+ if (provider.name === "noop") {
4232
+ logger.info("Summarize skipped — no LLM provider configured", { sessionId });
4233
+ return {
4234
+ success: false,
4235
+ error: "no_provider",
4236
+ reason: "No LLM provider key set; Summarize is a no-op. Set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env to enable."
3162
4237
  };
3163
4238
  }
3164
4239
  try {
3165
4240
  const prompt = buildSummaryPrompt(compressed);
3166
- const summary = parseSummaryXml(await provider.summarize(SUMMARY_SYSTEM, prompt), sessionId, session.project, compressed.length);
4241
+ const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
4242
+ if (!response || !response.trim()) {
4243
+ const latencyMs = Date.now() - startMs;
4244
+ if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
4245
+ logger.warn("Empty provider response on summarize", {
4246
+ sessionId,
4247
+ provider: provider.name,
4248
+ promptBytes: prompt.length,
4249
+ systemBytes: SUMMARY_SYSTEM.length,
4250
+ observationCount: compressed.length
4251
+ });
4252
+ return {
4253
+ success: false,
4254
+ error: "empty_provider_response"
4255
+ };
4256
+ }
4257
+ const summary = parseSummaryXml(response, sessionId, session.project, compressed.length);
3167
4258
  if (!summary) {
3168
4259
  const latencyMs = Date.now() - startMs;
3169
4260
  if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
@@ -3750,14 +4841,20 @@ function registerRememberFunction(sdk, kv) {
3750
4841
  const deletedMemoryIds = [];
3751
4842
  const deletedObservationIds = [];
3752
4843
  let deletedSession = false;
4844
+ const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
3753
4845
  if (data.memoryId) {
4846
+ const mem = await kv.get(KV.memories, data.memoryId);
3754
4847
  await kv.delete(KV.memories, data.memoryId);
4848
+ if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
3755
4849
  await deleteAccessLog(kv, data.memoryId);
3756
4850
  deletedMemoryIds.push(data.memoryId);
3757
4851
  deleted++;
3758
4852
  }
3759
4853
  if (data.sessionId && data.observationIds && data.observationIds.length > 0) for (const obsId of data.observationIds) {
4854
+ const obs = await kv.get(KV.observations(data.sessionId), obsId);
3760
4855
  await kv.delete(KV.observations(data.sessionId), obsId);
4856
+ if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
4857
+ if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
3761
4858
  deletedObservationIds.push(obsId);
3762
4859
  deleted++;
3763
4860
  }
@@ -3765,6 +4862,8 @@ function registerRememberFunction(sdk, kv) {
3765
4862
  const observations = await kv.list(KV.observations(data.sessionId));
3766
4863
  for (const obs of observations) {
3767
4864
  await kv.delete(KV.observations(data.sessionId), obs.id);
4865
+ if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
4866
+ if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
3768
4867
  deletedObservationIds.push(obs.id);
3769
4868
  deleted++;
3770
4869
  }
@@ -3801,6 +4900,7 @@ const DEFAULTS$1 = {
3801
4900
  function registerEvictFunction(sdk, kv) {
3802
4901
  sdk.registerFunction("mem::evict", async (data) => {
3803
4902
  const dryRun = data?.dryRun ?? false;
4903
+ const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
3804
4904
  const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
3805
4905
  const cfg = {
3806
4906
  ...DEFAULTS$1,
@@ -3859,6 +4959,8 @@ function registerEvictFunction(sdk, kv) {
3859
4959
  });
3860
4960
  continue;
3861
4961
  }
4962
+ if (o.imageData) await decrementImageRef(kv, sdk, o.imageData);
4963
+ if (o.imageRef && o.imageRef !== o.imageData) await decrementImageRef(kv, sdk, o.imageRef);
3862
4964
  await recordAudit(kv, "delete", "mem::evict", [o.id], {
3863
4965
  resource: "observation",
3864
4966
  reason: "low_importance_old_observation",
@@ -3888,6 +4990,8 @@ function registerEvictFunction(sdk, kv) {
3888
4990
  });
3889
4991
  continue;
3890
4992
  }
4993
+ if (o.imageData) await decrementImageRef(kv, sdk, o.imageData);
4994
+ if (o.imageRef && o.imageRef !== o.imageData) await decrementImageRef(kv, sdk, o.imageRef);
3891
4995
  await recordAudit(kv, "delete", "mem::evict", [o.id], {
3892
4996
  resource: "observation",
3893
4997
  reason: "project_observation_cap",
@@ -3917,6 +5021,7 @@ function registerEvictFunction(sdk, kv) {
3917
5021
  });
3918
5022
  continue;
3919
5023
  }
5024
+ if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
3920
5025
  await recordAudit(kv, "delete", "mem::evict", [mem.id], {
3921
5026
  resource: "memory",
3922
5027
  reason: "expired_memory",
@@ -3940,6 +5045,7 @@ function registerEvictFunction(sdk, kv) {
3940
5045
  });
3941
5046
  continue;
3942
5047
  }
5048
+ if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
3943
5049
  await recordAudit(kv, "delete", "mem::evict", [mem.id], {
3944
5050
  resource: "memory",
3945
5051
  reason: "old_non_latest_memory",
@@ -4397,6 +5503,7 @@ function registerAutoForgetFunction(sdk, kv) {
4397
5503
  sdk.registerFunction("mem::auto-forget", async (data) => {
4398
5504
  const dryRun = data?.dryRun ?? false;
4399
5505
  const now = Date.now();
5506
+ const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
4400
5507
  const result = {
4401
5508
  ttlExpired: [],
4402
5509
  contradictions: [],
@@ -4410,6 +5517,7 @@ function registerAutoForgetFunction(sdk, kv) {
4410
5517
  result.ttlExpired.push(mem.id);
4411
5518
  deletedIds.add(mem.id);
4412
5519
  if (!dryRun) {
5520
+ if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
4413
5521
  await kv.delete(KV.memories, mem.id);
4414
5522
  await recordAudit(kv, "delete", "mem::auto-forget", [mem.id], {
4415
5523
  resource: "memory",
@@ -4478,13 +5586,23 @@ function registerAutoForgetFunction(sdk, kv) {
4478
5586
  if (now - new Date(obs.timestamp).getTime() > 180 * MS_PER_DAY && (obs.importance ?? 5) <= 2) {
4479
5587
  result.lowValueObs.push(obs.id);
4480
5588
  if (!dryRun) {
4481
- await kv.delete(KV.observations(sessions[i].id), obs.id).catch(() => {});
4482
- await recordAudit(kv, "delete", "mem::auto-forget", [obs.id], {
4483
- resource: "observation",
4484
- reason: "auto-forget low-value observation",
4485
- sessionId: sessions[i].id,
4486
- timestamp: obs.timestamp
4487
- });
5589
+ let deletedOk = false;
5590
+ try {
5591
+ await kv.delete(KV.observations(sessions[i].id), obs.id);
5592
+ deletedOk = true;
5593
+ } catch {
5594
+ deletedOk = false;
5595
+ }
5596
+ if (deletedOk) {
5597
+ if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5598
+ if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5599
+ await recordAudit(kv, "delete", "mem::auto-forget", [obs.id], {
5600
+ resource: "observation",
5601
+ reason: "auto-forget low-value observation",
5602
+ sessionId: sessions[i].id,
5603
+ timestamp: obs.timestamp
5604
+ });
5605
+ }
4488
5606
  }
4489
5607
  }
4490
5608
  }
@@ -4500,7 +5618,7 @@ function registerAutoForgetFunction(sdk, kv) {
4500
5618
 
4501
5619
  //#endregion
4502
5620
  //#region src/version.ts
4503
- const VERSION = "0.9.1";
5621
+ const VERSION = "0.9.3";
4504
5622
 
4505
5623
  //#endregion
4506
5624
  //#region src/functions/export-import.ts
@@ -4619,7 +5737,9 @@ function registerExportImportFunction(sdk, kv) {
4619
5737
  "0.8.12",
4620
5738
  "0.8.13",
4621
5739
  "0.9.0",
4622
- "0.9.1"
5740
+ "0.9.1",
5741
+ "0.9.2",
5742
+ "0.9.3"
4623
5743
  ]).has(importData.version)) return {
4624
5744
  success: false,
4625
5745
  error: `Unsupported export version: ${importData.version}`
@@ -10976,6 +12096,7 @@ function registerRetentionFunctions(sdk, kv) {
10976
12096
  const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
10977
12097
  const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
10978
12098
  const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
12099
+ const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
10979
12100
  const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
10980
12101
  if (data?.dryRun) return {
10981
12102
  success: true,
@@ -11007,6 +12128,8 @@ function registerRetentionFunctions(sdk, kv) {
11007
12128
  resolvedSource = "semantic";
11008
12129
  }
11009
12130
  if (!scope || !resolvedSource) continue;
12131
+ const mem = await kv.get(scope, candidate.memoryId);
12132
+ if (mem && mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
11010
12133
  await kv.delete(scope, candidate.memoryId);
11011
12134
  await kv.delete(KV.retentionScores, candidate.memoryId);
11012
12135
  await deleteAccessLog(kv, candidate.memoryId);
@@ -11444,6 +12567,92 @@ function rawFromCompressed(obs) {
11444
12567
  }
11445
12568
  };
11446
12569
  }
12570
+ const LESSON_PATTERNS = [/\b(always|never|don'?t|do not|make sure|remember to|note:|caveat:|warning:)\b[^.\n]{10,200}[.!\n]/gi, /\b(prefer|avoid)\s[^.\n]{10,200}[.!\n]/gi];
12571
+ async function deriveCrystalAndLessons(kv, sessionId, project, rawObs, compressed, firstPrompt) {
12572
+ if (rawObs.length === 0) return;
12573
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
12574
+ const files = /* @__PURE__ */ new Set();
12575
+ const tools = /* @__PURE__ */ new Set();
12576
+ for (const c of compressed) {
12577
+ for (const f of c.files || []) files.add(f);
12578
+ if (c.type && c.type !== "conversation" && c.title) tools.add(c.title);
12579
+ }
12580
+ const assistantTexts = [];
12581
+ const userPrompts = [];
12582
+ for (const r of rawObs) {
12583
+ if (typeof r.assistantResponse === "string" && r.assistantResponse.trim()) assistantTexts.push(r.assistantResponse);
12584
+ if (typeof r.userPrompt === "string" && r.userPrompt.trim()) userPrompts.push(r.userPrompt);
12585
+ }
12586
+ const lessonMatches = /* @__PURE__ */ new Map();
12587
+ for (const text of assistantTexts.concat(userPrompts).slice(0, 200)) for (const pat of LESSON_PATTERNS) {
12588
+ pat.lastIndex = 0;
12589
+ let m;
12590
+ while ((m = pat.exec(text)) !== null && lessonMatches.size < 40) {
12591
+ const snippet = m[0].replace(/\s+/g, " ").trim();
12592
+ if (snippet.length >= 20 && snippet.length <= 220) {
12593
+ const key = snippet.toLowerCase();
12594
+ if (!lessonMatches.has(key)) lessonMatches.set(key, snippet);
12595
+ }
12596
+ }
12597
+ }
12598
+ const lessonEntries = Array.from(lessonMatches.values()).slice(0, 20);
12599
+ const lessonIds = [];
12600
+ for (const content of lessonEntries) {
12601
+ const lessonId = fingerprintId("lesson", content.trim().toLowerCase());
12602
+ try {
12603
+ const existing = await kv.get(KV.lessons, lessonId);
12604
+ if (existing) {
12605
+ const existingSources = existing.sourceIds || [];
12606
+ const mergedSources = existingSources.includes(sessionId) ? existingSources : [...existingSources, sessionId];
12607
+ const existingTags = existing.tags || [];
12608
+ const mergedTags = existingTags.includes("auto-import") ? existingTags : [...existingTags, "auto-import"];
12609
+ const merged = {
12610
+ ...existing,
12611
+ sourceIds: mergedSources,
12612
+ tags: mergedTags,
12613
+ reinforcements: (existing.reinforcements || 0) + 1,
12614
+ updatedAt: createdAt,
12615
+ lastReinforcedAt: createdAt
12616
+ };
12617
+ await kv.set(KV.lessons, lessonId, merged);
12618
+ } else {
12619
+ const lesson = {
12620
+ id: lessonId,
12621
+ content,
12622
+ context: firstPrompt || project,
12623
+ confidence: .4,
12624
+ reinforcements: 0,
12625
+ source: "consolidation",
12626
+ sourceIds: [sessionId],
12627
+ project,
12628
+ tags: ["auto-import"],
12629
+ createdAt,
12630
+ updatedAt: createdAt,
12631
+ decayRate: .05
12632
+ };
12633
+ await kv.set(KV.lessons, lessonId, lesson);
12634
+ }
12635
+ lessonIds.push(lessonId);
12636
+ } catch {}
12637
+ }
12638
+ const crystalId = fingerprintId("crystal", sessionId);
12639
+ const narrativePreview = firstPrompt ? firstPrompt.slice(0, 300) : compressed.slice(0, 5).map((c) => c.narrative || c.title).filter(Boolean).join(" · ").slice(0, 300);
12640
+ try {
12641
+ const existingCrystal = await kv.get(KV.crystals, crystalId);
12642
+ const crystal = {
12643
+ id: crystalId,
12644
+ narrative: narrativePreview || `Session ${sessionId.slice(0, 12)} (${rawObs.length} observations)`,
12645
+ keyOutcomes: Array.from(tools).slice(0, 8),
12646
+ filesAffected: Array.from(files).slice(0, 20),
12647
+ lessons: lessonIds,
12648
+ sourceActionIds: existingCrystal?.sourceActionIds ?? [],
12649
+ sessionId,
12650
+ project,
12651
+ createdAt: existingCrystal?.createdAt ?? createdAt
12652
+ };
12653
+ await kv.set(KV.crystals, crystalId, crystal);
12654
+ } catch {}
12655
+ }
11447
12656
  function isRawShape(o) {
11448
12657
  if (!o || typeof o !== "object") return false;
11449
12658
  return typeof o.hookType === "string";
@@ -11554,6 +12763,8 @@ function registerReplayFunctions(sdk, kv) {
11554
12763
  }
11555
12764
  const parsed = parseJsonlText(text, generateId("sess"));
11556
12765
  if (parsed.observations.length === 0) continue;
12766
+ const firstPromptObs = parsed.observations.find((o) => typeof o.userPrompt === "string" && o.userPrompt.trim().length > 0);
12767
+ const firstPrompt = firstPromptObs?.userPrompt ? firstPromptObs.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
11557
12768
  const existing = await kv.get(KV.sessions, parsed.sessionId);
11558
12769
  if (existing) {
11559
12770
  existing.observationCount = (existing.observationCount || 0) + parsed.observations.length;
@@ -11561,6 +12772,7 @@ function registerReplayFunctions(sdk, kv) {
11561
12772
  if (existing.status === "active") existing.status = "completed";
11562
12773
  const existingTags = existing.tags || [];
11563
12774
  if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
12775
+ if (!existing.firstPrompt && firstPrompt) existing.firstPrompt = firstPrompt;
11564
12776
  await kv.set(KV.sessions, existing.id, existing);
11565
12777
  } else {
11566
12778
  const session = {
@@ -11571,13 +12783,22 @@ function registerReplayFunctions(sdk, kv) {
11571
12783
  endedAt: parsed.endedAt,
11572
12784
  status: "completed",
11573
12785
  observationCount: parsed.observations.length,
11574
- tags: ["jsonl-import"]
12786
+ tags: ["jsonl-import"],
12787
+ firstPrompt
11575
12788
  };
11576
12789
  await kv.set(KV.sessions, session.id, session);
11577
12790
  }
11578
- await Promise.all(parsed.observations.map((obs) => kv.set(KV.observations(parsed.sessionId), obs.id, obs)));
12791
+ const searchIndex = getSearchIndex();
12792
+ const compressed = [];
12793
+ await Promise.all(parsed.observations.map(async (obs) => {
12794
+ const synthetic = buildSyntheticCompression(obs);
12795
+ compressed.push(synthetic);
12796
+ await kv.set(KV.observations(parsed.sessionId), obs.id, synthetic);
12797
+ searchIndex.add(synthetic);
12798
+ }));
11579
12799
  observationCount += parsed.observations.length;
11580
12800
  sessionIds.push(parsed.sessionId);
12801
+ await deriveCrystalAndLessons(kv, parsed.sessionId, parsed.project, parsed.observations, compressed, firstPrompt);
11581
12802
  }
11582
12803
  await safeAudit(kv, "import", "mem::replay::import-jsonl", sessionIds, {
11583
12804
  source: "jsonl",
@@ -11611,6 +12832,7 @@ function evaluateHealth(snapshot, config = {}) {
11611
12832
  ...config
11612
12833
  };
11613
12834
  const alerts = [];
12835
+ const notes = [];
11614
12836
  let critical = false;
11615
12837
  let degraded = false;
11616
12838
  if (snapshot.connectionState === "disconnected" || snapshot.connectionState === "failed") {
@@ -11644,10 +12866,11 @@ function evaluateHealth(snapshot, config = {}) {
11644
12866
  } else if (memPercent > cfg.memoryWarnPercent && rssAboveFloor) {
11645
12867
  alerts.push(`memory_warn_${Math.round(memPercent)}%_rss${memMb}mb`);
11646
12868
  degraded = true;
11647
- } else if (memPercent > cfg.memoryWarnPercent) alerts.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
12869
+ } else if (memPercent > cfg.memoryWarnPercent) notes.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
11648
12870
  return {
11649
12871
  status: critical ? "critical" : degraded ? "degraded" : "healthy",
11650
- alerts
12872
+ alerts,
12873
+ notes
11651
12874
  };
11652
12875
  }
11653
12876
 
@@ -11724,6 +12947,7 @@ function registerHealthMonitor(sdk, kv) {
11724
12947
  const evaluated = evaluateHealth(snapshot);
11725
12948
  snapshot.status = evaluated.status;
11726
12949
  snapshot.alerts = evaluated.alerts;
12950
+ snapshot.notes = evaluated.notes;
11727
12951
  await kv.set(KV.health, "latest", snapshot).catch(() => {});
11728
12952
  return snapshot;
11729
12953
  }
@@ -11813,6 +13037,28 @@ function requireConfiguredSecret(secret, feature) {
11813
13037
  body: { error: `${feature} requires AGENTMEMORY_SECRET` }
11814
13038
  };
11815
13039
  }
13040
+ function flagDisabledResponse(opts) {
13041
+ return {
13042
+ status_code: 503,
13043
+ body: opts
13044
+ };
13045
+ }
13046
+ function graphDisabledResponse() {
13047
+ return flagDisabledResponse({
13048
+ error: "Knowledge graph not enabled",
13049
+ flag: "GRAPH_EXTRACTION_ENABLED",
13050
+ enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and restart. Requires an LLM provider key.",
13051
+ docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
13052
+ });
13053
+ }
13054
+ function consolidationDisabledResponse() {
13055
+ return flagDisabledResponse({
13056
+ error: "Consolidation pipeline not enabled",
13057
+ flag: "CONSOLIDATION_ENABLED",
13058
+ enableHow: "Set CONSOLIDATION_ENABLED=true and restart. Requires an LLM provider key.",
13059
+ docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
13060
+ });
13061
+ }
11816
13062
  function asNonEmptyString$1(value) {
11817
13063
  if (typeof value !== "string") return null;
11818
13064
  const trimmed = value.trim();
@@ -11864,6 +13110,78 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
11864
13110
  http_method: "GET"
11865
13111
  }
11866
13112
  });
13113
+ sdk.registerFunction("api::config-flags", async (req) => {
13114
+ const authErr = checkAuth(req, secret);
13115
+ if (authErr) return authErr;
13116
+ const env = process.env;
13117
+ return {
13118
+ status_code: 200,
13119
+ body: {
13120
+ version: VERSION,
13121
+ provider: env["ANTHROPIC_API_KEY"] || env["GEMINI_API_KEY"] || env["OPENROUTER_API_KEY"] || env["MINIMAX_API_KEY"] ? "llm" : "noop",
13122
+ embeddingProvider: detectEmbeddingProvider() ? "embeddings" : "none",
13123
+ flags: [
13124
+ {
13125
+ key: "GRAPH_EXTRACTION_ENABLED",
13126
+ label: "Knowledge graph extraction",
13127
+ enabled: isGraphExtractionEnabled(),
13128
+ default: false,
13129
+ affects: ["Graph", "Dashboard"],
13130
+ needsLlm: true,
13131
+ description: "Extracts entities and relations from observations into a knowledge graph.",
13132
+ enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and provide an LLM key, then restart.",
13133
+ docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
13134
+ },
13135
+ {
13136
+ key: "CONSOLIDATION_ENABLED",
13137
+ label: "Memory consolidation",
13138
+ enabled: isConsolidationEnabled(),
13139
+ default: false,
13140
+ affects: [
13141
+ "Dashboard",
13142
+ "Memories",
13143
+ "Crystals"
13144
+ ],
13145
+ needsLlm: true,
13146
+ description: "Periodically summarizes sessions into semantic facts + procedures.",
13147
+ enableHow: "Set CONSOLIDATION_ENABLED=true and provide an LLM key, then restart.",
13148
+ docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
13149
+ },
13150
+ {
13151
+ key: "AGENTMEMORY_AUTO_COMPRESS",
13152
+ label: "LLM-powered observation compression",
13153
+ enabled: isAutoCompressEnabled(),
13154
+ default: false,
13155
+ affects: ["Memories", "Timeline"],
13156
+ needsLlm: true,
13157
+ description: "Every observation is compressed by the LLM for richer summaries (costs tokens). OFF uses zero-LLM synthetic compression.",
13158
+ enableHow: "Set AGENTMEMORY_AUTO_COMPRESS=true and provide an LLM key.",
13159
+ docsHref: "https://github.com/rohitg00/agentmemory/issues/138"
13160
+ },
13161
+ {
13162
+ key: "AGENTMEMORY_INJECT_CONTEXT",
13163
+ label: "In-conversation context injection",
13164
+ enabled: isContextInjectionEnabled(),
13165
+ default: false,
13166
+ affects: ["Hooks"],
13167
+ needsLlm: false,
13168
+ description: "Hooks write recalled context into Claude Code's conversation. OFF captures in the background without injecting.",
13169
+ enableHow: "Set AGENTMEMORY_INJECT_CONTEXT=true and restart.",
13170
+ docsHref: "https://github.com/rohitg00/agentmemory/issues/143"
13171
+ }
13172
+ ]
13173
+ }
13174
+ };
13175
+ });
13176
+ sdk.registerTrigger({
13177
+ type: "http",
13178
+ function_id: "api::config-flags",
13179
+ config: {
13180
+ api_path: "/agentmemory/config/flags",
13181
+ http_method: "GET",
13182
+ middleware_function_ids: ["middleware::api-auth"]
13183
+ }
13184
+ });
11867
13185
  sdk.registerFunction("api::health", async (req) => {
11868
13186
  const health = await getLatestHealth(kv);
11869
13187
  const functionMetrics = metricsStore ? await metricsStore.getAll() : [];
@@ -12067,9 +13385,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12067
13385
  sdk.registerFunction("api::replay::sessions", async (req) => {
12068
13386
  const authErr = checkAuth(req, secret);
12069
13387
  if (authErr) return authErr;
13388
+ const sessions = await kv.list(KV.sessions);
13389
+ sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
12070
13390
  return {
12071
13391
  status_code: 200,
12072
- body: await sdk.trigger({ function_id: "mem::replay::sessions" })
13392
+ body: {
13393
+ success: true,
13394
+ sessions
13395
+ }
12073
13396
  };
12074
13397
  });
12075
13398
  sdk.registerTrigger({
@@ -12679,10 +14002,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12679
14002
  })
12680
14003
  };
12681
14004
  } catch {
12682
- return {
12683
- status_code: 404,
12684
- body: { error: "Knowledge graph not enabled" }
12685
- };
14005
+ return graphDisabledResponse();
12686
14006
  }
12687
14007
  });
12688
14008
  sdk.registerTrigger({
@@ -12705,10 +14025,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12705
14025
  })
12706
14026
  };
12707
14027
  } catch {
12708
- return {
12709
- status_code: 404,
12710
- body: { error: "Knowledge graph not enabled" }
12711
- };
14028
+ return graphDisabledResponse();
12712
14029
  }
12713
14030
  });
12714
14031
  sdk.registerTrigger({
@@ -12735,10 +14052,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12735
14052
  })
12736
14053
  };
12737
14054
  } catch {
12738
- return {
12739
- status_code: 404,
12740
- body: { error: "Knowledge graph not enabled" }
12741
- };
14055
+ return graphDisabledResponse();
12742
14056
  }
12743
14057
  });
12744
14058
  sdk.registerTrigger({
@@ -12761,10 +14075,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12761
14075
  })
12762
14076
  };
12763
14077
  } catch {
12764
- return {
12765
- status_code: 404,
12766
- body: { error: "Consolidation pipeline not enabled" }
12767
- };
14078
+ return consolidationDisabledResponse();
12768
14079
  }
12769
14080
  });
12770
14081
  sdk.registerTrigger({
@@ -12864,13 +14175,16 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12864
14175
  const parsedLimit = parseOptionalInt(req.query_params?.["limit"]);
12865
14176
  return {
12866
14177
  status_code: 200,
12867
- body: await sdk.trigger({
12868
- function_id: "mem::audit-query",
12869
- payload: {
12870
- operation: req.query_params?.["operation"],
12871
- limit: parsedLimit ?? 50
12872
- }
12873
- })
14178
+ body: {
14179
+ entries: await sdk.trigger({
14180
+ function_id: "mem::audit-query",
14181
+ payload: {
14182
+ operation: req.query_params?.["operation"],
14183
+ limit: parsedLimit ?? 50
14184
+ }
14185
+ }),
14186
+ success: true
14187
+ }
12874
14188
  };
12875
14189
  });
12876
14190
  sdk.registerTrigger({
@@ -12999,75 +14313,407 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12999
14313
  });
13000
14314
  sdk.registerTrigger({
13001
14315
  type: "http",
13002
- function_id: "api::snapshot-restore",
14316
+ function_id: "api::snapshot-restore",
14317
+ config: {
14318
+ api_path: "/agentmemory/snapshot/restore",
14319
+ http_method: "POST"
14320
+ }
14321
+ });
14322
+ sdk.registerFunction("api::memories", async (req) => {
14323
+ const authErr = checkAuth(req, secret);
14324
+ if (authErr) return authErr;
14325
+ const memories = await kv.list(KV.memories);
14326
+ return {
14327
+ status_code: 200,
14328
+ body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
14329
+ };
14330
+ });
14331
+ sdk.registerTrigger({
14332
+ type: "http",
14333
+ function_id: "api::memories",
14334
+ config: {
14335
+ api_path: "/agentmemory/memories",
14336
+ http_method: "GET"
14337
+ }
14338
+ });
14339
+ sdk.registerFunction("api::semantic-list", async (req) => {
14340
+ const authErr = checkAuth(req, secret);
14341
+ if (authErr) return authErr;
14342
+ return {
14343
+ status_code: 200,
14344
+ body: { semantic: await kv.list(KV.semantic) }
14345
+ };
14346
+ });
14347
+ sdk.registerTrigger({
14348
+ type: "http",
14349
+ function_id: "api::semantic-list",
14350
+ config: {
14351
+ api_path: "/agentmemory/semantic",
14352
+ http_method: "GET"
14353
+ }
14354
+ });
14355
+ sdk.registerFunction("api::procedural-list", async (req) => {
14356
+ const authErr = checkAuth(req, secret);
14357
+ if (authErr) return authErr;
14358
+ return {
14359
+ status_code: 200,
14360
+ body: { procedural: await kv.list(KV.procedural) }
14361
+ };
14362
+ });
14363
+ sdk.registerTrigger({
14364
+ type: "http",
14365
+ function_id: "api::procedural-list",
14366
+ config: {
14367
+ api_path: "/agentmemory/procedural",
14368
+ http_method: "GET"
14369
+ }
14370
+ });
14371
+ sdk.registerFunction("api::relations-list", async (req) => {
14372
+ const authErr = checkAuth(req, secret);
14373
+ if (authErr) return authErr;
14374
+ return {
14375
+ status_code: 200,
14376
+ body: { relations: await kv.list(KV.relations) }
14377
+ };
14378
+ });
14379
+ sdk.registerTrigger({
14380
+ type: "http",
14381
+ function_id: "api::relations-list",
14382
+ config: {
14383
+ api_path: "/agentmemory/relations",
14384
+ http_method: "GET"
14385
+ }
14386
+ });
14387
+ sdk.registerFunction("api::vision-search", async (req) => {
14388
+ const authErr = checkAuth(req, secret);
14389
+ if (authErr) return authErr;
14390
+ const body = req.body ?? {};
14391
+ const queryText = asNonEmptyString$1(body["queryText"]);
14392
+ const queryImageRef = asNonEmptyString$1(body["queryImageRef"]);
14393
+ const queryImageBase64 = asNonEmptyString$1(body["queryImageBase64"]);
14394
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14395
+ if (!queryText && !queryImageRef && !queryImageBase64) return {
14396
+ status_code: 400,
14397
+ body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
14398
+ };
14399
+ const topKParsed = parseOptionalPositiveInt(body["topK"]);
14400
+ if (topKParsed === null) return {
14401
+ status_code: 400,
14402
+ body: { error: "topK must be a positive integer" }
14403
+ };
14404
+ const payload = {};
14405
+ if (queryText) payload["queryText"] = queryText;
14406
+ if (queryImageRef) payload["queryImageRef"] = queryImageRef;
14407
+ if (queryImageBase64) payload["queryImageBase64"] = queryImageBase64;
14408
+ if (sessionId) payload["sessionId"] = sessionId;
14409
+ if (topKParsed !== void 0) payload["topK"] = Math.min(50, topKParsed);
14410
+ const result = await sdk.trigger({
14411
+ function_id: "mem::vision-search",
14412
+ payload
14413
+ });
14414
+ const resp = result;
14415
+ if (resp?.success === false) return {
14416
+ status_code: resp.error?.includes("disabled") ? 503 : 400,
14417
+ body: resp
14418
+ };
14419
+ return {
14420
+ status_code: 200,
14421
+ body: result
14422
+ };
14423
+ });
14424
+ sdk.registerTrigger({
14425
+ type: "http",
14426
+ function_id: "api::vision-search",
14427
+ config: {
14428
+ api_path: "/agentmemory/vision-search",
14429
+ http_method: "POST"
14430
+ }
14431
+ });
14432
+ sdk.registerFunction("api::vision-embed", async (req) => {
14433
+ const authErr = checkAuth(req, secret);
14434
+ if (authErr) return authErr;
14435
+ const body = req.body ?? {};
14436
+ const imageRef = asNonEmptyString$1(body["imageRef"]);
14437
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14438
+ const observationId = asNonEmptyString$1(body["observationId"]);
14439
+ if (!imageRef) return {
14440
+ status_code: 400,
14441
+ body: { error: "imageRef is required" }
14442
+ };
14443
+ const payload = { imageRef };
14444
+ if (sessionId) payload["sessionId"] = sessionId;
14445
+ if (observationId) payload["observationId"] = observationId;
14446
+ const result = await sdk.trigger({
14447
+ function_id: "mem::vision-embed",
14448
+ payload
14449
+ });
14450
+ const resp = result;
14451
+ if (resp?.success === false) return {
14452
+ status_code: resp.error?.includes("disabled") ? 503 : 400,
14453
+ body: resp
14454
+ };
14455
+ return {
14456
+ status_code: 200,
14457
+ body: result
14458
+ };
14459
+ });
14460
+ sdk.registerTrigger({
14461
+ type: "http",
14462
+ function_id: "api::vision-embed",
14463
+ config: {
14464
+ api_path: "/agentmemory/vision-embed",
14465
+ http_method: "POST"
14466
+ }
14467
+ });
14468
+ sdk.registerFunction("api::slot-list", async (req) => {
14469
+ const authErr = checkAuth(req, secret);
14470
+ if (authErr) return authErr;
14471
+ return {
14472
+ status_code: 200,
14473
+ body: await sdk.trigger({
14474
+ function_id: "mem::slot-list",
14475
+ payload: {}
14476
+ })
14477
+ };
14478
+ });
14479
+ sdk.registerTrigger({
14480
+ type: "http",
14481
+ function_id: "api::slot-list",
14482
+ config: {
14483
+ api_path: "/agentmemory/slots",
14484
+ http_method: "GET"
14485
+ }
14486
+ });
14487
+ sdk.registerFunction("api::slot-get", async (req) => {
14488
+ const authErr = checkAuth(req, secret);
14489
+ if (authErr) return authErr;
14490
+ const label = asNonEmptyString$1(req.query_params?.["label"]);
14491
+ if (!label) return {
14492
+ status_code: 400,
14493
+ body: { error: "label query param required" }
14494
+ };
14495
+ const result = await sdk.trigger({
14496
+ function_id: "mem::slot-get",
14497
+ payload: { label }
14498
+ });
14499
+ const resp = result;
14500
+ if (resp?.success === false) return {
14501
+ status_code: resp.error?.includes("not found") ? 404 : 400,
14502
+ body: resp
14503
+ };
14504
+ return {
14505
+ status_code: 200,
14506
+ body: result
14507
+ };
14508
+ });
14509
+ sdk.registerTrigger({
14510
+ type: "http",
14511
+ function_id: "api::slot-get",
14512
+ config: {
14513
+ api_path: "/agentmemory/slot",
14514
+ http_method: "GET"
14515
+ }
14516
+ });
14517
+ sdk.registerFunction("api::slot-create", async (req) => {
14518
+ const authErr = checkAuth(req, secret);
14519
+ if (authErr) return authErr;
14520
+ const body = req.body ?? {};
14521
+ const label = asNonEmptyString$1(body["label"]);
14522
+ if (!label) return {
14523
+ status_code: 400,
14524
+ body: { error: "label required" }
14525
+ };
14526
+ if (body["content"] !== void 0 && typeof body["content"] !== "string") return {
14527
+ status_code: 400,
14528
+ body: { error: "content must be a string" }
14529
+ };
14530
+ if (body["description"] !== void 0 && typeof body["description"] !== "string") return {
14531
+ status_code: 400,
14532
+ body: { error: "description must be a string" }
14533
+ };
14534
+ if (body["pinned"] !== void 0 && typeof body["pinned"] !== "boolean") return {
14535
+ status_code: 400,
14536
+ body: { error: "pinned must be a boolean" }
14537
+ };
14538
+ if (body["scope"] !== void 0 && body["scope"] !== "project" && body["scope"] !== "global") return {
14539
+ status_code: 400,
14540
+ body: { error: "scope must be 'project' or 'global'" }
14541
+ };
14542
+ const sizeLimit = parseOptionalPositiveInt(body["sizeLimit"]);
14543
+ if (sizeLimit === null) return {
14544
+ status_code: 400,
14545
+ body: { error: "sizeLimit must be a positive integer" }
14546
+ };
14547
+ if (sizeLimit !== void 0 && sizeLimit > 2e4) return {
14548
+ status_code: 400,
14549
+ body: { error: "sizeLimit must be <= 20000" }
14550
+ };
14551
+ const payload = { label };
14552
+ if (typeof body["content"] === "string") payload["content"] = body["content"];
14553
+ if (typeof body["description"] === "string") payload["description"] = body["description"];
14554
+ if (sizeLimit !== void 0) payload["sizeLimit"] = sizeLimit;
14555
+ if (typeof body["pinned"] === "boolean") payload["pinned"] = body["pinned"];
14556
+ if (body["scope"] === "project" || body["scope"] === "global") payload["scope"] = body["scope"];
14557
+ const result = await sdk.trigger({
14558
+ function_id: "mem::slot-create",
14559
+ payload
14560
+ });
14561
+ const resp = result;
14562
+ if (resp?.success === false) return {
14563
+ status_code: resp.error?.includes("exists") ? 409 : 400,
14564
+ body: resp
14565
+ };
14566
+ return {
14567
+ status_code: 201,
14568
+ body: result
14569
+ };
14570
+ });
14571
+ sdk.registerTrigger({
14572
+ type: "http",
14573
+ function_id: "api::slot-create",
13003
14574
  config: {
13004
- api_path: "/agentmemory/snapshot/restore",
14575
+ api_path: "/agentmemory/slot",
13005
14576
  http_method: "POST"
13006
14577
  }
13007
14578
  });
13008
- sdk.registerFunction("api::memories", async (req) => {
14579
+ sdk.registerFunction("api::slot-append", async (req) => {
13009
14580
  const authErr = checkAuth(req, secret);
13010
14581
  if (authErr) return authErr;
13011
- const memories = await kv.list(KV.memories);
14582
+ const body = req.body ?? {};
14583
+ const label = asNonEmptyString$1(body["label"]);
14584
+ const text = typeof body["text"] === "string" ? body["text"] : null;
14585
+ if (!label || !text) return {
14586
+ status_code: 400,
14587
+ body: { error: "label and text required" }
14588
+ };
14589
+ const result = await sdk.trigger({
14590
+ function_id: "mem::slot-append",
14591
+ payload: {
14592
+ label,
14593
+ text
14594
+ }
14595
+ });
14596
+ const resp = result;
14597
+ if (resp?.success === false) {
14598
+ const notFound = resp.error?.includes("not found");
14599
+ const overLimit = resp.error?.includes("exceed");
14600
+ return {
14601
+ status_code: notFound ? 404 : overLimit ? 413 : 400,
14602
+ body: resp
14603
+ };
14604
+ }
13012
14605
  return {
13013
14606
  status_code: 200,
13014
- body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
14607
+ body: result
13015
14608
  };
13016
14609
  });
13017
14610
  sdk.registerTrigger({
13018
14611
  type: "http",
13019
- function_id: "api::memories",
14612
+ function_id: "api::slot-append",
13020
14613
  config: {
13021
- api_path: "/agentmemory/memories",
13022
- http_method: "GET"
14614
+ api_path: "/agentmemory/slot/append",
14615
+ http_method: "POST"
13023
14616
  }
13024
14617
  });
13025
- sdk.registerFunction("api::semantic-list", async (req) => {
14618
+ sdk.registerFunction("api::slot-replace", async (req) => {
13026
14619
  const authErr = checkAuth(req, secret);
13027
14620
  if (authErr) return authErr;
14621
+ const body = req.body ?? {};
14622
+ const label = asNonEmptyString$1(body["label"]);
14623
+ const content = body["content"];
14624
+ if (!label || typeof content !== "string") return {
14625
+ status_code: 400,
14626
+ body: { error: "label and content (string) required" }
14627
+ };
14628
+ const result = await sdk.trigger({
14629
+ function_id: "mem::slot-replace",
14630
+ payload: {
14631
+ label,
14632
+ content
14633
+ }
14634
+ });
14635
+ const resp = result;
14636
+ if (resp?.success === false) {
14637
+ const notFound = resp.error?.includes("not found");
14638
+ const overLimit = resp.error?.includes("exceed");
14639
+ return {
14640
+ status_code: notFound ? 404 : overLimit ? 413 : 400,
14641
+ body: resp
14642
+ };
14643
+ }
13028
14644
  return {
13029
14645
  status_code: 200,
13030
- body: { semantic: await kv.list(KV.semantic) }
14646
+ body: result
13031
14647
  };
13032
14648
  });
13033
14649
  sdk.registerTrigger({
13034
14650
  type: "http",
13035
- function_id: "api::semantic-list",
14651
+ function_id: "api::slot-replace",
13036
14652
  config: {
13037
- api_path: "/agentmemory/semantic",
13038
- http_method: "GET"
14653
+ api_path: "/agentmemory/slot/replace",
14654
+ http_method: "POST"
13039
14655
  }
13040
14656
  });
13041
- sdk.registerFunction("api::procedural-list", async (req) => {
14657
+ sdk.registerFunction("api::slot-delete", async (req) => {
13042
14658
  const authErr = checkAuth(req, secret);
13043
14659
  if (authErr) return authErr;
14660
+ const label = asNonEmptyString$1(req.query_params?.["label"]);
14661
+ if (!label) return {
14662
+ status_code: 400,
14663
+ body: { error: "label query param required" }
14664
+ };
14665
+ const result = await sdk.trigger({
14666
+ function_id: "mem::slot-delete",
14667
+ payload: { label }
14668
+ });
14669
+ const resp = result;
14670
+ if (resp?.success === false) return {
14671
+ status_code: resp.error?.includes("not found") ? 404 : 400,
14672
+ body: resp
14673
+ };
13044
14674
  return {
13045
14675
  status_code: 200,
13046
- body: { procedural: await kv.list(KV.procedural) }
14676
+ body: result
13047
14677
  };
13048
14678
  });
13049
14679
  sdk.registerTrigger({
13050
14680
  type: "http",
13051
- function_id: "api::procedural-list",
14681
+ function_id: "api::slot-delete",
13052
14682
  config: {
13053
- api_path: "/agentmemory/procedural",
13054
- http_method: "GET"
14683
+ api_path: "/agentmemory/slot",
14684
+ http_method: "DELETE"
13055
14685
  }
13056
14686
  });
13057
- sdk.registerFunction("api::relations-list", async (req) => {
14687
+ sdk.registerFunction("api::slot-reflect", async (req) => {
13058
14688
  const authErr = checkAuth(req, secret);
13059
14689
  if (authErr) return authErr;
14690
+ const body = req.body ?? {};
14691
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14692
+ if (!sessionId) return {
14693
+ status_code: 400,
14694
+ body: { error: "sessionId required" }
14695
+ };
14696
+ const maxObservations = parseOptionalPositiveInt(body["maxObservations"]);
14697
+ if (maxObservations === null) return {
14698
+ status_code: 400,
14699
+ body: { error: "maxObservations must be a positive integer" }
14700
+ };
14701
+ const payload = { sessionId };
14702
+ if (maxObservations !== void 0) payload["maxObservations"] = maxObservations;
13060
14703
  return {
13061
14704
  status_code: 200,
13062
- body: { relations: await kv.list(KV.relations) }
14705
+ body: await sdk.trigger({
14706
+ function_id: "mem::slot-reflect",
14707
+ payload
14708
+ })
13063
14709
  };
13064
14710
  });
13065
14711
  sdk.registerTrigger({
13066
14712
  type: "http",
13067
- function_id: "api::relations-list",
14713
+ function_id: "api::slot-reflect",
13068
14714
  config: {
13069
- api_path: "/agentmemory/relations",
13070
- http_method: "GET"
14715
+ api_path: "/agentmemory/slot/reflect",
14716
+ http_method: "POST"
13071
14717
  }
13072
14718
  });
13073
14719
  sdk.registerFunction("api::action-create", async (req) => {
@@ -13304,9 +14950,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13304
14950
  sdk.registerFunction("api::routine-create", async (req) => {
13305
14951
  const authErr = checkAuth(req, secret);
13306
14952
  if (authErr) return authErr;
13307
- if (!req.body?.name) return {
14953
+ if (!req.body?.name || !req.body?.steps) return {
13308
14954
  status_code: 400,
13309
- body: { error: "name is required" }
14955
+ body: { error: "name and steps are required" }
13310
14956
  };
13311
14957
  return {
13312
14958
  status_code: 201,
@@ -14555,10 +16201,21 @@ function registerEventTriggers(sdk, kv) {
14555
16201
  function_id: "event::observation",
14556
16202
  config: { topic: "agentmemory.observation" }
14557
16203
  });
14558
- sdk.registerFunction("event::session::stopped", async (data) => sdk.trigger({
14559
- function_id: "mem::summarize",
14560
- payload: data
14561
- }));
16204
+ sdk.registerFunction("event::session::stopped", async (data) => {
16205
+ const summary = await sdk.trigger({
16206
+ function_id: "mem::summarize",
16207
+ payload: data
16208
+ });
16209
+ if (isReflectEnabled()) try {
16210
+ sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
16211
+ } catch (err) {
16212
+ logger.warn("slot-reflect triggerVoid failed", {
16213
+ sessionId: data.sessionId,
16214
+ error: err instanceof Error ? err.message : String(err)
16215
+ });
16216
+ }
16217
+ return summary;
16218
+ });
14562
16219
  sdk.registerTrigger({
14563
16220
  type: "durable:subscriber",
14564
16221
  function_id: "event::session::stopped",
@@ -14737,6 +16394,35 @@ const CORE_TOOLS = [
14737
16394
  required: ["query"]
14738
16395
  }
14739
16396
  },
16397
+ {
16398
+ name: "memory_vision_search",
16399
+ description: "Cross-modal image search via CLIP embeddings. Pass queryText to find screenshots matching a description, or queryImageBase64/queryImageRef to find similar images. Requires AGENTMEMORY_IMAGE_EMBEDDINGS=true.",
16400
+ inputSchema: {
16401
+ type: "object",
16402
+ properties: {
16403
+ queryText: {
16404
+ type: "string",
16405
+ description: "Text query (e.g. 'login form with error banner')"
16406
+ },
16407
+ queryImageRef: {
16408
+ type: "string",
16409
+ description: "Absolute path to a stored image to match against"
16410
+ },
16411
+ queryImageBase64: {
16412
+ type: "string",
16413
+ description: "Raw base64 image bytes or data URL"
16414
+ },
16415
+ topK: {
16416
+ type: "number",
16417
+ description: "Max results (default 10, max 50)"
16418
+ },
16419
+ sessionId: {
16420
+ type: "string",
16421
+ description: "Filter to a single session"
16422
+ }
16423
+ }
16424
+ }
16425
+ },
14740
16426
  {
14741
16427
  name: "memory_timeline",
14742
16428
  description: "Chronological observations around an anchor point.",
@@ -15524,6 +17210,110 @@ const V073_TOOLS = [{
15524
17210
  }
15525
17211
  }
15526
17212
  }];
17213
+ const V010_SLOTS_TOOLS = [
17214
+ {
17215
+ name: "memory_slot_list",
17216
+ description: "List all memory slots (pinned + project + global). Slots are editable, size-limited memory units the agent can read and modify across sessions.",
17217
+ inputSchema: {
17218
+ type: "object",
17219
+ properties: {}
17220
+ }
17221
+ },
17222
+ {
17223
+ name: "memory_slot_get",
17224
+ description: "Read a single slot by label.",
17225
+ inputSchema: {
17226
+ type: "object",
17227
+ properties: { label: {
17228
+ type: "string",
17229
+ description: "Slot label (e.g. 'persona', 'pending_items')"
17230
+ } },
17231
+ required: ["label"]
17232
+ }
17233
+ },
17234
+ {
17235
+ name: "memory_slot_create",
17236
+ description: "Create a new slot. Reject if a slot with the same label already exists.",
17237
+ inputSchema: {
17238
+ type: "object",
17239
+ properties: {
17240
+ label: {
17241
+ type: "string",
17242
+ description: "Slot label — lowercase, starts with letter, [a-z0-9_]"
17243
+ },
17244
+ content: {
17245
+ type: "string",
17246
+ description: "Initial content (default empty)"
17247
+ },
17248
+ sizeLimit: {
17249
+ type: "number",
17250
+ description: "Max chars (default 2000, hard cap 20000)"
17251
+ },
17252
+ description: {
17253
+ type: "string",
17254
+ description: "What this slot is for"
17255
+ },
17256
+ pinned: {
17257
+ type: "string",
17258
+ description: "'false' to exclude from context injection; default true"
17259
+ },
17260
+ scope: {
17261
+ type: "string",
17262
+ description: "'project' (default) or 'global' (shared across projects)"
17263
+ }
17264
+ },
17265
+ required: ["label"]
17266
+ }
17267
+ },
17268
+ {
17269
+ name: "memory_slot_append",
17270
+ description: "Append text to an existing slot. Fails with 413 if the append would exceed the slot's sizeLimit — agent must compact via memory_slot_replace first.",
17271
+ inputSchema: {
17272
+ type: "object",
17273
+ properties: {
17274
+ label: {
17275
+ type: "string",
17276
+ description: "Slot label"
17277
+ },
17278
+ text: {
17279
+ type: "string",
17280
+ description: "Text to append"
17281
+ }
17282
+ },
17283
+ required: ["label", "text"]
17284
+ }
17285
+ },
17286
+ {
17287
+ name: "memory_slot_replace",
17288
+ description: "Replace slot content in place. Fails if content exceeds sizeLimit.",
17289
+ inputSchema: {
17290
+ type: "object",
17291
+ properties: {
17292
+ label: {
17293
+ type: "string",
17294
+ description: "Slot label"
17295
+ },
17296
+ content: {
17297
+ type: "string",
17298
+ description: "New full content"
17299
+ }
17300
+ },
17301
+ required: ["label", "content"]
17302
+ }
17303
+ },
17304
+ {
17305
+ name: "memory_slot_delete",
17306
+ description: "Delete a slot. Seeded default slots can be deleted unless marked readOnly.",
17307
+ inputSchema: {
17308
+ type: "object",
17309
+ properties: { label: {
17310
+ type: "string",
17311
+ description: "Slot label"
17312
+ } },
17313
+ required: ["label"]
17314
+ }
17315
+ }
17316
+ ];
15527
17317
  const ESSENTIAL_TOOLS = new Set([
15528
17318
  "memory_save",
15529
17319
  "memory_recall",
@@ -15542,7 +17332,8 @@ function getAllTools() {
15542
17332
  ...V051_TOOLS,
15543
17333
  ...V061_TOOLS,
15544
17334
  ...V070_TOOLS,
15545
- ...V073_TOOLS
17335
+ ...V073_TOOLS,
17336
+ ...V010_SLOTS_TOOLS
15546
17337
  ];
15547
17338
  }
15548
17339
  function getVisibleTools() {
@@ -15749,6 +17540,34 @@ function registerMcpEndpoints(sdk, kv, secret) {
15749
17540
  }] }
15750
17541
  };
15751
17542
  }
17543
+ case "memory_vision_search": {
17544
+ const queryText = typeof args.queryText === "string" ? args.queryText : void 0;
17545
+ const queryImageRef = typeof args.queryImageRef === "string" ? args.queryImageRef : void 0;
17546
+ const queryImageBase64 = typeof args.queryImageBase64 === "string" ? args.queryImageBase64 : void 0;
17547
+ if (!queryText && !queryImageRef && !queryImageBase64) return {
17548
+ status_code: 400,
17549
+ body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
17550
+ };
17551
+ const topK = Math.max(1, Math.min(50, asNumber(args.topK, 10) ?? 10));
17552
+ const sessionId = typeof args.sessionId === "string" ? args.sessionId : void 0;
17553
+ const result = await sdk.trigger({
17554
+ function_id: "mem::vision-search",
17555
+ payload: {
17556
+ queryText,
17557
+ queryImageRef,
17558
+ queryImageBase64,
17559
+ topK,
17560
+ sessionId
17561
+ }
17562
+ });
17563
+ return {
17564
+ status_code: 200,
17565
+ body: { content: [{
17566
+ type: "text",
17567
+ text: JSON.stringify(result, null, 2)
17568
+ }] }
17569
+ };
17570
+ }
15752
17571
  case "memory_timeline": {
15753
17572
  if (typeof args.anchor !== "string" || !args.anchor.trim()) return {
15754
17573
  status_code: 400,
@@ -16614,6 +18433,123 @@ function registerMcpEndpoints(sdk, kv, secret) {
16614
18433
  }] }
16615
18434
  };
16616
18435
  }
18436
+ case "memory_slot_list": {
18437
+ const result = await sdk.trigger({
18438
+ function_id: "mem::slot-list",
18439
+ payload: {}
18440
+ });
18441
+ return {
18442
+ status_code: 200,
18443
+ body: { content: [{
18444
+ type: "text",
18445
+ text: JSON.stringify(result, null, 2)
18446
+ }] }
18447
+ };
18448
+ }
18449
+ case "memory_slot_get": {
18450
+ const label = asNonEmptyString(args.label);
18451
+ if (!label) return {
18452
+ status_code: 400,
18453
+ body: { error: "label required" }
18454
+ };
18455
+ const result = await sdk.trigger({
18456
+ function_id: "mem::slot-get",
18457
+ payload: { label }
18458
+ });
18459
+ return {
18460
+ status_code: 200,
18461
+ body: { content: [{
18462
+ type: "text",
18463
+ text: JSON.stringify(result, null, 2)
18464
+ }] }
18465
+ };
18466
+ }
18467
+ case "memory_slot_create": {
18468
+ const label = asNonEmptyString(args.label);
18469
+ if (!label) return {
18470
+ status_code: 400,
18471
+ body: { error: "label required" }
18472
+ };
18473
+ const payload = { label };
18474
+ if (typeof args.content === "string") payload.content = args.content;
18475
+ if (typeof args.description === "string") payload.description = args.description;
18476
+ if (typeof args.sizeLimit === "number") payload.sizeLimit = args.sizeLimit;
18477
+ if (args.pinned === false || args.pinned === "false") payload.pinned = false;
18478
+ else if (args.pinned === true || args.pinned === "true") payload.pinned = true;
18479
+ if (args.scope === "global" || args.scope === "project") payload.scope = args.scope;
18480
+ const result = await sdk.trigger({
18481
+ function_id: "mem::slot-create",
18482
+ payload
18483
+ });
18484
+ return {
18485
+ status_code: 200,
18486
+ body: { content: [{
18487
+ type: "text",
18488
+ text: JSON.stringify(result, null, 2)
18489
+ }] }
18490
+ };
18491
+ }
18492
+ case "memory_slot_append": {
18493
+ const label = asNonEmptyString(args.label);
18494
+ const text = typeof args.text === "string" ? args.text : null;
18495
+ if (!label || !text) return {
18496
+ status_code: 400,
18497
+ body: { error: "label and text required" }
18498
+ };
18499
+ const result = await sdk.trigger({
18500
+ function_id: "mem::slot-append",
18501
+ payload: {
18502
+ label,
18503
+ text
18504
+ }
18505
+ });
18506
+ return {
18507
+ status_code: 200,
18508
+ body: { content: [{
18509
+ type: "text",
18510
+ text: JSON.stringify(result, null, 2)
18511
+ }] }
18512
+ };
18513
+ }
18514
+ case "memory_slot_replace": {
18515
+ const label = asNonEmptyString(args.label);
18516
+ if (!label || typeof args.content !== "string") return {
18517
+ status_code: 400,
18518
+ body: { error: "label and content (string) required" }
18519
+ };
18520
+ const result = await sdk.trigger({
18521
+ function_id: "mem::slot-replace",
18522
+ payload: {
18523
+ label,
18524
+ content: args.content
18525
+ }
18526
+ });
18527
+ return {
18528
+ status_code: 200,
18529
+ body: { content: [{
18530
+ type: "text",
18531
+ text: JSON.stringify(result, null, 2)
18532
+ }] }
18533
+ };
18534
+ }
18535
+ case "memory_slot_delete": {
18536
+ const label = asNonEmptyString(args.label);
18537
+ if (!label) return {
18538
+ status_code: 400,
18539
+ body: { error: "label required" }
18540
+ };
18541
+ const result = await sdk.trigger({
18542
+ function_id: "mem::slot-delete",
18543
+ payload: { label }
18544
+ });
18545
+ return {
18546
+ status_code: 200,
18547
+ body: { content: [{
18548
+ type: "text",
18549
+ text: JSON.stringify(result, null, 2)
18550
+ }] }
18551
+ };
18552
+ }
16617
18553
  default: return {
16618
18554
  status_code: 400,
16619
18555
  body: { error: `Unknown tool: ${name}` }
@@ -17268,11 +19204,13 @@ async function main() {
17268
19204
  const fallbackConfig = loadFallbackConfig();
17269
19205
  const provider = fallbackConfig.providers.length > 0 ? createFallbackProvider(config.provider, fallbackConfig) : createProvider(config.provider);
17270
19206
  const embeddingProvider = createEmbeddingProvider();
19207
+ const imageEmbeddingProvider = createImageEmbeddingProvider();
17271
19208
  console.log(`[agentmemory] Starting worker v${VERSION}...`);
17272
19209
  console.log(`[agentmemory] Engine: ${config.engineUrl}`);
17273
19210
  console.log(`[agentmemory] Provider: ${config.provider.provider} (${config.provider.model})`);
17274
19211
  if (embeddingProvider) console.log(`[agentmemory] Embedding provider: ${embeddingProvider.name} (${embeddingProvider.dimensions} dims)`);
17275
19212
  else console.log(`[agentmemory] Embedding provider: none (BM25-only mode)`);
19213
+ if (imageEmbeddingProvider) console.log(`[agentmemory] Image embedding provider: ${imageEmbeddingProvider.name} (${imageEmbeddingProvider.dimensions} dims) — vision-search active`);
17276
19214
  console.log(`[agentmemory] REST API: http://localhost:${config.restPort}/agentmemory/*`);
17277
19215
  console.log(`[agentmemory] Streams: ws://localhost:${config.streamsPort}`);
17278
19216
  const sdk = registerWorker(config.engineUrl, {
@@ -17291,6 +19229,10 @@ async function main() {
17291
19229
  initMetrics(hasGetMeter(sdk) ? sdk.getMeter.bind(sdk) : void 0);
17292
19230
  registerPrivacyFunction(sdk);
17293
19231
  registerObserveFunction(sdk, kv, dedupMap, config.maxObservationsPerSession);
19232
+ registerImageQuotaCleanup(sdk, kv);
19233
+ registerVisionSearchFunctions(sdk, kv, imageEmbeddingProvider);
19234
+ if (isSlotsEnabled()) registerSlotsFunctions(sdk, kv);
19235
+ registerDiskSizeManager(sdk, kv);
17294
19236
  registerCompressFunction(sdk, kv, provider, metricsStore);
17295
19237
  registerSearchFunction(sdk, kv);
17296
19238
  registerContextFunction(sdk, kv, config.tokenBudget);
@@ -17357,6 +19299,7 @@ async function main() {
17357
19299
  registerReplayFunctions(sdk, kv);
17358
19300
  console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
17359
19301
  console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
19302
+ if (isSlotsEnabled()) console.log(`[agentmemory] Slots: enabled (pinned editable memory). Reflect on Stop hook: ${isReflectEnabled() ? "on" : "off"}`);
17360
19303
  const snapshotConfig = loadSnapshotConfig();
17361
19304
  if (snapshotConfig.enabled) {
17362
19305
  registerSnapshotFunction(sdk, kv, snapshotConfig.dir);
@@ -17394,7 +19337,7 @@ async function main() {
17394
19337
  }
17395
19338
  }
17396
19339
  console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
17397
- console.log(`[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`);
19340
+ console.log(`[agentmemory] Endpoints: 107 REST + ${getAllTools().length} MCP tools + 6 MCP resources + 3 MCP prompts`);
17398
19341
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
17399
19342
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
17400
19343
  const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);