@agentmemory/agentmemory 0.9.0 → 0.9.2

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 +42 -15
  2. package/dist/cli.mjs +60 -25
  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-Dq5wcV-a.mjs +3 -0
  29. package/dist/image-store-BLOkD0xV.mjs +3 -0
  30. package/dist/index.mjs +2054 -144
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/{src-B3pEsBSb.mjs → src-tmuZyobT.mjs} +1974 -253
  33. package/dist/src-tmuZyobT.mjs.map +1 -0
  34. package/dist/{standalone-DXc-BEqr.mjs → standalone-BiwX0rdC.mjs} +2 -2
  35. package/dist/{standalone-DXc-BEqr.mjs.map → standalone-BiwX0rdC.mjs.map} +1 -1
  36. package/dist/standalone.mjs +136 -2
  37. package/dist/standalone.mjs.map +1 -1
  38. package/dist/{tools-registry-DXIK5CxQ.mjs → tools-registry-CHH84gIQ.mjs} +166 -12
  39. package/dist/tools-registry-CHH84gIQ.mjs.map +1 -0
  40. package/dist/viewer/index.html +249 -62
  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-B3pEsBSb.mjs.map +0 -1
  68. package/dist/tools-registry-DXIK5CxQ.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) {
@@ -3161,9 +4228,33 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
3161
4228
  error: "no_observations"
3162
4229
  };
3163
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."
4237
+ };
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.0";
5621
+ const VERSION = "0.9.2";
4504
5622
 
4505
5623
  //#endregion
4506
5624
  //#region src/functions/export-import.ts
@@ -4618,7 +5736,9 @@ function registerExportImportFunction(sdk, kv) {
4618
5736
  "0.8.11",
4619
5737
  "0.8.12",
4620
5738
  "0.8.13",
4621
- "0.9.0"
5739
+ "0.9.0",
5740
+ "0.9.1",
5741
+ "0.9.2"
4622
5742
  ]).has(importData.version)) return {
4623
5743
  success: false,
4624
5744
  error: `Unsupported export version: ${importData.version}`
@@ -10975,6 +12095,7 @@ function registerRetentionFunctions(sdk, kv) {
10975
12095
  const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
10976
12096
  const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
10977
12097
  const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
12098
+ const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
10978
12099
  const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
10979
12100
  if (data?.dryRun) return {
10980
12101
  success: true,
@@ -11006,6 +12127,8 @@ function registerRetentionFunctions(sdk, kv) {
11006
12127
  resolvedSource = "semantic";
11007
12128
  }
11008
12129
  if (!scope || !resolvedSource) continue;
12130
+ const mem = await kv.get(scope, candidate.memoryId);
12131
+ if (mem && mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
11009
12132
  await kv.delete(scope, candidate.memoryId);
11010
12133
  await kv.delete(KV.retentionScores, candidate.memoryId);
11011
12134
  await deleteAccessLog(kv, candidate.memoryId);
@@ -11443,6 +12566,92 @@ function rawFromCompressed(obs) {
11443
12566
  }
11444
12567
  };
11445
12568
  }
12569
+ 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];
12570
+ async function deriveCrystalAndLessons(kv, sessionId, project, rawObs, compressed, firstPrompt) {
12571
+ if (rawObs.length === 0) return;
12572
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
12573
+ const files = /* @__PURE__ */ new Set();
12574
+ const tools = /* @__PURE__ */ new Set();
12575
+ for (const c of compressed) {
12576
+ for (const f of c.files || []) files.add(f);
12577
+ if (c.type && c.type !== "conversation" && c.title) tools.add(c.title);
12578
+ }
12579
+ const assistantTexts = [];
12580
+ const userPrompts = [];
12581
+ for (const r of rawObs) {
12582
+ if (typeof r.assistantResponse === "string" && r.assistantResponse.trim()) assistantTexts.push(r.assistantResponse);
12583
+ if (typeof r.userPrompt === "string" && r.userPrompt.trim()) userPrompts.push(r.userPrompt);
12584
+ }
12585
+ const lessonMatches = /* @__PURE__ */ new Map();
12586
+ for (const text of assistantTexts.concat(userPrompts).slice(0, 200)) for (const pat of LESSON_PATTERNS) {
12587
+ pat.lastIndex = 0;
12588
+ let m;
12589
+ while ((m = pat.exec(text)) !== null && lessonMatches.size < 40) {
12590
+ const snippet = m[0].replace(/\s+/g, " ").trim();
12591
+ if (snippet.length >= 20 && snippet.length <= 220) {
12592
+ const key = snippet.toLowerCase();
12593
+ if (!lessonMatches.has(key)) lessonMatches.set(key, snippet);
12594
+ }
12595
+ }
12596
+ }
12597
+ const lessonEntries = Array.from(lessonMatches.values()).slice(0, 20);
12598
+ const lessonIds = [];
12599
+ for (const content of lessonEntries) {
12600
+ const lessonId = fingerprintId("lesson", content.trim().toLowerCase());
12601
+ try {
12602
+ const existing = await kv.get(KV.lessons, lessonId);
12603
+ if (existing) {
12604
+ const existingSources = existing.sourceIds || [];
12605
+ const mergedSources = existingSources.includes(sessionId) ? existingSources : [...existingSources, sessionId];
12606
+ const existingTags = existing.tags || [];
12607
+ const mergedTags = existingTags.includes("auto-import") ? existingTags : [...existingTags, "auto-import"];
12608
+ const merged = {
12609
+ ...existing,
12610
+ sourceIds: mergedSources,
12611
+ tags: mergedTags,
12612
+ reinforcements: (existing.reinforcements || 0) + 1,
12613
+ updatedAt: createdAt,
12614
+ lastReinforcedAt: createdAt
12615
+ };
12616
+ await kv.set(KV.lessons, lessonId, merged);
12617
+ } else {
12618
+ const lesson = {
12619
+ id: lessonId,
12620
+ content,
12621
+ context: firstPrompt || project,
12622
+ confidence: .4,
12623
+ reinforcements: 0,
12624
+ source: "consolidation",
12625
+ sourceIds: [sessionId],
12626
+ project,
12627
+ tags: ["auto-import"],
12628
+ createdAt,
12629
+ updatedAt: createdAt,
12630
+ decayRate: .05
12631
+ };
12632
+ await kv.set(KV.lessons, lessonId, lesson);
12633
+ }
12634
+ lessonIds.push(lessonId);
12635
+ } catch {}
12636
+ }
12637
+ const crystalId = fingerprintId("crystal", sessionId);
12638
+ const narrativePreview = firstPrompt ? firstPrompt.slice(0, 300) : compressed.slice(0, 5).map((c) => c.narrative || c.title).filter(Boolean).join(" · ").slice(0, 300);
12639
+ try {
12640
+ const existingCrystal = await kv.get(KV.crystals, crystalId);
12641
+ const crystal = {
12642
+ id: crystalId,
12643
+ narrative: narrativePreview || `Session ${sessionId.slice(0, 12)} (${rawObs.length} observations)`,
12644
+ keyOutcomes: Array.from(tools).slice(0, 8),
12645
+ filesAffected: Array.from(files).slice(0, 20),
12646
+ lessons: lessonIds,
12647
+ sourceActionIds: existingCrystal?.sourceActionIds ?? [],
12648
+ sessionId,
12649
+ project,
12650
+ createdAt: existingCrystal?.createdAt ?? createdAt
12651
+ };
12652
+ await kv.set(KV.crystals, crystalId, crystal);
12653
+ } catch {}
12654
+ }
11446
12655
  function isRawShape(o) {
11447
12656
  if (!o || typeof o !== "object") return false;
11448
12657
  return typeof o.hookType === "string";
@@ -11553,6 +12762,8 @@ function registerReplayFunctions(sdk, kv) {
11553
12762
  }
11554
12763
  const parsed = parseJsonlText(text, generateId("sess"));
11555
12764
  if (parsed.observations.length === 0) continue;
12765
+ const firstPromptObs = parsed.observations.find((o) => typeof o.userPrompt === "string" && o.userPrompt.trim().length > 0);
12766
+ const firstPrompt = firstPromptObs?.userPrompt ? firstPromptObs.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
11556
12767
  const existing = await kv.get(KV.sessions, parsed.sessionId);
11557
12768
  if (existing) {
11558
12769
  existing.observationCount = (existing.observationCount || 0) + parsed.observations.length;
@@ -11560,6 +12771,7 @@ function registerReplayFunctions(sdk, kv) {
11560
12771
  if (existing.status === "active") existing.status = "completed";
11561
12772
  const existingTags = existing.tags || [];
11562
12773
  if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
12774
+ if (!existing.firstPrompt && firstPrompt) existing.firstPrompt = firstPrompt;
11563
12775
  await kv.set(KV.sessions, existing.id, existing);
11564
12776
  } else {
11565
12777
  const session = {
@@ -11570,13 +12782,22 @@ function registerReplayFunctions(sdk, kv) {
11570
12782
  endedAt: parsed.endedAt,
11571
12783
  status: "completed",
11572
12784
  observationCount: parsed.observations.length,
11573
- tags: ["jsonl-import"]
12785
+ tags: ["jsonl-import"],
12786
+ firstPrompt
11574
12787
  };
11575
12788
  await kv.set(KV.sessions, session.id, session);
11576
12789
  }
11577
- await Promise.all(parsed.observations.map((obs) => kv.set(KV.observations(parsed.sessionId), obs.id, obs)));
12790
+ const searchIndex = getSearchIndex();
12791
+ const compressed = [];
12792
+ await Promise.all(parsed.observations.map(async (obs) => {
12793
+ const synthetic = buildSyntheticCompression(obs);
12794
+ compressed.push(synthetic);
12795
+ await kv.set(KV.observations(parsed.sessionId), obs.id, synthetic);
12796
+ searchIndex.add(synthetic);
12797
+ }));
11578
12798
  observationCount += parsed.observations.length;
11579
12799
  sessionIds.push(parsed.sessionId);
12800
+ await deriveCrystalAndLessons(kv, parsed.sessionId, parsed.project, parsed.observations, compressed, firstPrompt);
11580
12801
  }
11581
12802
  await safeAudit(kv, "import", "mem::replay::import-jsonl", sessionIds, {
11582
12803
  source: "jsonl",
@@ -11610,6 +12831,7 @@ function evaluateHealth(snapshot, config = {}) {
11610
12831
  ...config
11611
12832
  };
11612
12833
  const alerts = [];
12834
+ const notes = [];
11613
12835
  let critical = false;
11614
12836
  let degraded = false;
11615
12837
  if (snapshot.connectionState === "disconnected" || snapshot.connectionState === "failed") {
@@ -11643,10 +12865,11 @@ function evaluateHealth(snapshot, config = {}) {
11643
12865
  } else if (memPercent > cfg.memoryWarnPercent && rssAboveFloor) {
11644
12866
  alerts.push(`memory_warn_${Math.round(memPercent)}%_rss${memMb}mb`);
11645
12867
  degraded = true;
11646
- } else if (memPercent > cfg.memoryWarnPercent) alerts.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
12868
+ } else if (memPercent > cfg.memoryWarnPercent) notes.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
11647
12869
  return {
11648
12870
  status: critical ? "critical" : degraded ? "degraded" : "healthy",
11649
- alerts
12871
+ alerts,
12872
+ notes
11650
12873
  };
11651
12874
  }
11652
12875
 
@@ -11723,6 +12946,7 @@ function registerHealthMonitor(sdk, kv) {
11723
12946
  const evaluated = evaluateHealth(snapshot);
11724
12947
  snapshot.status = evaluated.status;
11725
12948
  snapshot.alerts = evaluated.alerts;
12949
+ snapshot.notes = evaluated.notes;
11726
12950
  await kv.set(KV.health, "latest", snapshot).catch(() => {});
11727
12951
  return snapshot;
11728
12952
  }
@@ -11765,6 +12989,7 @@ function buildViewerCsp(nonce) {
11765
12989
 
11766
12990
  //#endregion
11767
12991
  //#region src/viewer/document.ts
12992
+ const VIEWER_VERSION_PLACEHOLDER = "__AGENTMEMORY_VERSION__";
11768
12993
  function loadViewerTemplate() {
11769
12994
  const base = dirname(fileURLToPath(import.meta.url));
11770
12995
  const candidates = [
@@ -11783,7 +13008,7 @@ function renderViewerDocument() {
11783
13008
  const nonce = createViewerNonce();
11784
13009
  return {
11785
13010
  found: true,
11786
- html: template.replaceAll(VIEWER_NONCE_PLACEHOLDER, nonce),
13011
+ html: template.replaceAll(VIEWER_NONCE_PLACEHOLDER, nonce).replaceAll(VIEWER_VERSION_PLACEHOLDER, VERSION),
11787
13012
  csp: buildViewerCsp(nonce)
11788
13013
  };
11789
13014
  }
@@ -12065,9 +13290,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12065
13290
  sdk.registerFunction("api::replay::sessions", async (req) => {
12066
13291
  const authErr = checkAuth(req, secret);
12067
13292
  if (authErr) return authErr;
13293
+ const sessions = await kv.list(KV.sessions);
13294
+ sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
12068
13295
  return {
12069
13296
  status_code: 200,
12070
- body: await sdk.trigger({ function_id: "mem::replay::sessions" })
13297
+ body: {
13298
+ success: true,
13299
+ sessions
13300
+ }
12071
13301
  };
12072
13302
  });
12073
13303
  sdk.registerTrigger({
@@ -12862,13 +14092,16 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12862
14092
  const parsedLimit = parseOptionalInt(req.query_params?.["limit"]);
12863
14093
  return {
12864
14094
  status_code: 200,
12865
- body: await sdk.trigger({
12866
- function_id: "mem::audit-query",
12867
- payload: {
12868
- operation: req.query_params?.["operation"],
12869
- limit: parsedLimit ?? 50
12870
- }
12871
- })
14095
+ body: {
14096
+ entries: await sdk.trigger({
14097
+ function_id: "mem::audit-query",
14098
+ payload: {
14099
+ operation: req.query_params?.["operation"],
14100
+ limit: parsedLimit ?? 50
14101
+ }
14102
+ }),
14103
+ success: true
14104
+ }
12872
14105
  };
12873
14106
  });
12874
14107
  sdk.registerTrigger({
@@ -12960,64 +14193,444 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12960
14193
  };
12961
14194
  } catch {
12962
14195
  return {
12963
- status_code: 404,
12964
- body: { error: "Snapshots not enabled" }
14196
+ status_code: 404,
14197
+ body: { error: "Snapshots not enabled" }
14198
+ };
14199
+ }
14200
+ });
14201
+ sdk.registerTrigger({
14202
+ type: "http",
14203
+ function_id: "api::snapshot-create",
14204
+ config: {
14205
+ api_path: "/agentmemory/snapshot/create",
14206
+ http_method: "POST"
14207
+ }
14208
+ });
14209
+ sdk.registerFunction("api::snapshot-restore", async (req) => {
14210
+ const authErr = checkAuth(req, secret);
14211
+ if (authErr) return authErr;
14212
+ if (!req.body?.commitHash) return {
14213
+ status_code: 400,
14214
+ body: { error: "commitHash is required" }
14215
+ };
14216
+ try {
14217
+ return {
14218
+ status_code: 200,
14219
+ body: await sdk.trigger({
14220
+ function_id: "mem::snapshot-restore",
14221
+ payload: req.body
14222
+ })
14223
+ };
14224
+ } catch {
14225
+ return {
14226
+ status_code: 404,
14227
+ body: { error: "Snapshots not enabled" }
14228
+ };
14229
+ }
14230
+ });
14231
+ sdk.registerTrigger({
14232
+ type: "http",
14233
+ function_id: "api::snapshot-restore",
14234
+ config: {
14235
+ api_path: "/agentmemory/snapshot/restore",
14236
+ http_method: "POST"
14237
+ }
14238
+ });
14239
+ sdk.registerFunction("api::memories", async (req) => {
14240
+ const authErr = checkAuth(req, secret);
14241
+ if (authErr) return authErr;
14242
+ const memories = await kv.list(KV.memories);
14243
+ return {
14244
+ status_code: 200,
14245
+ body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
14246
+ };
14247
+ });
14248
+ sdk.registerTrigger({
14249
+ type: "http",
14250
+ function_id: "api::memories",
14251
+ config: {
14252
+ api_path: "/agentmemory/memories",
14253
+ http_method: "GET"
14254
+ }
14255
+ });
14256
+ sdk.registerFunction("api::semantic-list", async (req) => {
14257
+ const authErr = checkAuth(req, secret);
14258
+ if (authErr) return authErr;
14259
+ return {
14260
+ status_code: 200,
14261
+ body: { semantic: await kv.list(KV.semantic) }
14262
+ };
14263
+ });
14264
+ sdk.registerTrigger({
14265
+ type: "http",
14266
+ function_id: "api::semantic-list",
14267
+ config: {
14268
+ api_path: "/agentmemory/semantic",
14269
+ http_method: "GET"
14270
+ }
14271
+ });
14272
+ sdk.registerFunction("api::procedural-list", async (req) => {
14273
+ const authErr = checkAuth(req, secret);
14274
+ if (authErr) return authErr;
14275
+ return {
14276
+ status_code: 200,
14277
+ body: { procedural: await kv.list(KV.procedural) }
14278
+ };
14279
+ });
14280
+ sdk.registerTrigger({
14281
+ type: "http",
14282
+ function_id: "api::procedural-list",
14283
+ config: {
14284
+ api_path: "/agentmemory/procedural",
14285
+ http_method: "GET"
14286
+ }
14287
+ });
14288
+ sdk.registerFunction("api::relations-list", async (req) => {
14289
+ const authErr = checkAuth(req, secret);
14290
+ if (authErr) return authErr;
14291
+ return {
14292
+ status_code: 200,
14293
+ body: { relations: await kv.list(KV.relations) }
14294
+ };
14295
+ });
14296
+ sdk.registerTrigger({
14297
+ type: "http",
14298
+ function_id: "api::relations-list",
14299
+ config: {
14300
+ api_path: "/agentmemory/relations",
14301
+ http_method: "GET"
14302
+ }
14303
+ });
14304
+ sdk.registerFunction("api::vision-search", async (req) => {
14305
+ const authErr = checkAuth(req, secret);
14306
+ if (authErr) return authErr;
14307
+ const body = req.body ?? {};
14308
+ const queryText = asNonEmptyString$1(body["queryText"]);
14309
+ const queryImageRef = asNonEmptyString$1(body["queryImageRef"]);
14310
+ const queryImageBase64 = asNonEmptyString$1(body["queryImageBase64"]);
14311
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14312
+ if (!queryText && !queryImageRef && !queryImageBase64) return {
14313
+ status_code: 400,
14314
+ body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
14315
+ };
14316
+ const topKParsed = parseOptionalPositiveInt(body["topK"]);
14317
+ if (topKParsed === null) return {
14318
+ status_code: 400,
14319
+ body: { error: "topK must be a positive integer" }
14320
+ };
14321
+ const payload = {};
14322
+ if (queryText) payload["queryText"] = queryText;
14323
+ if (queryImageRef) payload["queryImageRef"] = queryImageRef;
14324
+ if (queryImageBase64) payload["queryImageBase64"] = queryImageBase64;
14325
+ if (sessionId) payload["sessionId"] = sessionId;
14326
+ if (topKParsed !== void 0) payload["topK"] = Math.min(50, topKParsed);
14327
+ const result = await sdk.trigger({
14328
+ function_id: "mem::vision-search",
14329
+ payload
14330
+ });
14331
+ const resp = result;
14332
+ if (resp?.success === false) return {
14333
+ status_code: resp.error?.includes("disabled") ? 503 : 400,
14334
+ body: resp
14335
+ };
14336
+ return {
14337
+ status_code: 200,
14338
+ body: result
14339
+ };
14340
+ });
14341
+ sdk.registerTrigger({
14342
+ type: "http",
14343
+ function_id: "api::vision-search",
14344
+ config: {
14345
+ api_path: "/agentmemory/vision-search",
14346
+ http_method: "POST"
14347
+ }
14348
+ });
14349
+ sdk.registerFunction("api::vision-embed", async (req) => {
14350
+ const authErr = checkAuth(req, secret);
14351
+ if (authErr) return authErr;
14352
+ const body = req.body ?? {};
14353
+ const imageRef = asNonEmptyString$1(body["imageRef"]);
14354
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14355
+ const observationId = asNonEmptyString$1(body["observationId"]);
14356
+ if (!imageRef) return {
14357
+ status_code: 400,
14358
+ body: { error: "imageRef is required" }
14359
+ };
14360
+ const payload = { imageRef };
14361
+ if (sessionId) payload["sessionId"] = sessionId;
14362
+ if (observationId) payload["observationId"] = observationId;
14363
+ const result = await sdk.trigger({
14364
+ function_id: "mem::vision-embed",
14365
+ payload
14366
+ });
14367
+ const resp = result;
14368
+ if (resp?.success === false) return {
14369
+ status_code: resp.error?.includes("disabled") ? 503 : 400,
14370
+ body: resp
14371
+ };
14372
+ return {
14373
+ status_code: 200,
14374
+ body: result
14375
+ };
14376
+ });
14377
+ sdk.registerTrigger({
14378
+ type: "http",
14379
+ function_id: "api::vision-embed",
14380
+ config: {
14381
+ api_path: "/agentmemory/vision-embed",
14382
+ http_method: "POST"
14383
+ }
14384
+ });
14385
+ sdk.registerFunction("api::slot-list", async (req) => {
14386
+ const authErr = checkAuth(req, secret);
14387
+ if (authErr) return authErr;
14388
+ return {
14389
+ status_code: 200,
14390
+ body: await sdk.trigger({
14391
+ function_id: "mem::slot-list",
14392
+ payload: {}
14393
+ })
14394
+ };
14395
+ });
14396
+ sdk.registerTrigger({
14397
+ type: "http",
14398
+ function_id: "api::slot-list",
14399
+ config: {
14400
+ api_path: "/agentmemory/slots",
14401
+ http_method: "GET"
14402
+ }
14403
+ });
14404
+ sdk.registerFunction("api::slot-get", async (req) => {
14405
+ const authErr = checkAuth(req, secret);
14406
+ if (authErr) return authErr;
14407
+ const label = asNonEmptyString$1(req.query_params?.["label"]);
14408
+ if (!label) return {
14409
+ status_code: 400,
14410
+ body: { error: "label query param required" }
14411
+ };
14412
+ const result = await sdk.trigger({
14413
+ function_id: "mem::slot-get",
14414
+ payload: { label }
14415
+ });
14416
+ const resp = result;
14417
+ if (resp?.success === false) return {
14418
+ status_code: resp.error?.includes("not found") ? 404 : 400,
14419
+ body: resp
14420
+ };
14421
+ return {
14422
+ status_code: 200,
14423
+ body: result
14424
+ };
14425
+ });
14426
+ sdk.registerTrigger({
14427
+ type: "http",
14428
+ function_id: "api::slot-get",
14429
+ config: {
14430
+ api_path: "/agentmemory/slot",
14431
+ http_method: "GET"
14432
+ }
14433
+ });
14434
+ sdk.registerFunction("api::slot-create", async (req) => {
14435
+ const authErr = checkAuth(req, secret);
14436
+ if (authErr) return authErr;
14437
+ const body = req.body ?? {};
14438
+ const label = asNonEmptyString$1(body["label"]);
14439
+ if (!label) return {
14440
+ status_code: 400,
14441
+ body: { error: "label required" }
14442
+ };
14443
+ if (body["content"] !== void 0 && typeof body["content"] !== "string") return {
14444
+ status_code: 400,
14445
+ body: { error: "content must be a string" }
14446
+ };
14447
+ if (body["description"] !== void 0 && typeof body["description"] !== "string") return {
14448
+ status_code: 400,
14449
+ body: { error: "description must be a string" }
14450
+ };
14451
+ if (body["pinned"] !== void 0 && typeof body["pinned"] !== "boolean") return {
14452
+ status_code: 400,
14453
+ body: { error: "pinned must be a boolean" }
14454
+ };
14455
+ if (body["scope"] !== void 0 && body["scope"] !== "project" && body["scope"] !== "global") return {
14456
+ status_code: 400,
14457
+ body: { error: "scope must be 'project' or 'global'" }
14458
+ };
14459
+ const sizeLimit = parseOptionalPositiveInt(body["sizeLimit"]);
14460
+ if (sizeLimit === null) return {
14461
+ status_code: 400,
14462
+ body: { error: "sizeLimit must be a positive integer" }
14463
+ };
14464
+ if (sizeLimit !== void 0 && sizeLimit > 2e4) return {
14465
+ status_code: 400,
14466
+ body: { error: "sizeLimit must be <= 20000" }
14467
+ };
14468
+ const payload = { label };
14469
+ if (typeof body["content"] === "string") payload["content"] = body["content"];
14470
+ if (typeof body["description"] === "string") payload["description"] = body["description"];
14471
+ if (sizeLimit !== void 0) payload["sizeLimit"] = sizeLimit;
14472
+ if (typeof body["pinned"] === "boolean") payload["pinned"] = body["pinned"];
14473
+ if (body["scope"] === "project" || body["scope"] === "global") payload["scope"] = body["scope"];
14474
+ const result = await sdk.trigger({
14475
+ function_id: "mem::slot-create",
14476
+ payload
14477
+ });
14478
+ const resp = result;
14479
+ if (resp?.success === false) return {
14480
+ status_code: resp.error?.includes("exists") ? 409 : 400,
14481
+ body: resp
14482
+ };
14483
+ return {
14484
+ status_code: 201,
14485
+ body: result
14486
+ };
14487
+ });
14488
+ sdk.registerTrigger({
14489
+ type: "http",
14490
+ function_id: "api::slot-create",
14491
+ config: {
14492
+ api_path: "/agentmemory/slot",
14493
+ http_method: "POST"
14494
+ }
14495
+ });
14496
+ sdk.registerFunction("api::slot-append", async (req) => {
14497
+ const authErr = checkAuth(req, secret);
14498
+ if (authErr) return authErr;
14499
+ const body = req.body ?? {};
14500
+ const label = asNonEmptyString$1(body["label"]);
14501
+ const text = typeof body["text"] === "string" ? body["text"] : null;
14502
+ if (!label || !text) return {
14503
+ status_code: 400,
14504
+ body: { error: "label and text required" }
14505
+ };
14506
+ const result = await sdk.trigger({
14507
+ function_id: "mem::slot-append",
14508
+ payload: {
14509
+ label,
14510
+ text
14511
+ }
14512
+ });
14513
+ const resp = result;
14514
+ if (resp?.success === false) {
14515
+ const notFound = resp.error?.includes("not found");
14516
+ const overLimit = resp.error?.includes("exceed");
14517
+ return {
14518
+ status_code: notFound ? 404 : overLimit ? 413 : 400,
14519
+ body: resp
12965
14520
  };
12966
14521
  }
14522
+ return {
14523
+ status_code: 200,
14524
+ body: result
14525
+ };
12967
14526
  });
12968
14527
  sdk.registerTrigger({
12969
14528
  type: "http",
12970
- function_id: "api::snapshot-create",
14529
+ function_id: "api::slot-append",
12971
14530
  config: {
12972
- api_path: "/agentmemory/snapshot/create",
14531
+ api_path: "/agentmemory/slot/append",
12973
14532
  http_method: "POST"
12974
14533
  }
12975
14534
  });
12976
- sdk.registerFunction("api::snapshot-restore", async (req) => {
14535
+ sdk.registerFunction("api::slot-replace", async (req) => {
12977
14536
  const authErr = checkAuth(req, secret);
12978
14537
  if (authErr) return authErr;
12979
- if (!req.body?.commitHash) return {
14538
+ const body = req.body ?? {};
14539
+ const label = asNonEmptyString$1(body["label"]);
14540
+ const content = body["content"];
14541
+ if (!label || typeof content !== "string") return {
12980
14542
  status_code: 400,
12981
- body: { error: "commitHash is required" }
14543
+ body: { error: "label and content (string) required" }
12982
14544
  };
12983
- try {
12984
- return {
12985
- status_code: 200,
12986
- body: await sdk.trigger({
12987
- function_id: "mem::snapshot-restore",
12988
- payload: req.body
12989
- })
12990
- };
12991
- } catch {
14545
+ const result = await sdk.trigger({
14546
+ function_id: "mem::slot-replace",
14547
+ payload: {
14548
+ label,
14549
+ content
14550
+ }
14551
+ });
14552
+ const resp = result;
14553
+ if (resp?.success === false) {
14554
+ const notFound = resp.error?.includes("not found");
14555
+ const overLimit = resp.error?.includes("exceed");
12992
14556
  return {
12993
- status_code: 404,
12994
- body: { error: "Snapshots not enabled" }
14557
+ status_code: notFound ? 404 : overLimit ? 413 : 400,
14558
+ body: resp
12995
14559
  };
12996
14560
  }
14561
+ return {
14562
+ status_code: 200,
14563
+ body: result
14564
+ };
12997
14565
  });
12998
14566
  sdk.registerTrigger({
12999
14567
  type: "http",
13000
- function_id: "api::snapshot-restore",
14568
+ function_id: "api::slot-replace",
13001
14569
  config: {
13002
- api_path: "/agentmemory/snapshot/restore",
14570
+ api_path: "/agentmemory/slot/replace",
13003
14571
  http_method: "POST"
13004
14572
  }
13005
14573
  });
13006
- sdk.registerFunction("api::memories", async (req) => {
14574
+ sdk.registerFunction("api::slot-delete", async (req) => {
13007
14575
  const authErr = checkAuth(req, secret);
13008
14576
  if (authErr) return authErr;
13009
- const memories = await kv.list(KV.memories);
14577
+ const label = asNonEmptyString$1(req.query_params?.["label"]);
14578
+ if (!label) return {
14579
+ status_code: 400,
14580
+ body: { error: "label query param required" }
14581
+ };
14582
+ const result = await sdk.trigger({
14583
+ function_id: "mem::slot-delete",
14584
+ payload: { label }
14585
+ });
14586
+ const resp = result;
14587
+ if (resp?.success === false) return {
14588
+ status_code: resp.error?.includes("not found") ? 404 : 400,
14589
+ body: resp
14590
+ };
13010
14591
  return {
13011
14592
  status_code: 200,
13012
- body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
14593
+ body: result
13013
14594
  };
13014
14595
  });
13015
14596
  sdk.registerTrigger({
13016
14597
  type: "http",
13017
- function_id: "api::memories",
14598
+ function_id: "api::slot-delete",
13018
14599
  config: {
13019
- api_path: "/agentmemory/memories",
13020
- http_method: "GET"
14600
+ api_path: "/agentmemory/slot",
14601
+ http_method: "DELETE"
14602
+ }
14603
+ });
14604
+ sdk.registerFunction("api::slot-reflect", async (req) => {
14605
+ const authErr = checkAuth(req, secret);
14606
+ if (authErr) return authErr;
14607
+ const body = req.body ?? {};
14608
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14609
+ if (!sessionId) return {
14610
+ status_code: 400,
14611
+ body: { error: "sessionId required" }
14612
+ };
14613
+ const maxObservations = parseOptionalPositiveInt(body["maxObservations"]);
14614
+ if (maxObservations === null) return {
14615
+ status_code: 400,
14616
+ body: { error: "maxObservations must be a positive integer" }
14617
+ };
14618
+ const payload = { sessionId };
14619
+ if (maxObservations !== void 0) payload["maxObservations"] = maxObservations;
14620
+ return {
14621
+ status_code: 200,
14622
+ body: await sdk.trigger({
14623
+ function_id: "mem::slot-reflect",
14624
+ payload
14625
+ })
14626
+ };
14627
+ });
14628
+ sdk.registerTrigger({
14629
+ type: "http",
14630
+ function_id: "api::slot-reflect",
14631
+ config: {
14632
+ api_path: "/agentmemory/slot/reflect",
14633
+ http_method: "POST"
13021
14634
  }
13022
14635
  });
13023
14636
  sdk.registerFunction("api::action-create", async (req) => {
@@ -13254,9 +14867,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13254
14867
  sdk.registerFunction("api::routine-create", async (req) => {
13255
14868
  const authErr = checkAuth(req, secret);
13256
14869
  if (authErr) return authErr;
13257
- if (!req.body?.name) return {
14870
+ if (!req.body?.name || !req.body?.steps) return {
13258
14871
  status_code: 400,
13259
- body: { error: "name is required" }
14872
+ body: { error: "name and steps are required" }
13260
14873
  };
13261
14874
  return {
13262
14875
  status_code: 201,
@@ -14505,10 +16118,21 @@ function registerEventTriggers(sdk, kv) {
14505
16118
  function_id: "event::observation",
14506
16119
  config: { topic: "agentmemory.observation" }
14507
16120
  });
14508
- sdk.registerFunction("event::session::stopped", async (data) => sdk.trigger({
14509
- function_id: "mem::summarize",
14510
- payload: data
14511
- }));
16121
+ sdk.registerFunction("event::session::stopped", async (data) => {
16122
+ const summary = await sdk.trigger({
16123
+ function_id: "mem::summarize",
16124
+ payload: data
16125
+ });
16126
+ if (isReflectEnabled()) try {
16127
+ sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
16128
+ } catch (err) {
16129
+ logger.warn("slot-reflect triggerVoid failed", {
16130
+ sessionId: data.sessionId,
16131
+ error: err instanceof Error ? err.message : String(err)
16132
+ });
16133
+ }
16134
+ return summary;
16135
+ });
14512
16136
  sdk.registerTrigger({
14513
16137
  type: "durable:subscriber",
14514
16138
  function_id: "event::session::stopped",
@@ -14687,6 +16311,35 @@ const CORE_TOOLS = [
14687
16311
  required: ["query"]
14688
16312
  }
14689
16313
  },
16314
+ {
16315
+ name: "memory_vision_search",
16316
+ 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.",
16317
+ inputSchema: {
16318
+ type: "object",
16319
+ properties: {
16320
+ queryText: {
16321
+ type: "string",
16322
+ description: "Text query (e.g. 'login form with error banner')"
16323
+ },
16324
+ queryImageRef: {
16325
+ type: "string",
16326
+ description: "Absolute path to a stored image to match against"
16327
+ },
16328
+ queryImageBase64: {
16329
+ type: "string",
16330
+ description: "Raw base64 image bytes or data URL"
16331
+ },
16332
+ topK: {
16333
+ type: "number",
16334
+ description: "Max results (default 10, max 50)"
16335
+ },
16336
+ sessionId: {
16337
+ type: "string",
16338
+ description: "Filter to a single session"
16339
+ }
16340
+ }
16341
+ }
16342
+ },
14690
16343
  {
14691
16344
  name: "memory_timeline",
14692
16345
  description: "Chronological observations around an anchor point.",
@@ -15474,6 +17127,110 @@ const V073_TOOLS = [{
15474
17127
  }
15475
17128
  }
15476
17129
  }];
17130
+ const V010_SLOTS_TOOLS = [
17131
+ {
17132
+ name: "memory_slot_list",
17133
+ description: "List all memory slots (pinned + project + global). Slots are editable, size-limited memory units the agent can read and modify across sessions.",
17134
+ inputSchema: {
17135
+ type: "object",
17136
+ properties: {}
17137
+ }
17138
+ },
17139
+ {
17140
+ name: "memory_slot_get",
17141
+ description: "Read a single slot by label.",
17142
+ inputSchema: {
17143
+ type: "object",
17144
+ properties: { label: {
17145
+ type: "string",
17146
+ description: "Slot label (e.g. 'persona', 'pending_items')"
17147
+ } },
17148
+ required: ["label"]
17149
+ }
17150
+ },
17151
+ {
17152
+ name: "memory_slot_create",
17153
+ description: "Create a new slot. Reject if a slot with the same label already exists.",
17154
+ inputSchema: {
17155
+ type: "object",
17156
+ properties: {
17157
+ label: {
17158
+ type: "string",
17159
+ description: "Slot label — lowercase, starts with letter, [a-z0-9_]"
17160
+ },
17161
+ content: {
17162
+ type: "string",
17163
+ description: "Initial content (default empty)"
17164
+ },
17165
+ sizeLimit: {
17166
+ type: "number",
17167
+ description: "Max chars (default 2000, hard cap 20000)"
17168
+ },
17169
+ description: {
17170
+ type: "string",
17171
+ description: "What this slot is for"
17172
+ },
17173
+ pinned: {
17174
+ type: "string",
17175
+ description: "'false' to exclude from context injection; default true"
17176
+ },
17177
+ scope: {
17178
+ type: "string",
17179
+ description: "'project' (default) or 'global' (shared across projects)"
17180
+ }
17181
+ },
17182
+ required: ["label"]
17183
+ }
17184
+ },
17185
+ {
17186
+ name: "memory_slot_append",
17187
+ 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.",
17188
+ inputSchema: {
17189
+ type: "object",
17190
+ properties: {
17191
+ label: {
17192
+ type: "string",
17193
+ description: "Slot label"
17194
+ },
17195
+ text: {
17196
+ type: "string",
17197
+ description: "Text to append"
17198
+ }
17199
+ },
17200
+ required: ["label", "text"]
17201
+ }
17202
+ },
17203
+ {
17204
+ name: "memory_slot_replace",
17205
+ description: "Replace slot content in place. Fails if content exceeds sizeLimit.",
17206
+ inputSchema: {
17207
+ type: "object",
17208
+ properties: {
17209
+ label: {
17210
+ type: "string",
17211
+ description: "Slot label"
17212
+ },
17213
+ content: {
17214
+ type: "string",
17215
+ description: "New full content"
17216
+ }
17217
+ },
17218
+ required: ["label", "content"]
17219
+ }
17220
+ },
17221
+ {
17222
+ name: "memory_slot_delete",
17223
+ description: "Delete a slot. Seeded default slots can be deleted unless marked readOnly.",
17224
+ inputSchema: {
17225
+ type: "object",
17226
+ properties: { label: {
17227
+ type: "string",
17228
+ description: "Slot label"
17229
+ } },
17230
+ required: ["label"]
17231
+ }
17232
+ }
17233
+ ];
15477
17234
  const ESSENTIAL_TOOLS = new Set([
15478
17235
  "memory_save",
15479
17236
  "memory_recall",
@@ -15492,7 +17249,8 @@ function getAllTools() {
15492
17249
  ...V051_TOOLS,
15493
17250
  ...V061_TOOLS,
15494
17251
  ...V070_TOOLS,
15495
- ...V073_TOOLS
17252
+ ...V073_TOOLS,
17253
+ ...V010_SLOTS_TOOLS
15496
17254
  ];
15497
17255
  }
15498
17256
  function getVisibleTools() {
@@ -15699,6 +17457,34 @@ function registerMcpEndpoints(sdk, kv, secret) {
15699
17457
  }] }
15700
17458
  };
15701
17459
  }
17460
+ case "memory_vision_search": {
17461
+ const queryText = typeof args.queryText === "string" ? args.queryText : void 0;
17462
+ const queryImageRef = typeof args.queryImageRef === "string" ? args.queryImageRef : void 0;
17463
+ const queryImageBase64 = typeof args.queryImageBase64 === "string" ? args.queryImageBase64 : void 0;
17464
+ if (!queryText && !queryImageRef && !queryImageBase64) return {
17465
+ status_code: 400,
17466
+ body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
17467
+ };
17468
+ const topK = Math.max(1, Math.min(50, asNumber(args.topK, 10) ?? 10));
17469
+ const sessionId = typeof args.sessionId === "string" ? args.sessionId : void 0;
17470
+ const result = await sdk.trigger({
17471
+ function_id: "mem::vision-search",
17472
+ payload: {
17473
+ queryText,
17474
+ queryImageRef,
17475
+ queryImageBase64,
17476
+ topK,
17477
+ sessionId
17478
+ }
17479
+ });
17480
+ return {
17481
+ status_code: 200,
17482
+ body: { content: [{
17483
+ type: "text",
17484
+ text: JSON.stringify(result, null, 2)
17485
+ }] }
17486
+ };
17487
+ }
15702
17488
  case "memory_timeline": {
15703
17489
  if (typeof args.anchor !== "string" || !args.anchor.trim()) return {
15704
17490
  status_code: 400,
@@ -16564,6 +18350,123 @@ function registerMcpEndpoints(sdk, kv, secret) {
16564
18350
  }] }
16565
18351
  };
16566
18352
  }
18353
+ case "memory_slot_list": {
18354
+ const result = await sdk.trigger({
18355
+ function_id: "mem::slot-list",
18356
+ payload: {}
18357
+ });
18358
+ return {
18359
+ status_code: 200,
18360
+ body: { content: [{
18361
+ type: "text",
18362
+ text: JSON.stringify(result, null, 2)
18363
+ }] }
18364
+ };
18365
+ }
18366
+ case "memory_slot_get": {
18367
+ const label = asNonEmptyString(args.label);
18368
+ if (!label) return {
18369
+ status_code: 400,
18370
+ body: { error: "label required" }
18371
+ };
18372
+ const result = await sdk.trigger({
18373
+ function_id: "mem::slot-get",
18374
+ payload: { label }
18375
+ });
18376
+ return {
18377
+ status_code: 200,
18378
+ body: { content: [{
18379
+ type: "text",
18380
+ text: JSON.stringify(result, null, 2)
18381
+ }] }
18382
+ };
18383
+ }
18384
+ case "memory_slot_create": {
18385
+ const label = asNonEmptyString(args.label);
18386
+ if (!label) return {
18387
+ status_code: 400,
18388
+ body: { error: "label required" }
18389
+ };
18390
+ const payload = { label };
18391
+ if (typeof args.content === "string") payload.content = args.content;
18392
+ if (typeof args.description === "string") payload.description = args.description;
18393
+ if (typeof args.sizeLimit === "number") payload.sizeLimit = args.sizeLimit;
18394
+ if (args.pinned === false || args.pinned === "false") payload.pinned = false;
18395
+ else if (args.pinned === true || args.pinned === "true") payload.pinned = true;
18396
+ if (args.scope === "global" || args.scope === "project") payload.scope = args.scope;
18397
+ const result = await sdk.trigger({
18398
+ function_id: "mem::slot-create",
18399
+ payload
18400
+ });
18401
+ return {
18402
+ status_code: 200,
18403
+ body: { content: [{
18404
+ type: "text",
18405
+ text: JSON.stringify(result, null, 2)
18406
+ }] }
18407
+ };
18408
+ }
18409
+ case "memory_slot_append": {
18410
+ const label = asNonEmptyString(args.label);
18411
+ const text = typeof args.text === "string" ? args.text : null;
18412
+ if (!label || !text) return {
18413
+ status_code: 400,
18414
+ body: { error: "label and text required" }
18415
+ };
18416
+ const result = await sdk.trigger({
18417
+ function_id: "mem::slot-append",
18418
+ payload: {
18419
+ label,
18420
+ text
18421
+ }
18422
+ });
18423
+ return {
18424
+ status_code: 200,
18425
+ body: { content: [{
18426
+ type: "text",
18427
+ text: JSON.stringify(result, null, 2)
18428
+ }] }
18429
+ };
18430
+ }
18431
+ case "memory_slot_replace": {
18432
+ const label = asNonEmptyString(args.label);
18433
+ if (!label || typeof args.content !== "string") return {
18434
+ status_code: 400,
18435
+ body: { error: "label and content (string) required" }
18436
+ };
18437
+ const result = await sdk.trigger({
18438
+ function_id: "mem::slot-replace",
18439
+ payload: {
18440
+ label,
18441
+ content: args.content
18442
+ }
18443
+ });
18444
+ return {
18445
+ status_code: 200,
18446
+ body: { content: [{
18447
+ type: "text",
18448
+ text: JSON.stringify(result, null, 2)
18449
+ }] }
18450
+ };
18451
+ }
18452
+ case "memory_slot_delete": {
18453
+ const label = asNonEmptyString(args.label);
18454
+ if (!label) return {
18455
+ status_code: 400,
18456
+ body: { error: "label required" }
18457
+ };
18458
+ const result = await sdk.trigger({
18459
+ function_id: "mem::slot-delete",
18460
+ payload: { label }
18461
+ });
18462
+ return {
18463
+ status_code: 200,
18464
+ body: { content: [{
18465
+ type: "text",
18466
+ text: JSON.stringify(result, null, 2)
18467
+ }] }
18468
+ };
18469
+ }
16567
18470
  default: return {
16568
18471
  status_code: 400,
16569
18472
  body: { error: `Unknown tool: ${name}` }
@@ -17218,11 +19121,13 @@ async function main() {
17218
19121
  const fallbackConfig = loadFallbackConfig();
17219
19122
  const provider = fallbackConfig.providers.length > 0 ? createFallbackProvider(config.provider, fallbackConfig) : createProvider(config.provider);
17220
19123
  const embeddingProvider = createEmbeddingProvider();
19124
+ const imageEmbeddingProvider = createImageEmbeddingProvider();
17221
19125
  console.log(`[agentmemory] Starting worker v${VERSION}...`);
17222
19126
  console.log(`[agentmemory] Engine: ${config.engineUrl}`);
17223
19127
  console.log(`[agentmemory] Provider: ${config.provider.provider} (${config.provider.model})`);
17224
19128
  if (embeddingProvider) console.log(`[agentmemory] Embedding provider: ${embeddingProvider.name} (${embeddingProvider.dimensions} dims)`);
17225
19129
  else console.log(`[agentmemory] Embedding provider: none (BM25-only mode)`);
19130
+ if (imageEmbeddingProvider) console.log(`[agentmemory] Image embedding provider: ${imageEmbeddingProvider.name} (${imageEmbeddingProvider.dimensions} dims) — vision-search active`);
17226
19131
  console.log(`[agentmemory] REST API: http://localhost:${config.restPort}/agentmemory/*`);
17227
19132
  console.log(`[agentmemory] Streams: ws://localhost:${config.streamsPort}`);
17228
19133
  const sdk = registerWorker(config.engineUrl, {
@@ -17241,6 +19146,10 @@ async function main() {
17241
19146
  initMetrics(hasGetMeter(sdk) ? sdk.getMeter.bind(sdk) : void 0);
17242
19147
  registerPrivacyFunction(sdk);
17243
19148
  registerObserveFunction(sdk, kv, dedupMap, config.maxObservationsPerSession);
19149
+ registerImageQuotaCleanup(sdk, kv);
19150
+ registerVisionSearchFunctions(sdk, kv, imageEmbeddingProvider);
19151
+ if (isSlotsEnabled()) registerSlotsFunctions(sdk, kv);
19152
+ registerDiskSizeManager(sdk, kv);
17244
19153
  registerCompressFunction(sdk, kv, provider, metricsStore);
17245
19154
  registerSearchFunction(sdk, kv);
17246
19155
  registerContextFunction(sdk, kv, config.tokenBudget);
@@ -17307,6 +19216,7 @@ async function main() {
17307
19216
  registerReplayFunctions(sdk, kv);
17308
19217
  console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
17309
19218
  console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
19219
+ if (isSlotsEnabled()) console.log(`[agentmemory] Slots: enabled (pinned editable memory). Reflect on Stop hook: ${isReflectEnabled() ? "on" : "off"}`);
17310
19220
  const snapshotConfig = loadSnapshotConfig();
17311
19221
  if (snapshotConfig.enabled) {
17312
19222
  registerSnapshotFunction(sdk, kv, snapshotConfig.dir);
@@ -17344,7 +19254,7 @@ async function main() {
17344
19254
  }
17345
19255
  }
17346
19256
  console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
17347
- console.log(`[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`);
19257
+ console.log(`[agentmemory] Endpoints: 107 REST + ${getAllTools().length} MCP tools + 6 MCP resources + 3 MCP prompts`);
17348
19258
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
17349
19259
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
17350
19260
  const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);