@agentmemory/agentmemory 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +40 -13
  2. package/dist/cli.mjs +147 -26
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/hooks/notification.mjs +6 -0
  5. package/dist/hooks/notification.mjs.map +1 -1
  6. package/dist/hooks/post-tool-failure.mjs +6 -0
  7. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  8. package/dist/hooks/post-tool-use.mjs +35 -1
  9. package/dist/hooks/post-tool-use.mjs.map +1 -1
  10. package/dist/hooks/pre-compact.mjs +6 -0
  11. package/dist/hooks/pre-compact.mjs.map +1 -1
  12. package/dist/hooks/pre-tool-use.mjs +6 -0
  13. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  14. package/dist/hooks/prompt-submit.mjs +6 -0
  15. package/dist/hooks/prompt-submit.mjs.map +1 -1
  16. package/dist/hooks/session-end.mjs +6 -0
  17. package/dist/hooks/session-end.mjs.map +1 -1
  18. package/dist/hooks/session-start.mjs +6 -0
  19. package/dist/hooks/session-start.mjs.map +1 -1
  20. package/dist/hooks/stop.mjs +6 -0
  21. package/dist/hooks/stop.mjs.map +1 -1
  22. package/dist/hooks/subagent-start.mjs +6 -0
  23. package/dist/hooks/subagent-start.mjs.map +1 -1
  24. package/dist/hooks/subagent-stop.mjs +6 -0
  25. package/dist/hooks/subagent-stop.mjs.map +1 -1
  26. package/dist/hooks/task-completed.mjs +6 -0
  27. package/dist/hooks/task-completed.mjs.map +1 -1
  28. package/dist/image-refs-CESf9ndJ.mjs +3 -0
  29. package/dist/image-store-DGvZMMrI.mjs +3 -0
  30. package/dist/index.mjs +2100 -157
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/{src-Dw_gJcCy.mjs → src-3Snd7D3T.mjs} +2021 -267
  33. package/dist/src-3Snd7D3T.mjs.map +1 -0
  34. package/dist/{standalone-BEWvWM5P.mjs → standalone-BG9uPsDK.mjs} +2 -2
  35. package/dist/{standalone-BEWvWM5P.mjs.map → standalone-BG9uPsDK.mjs.map} +1 -1
  36. package/dist/standalone.mjs +136 -2
  37. package/dist/standalone.mjs.map +1 -1
  38. package/dist/{tools-registry-BvWNlj6u.mjs → tools-registry-m8Ofn9vV.mjs} +166 -12
  39. package/dist/tools-registry-m8Ofn9vV.mjs.map +1 -0
  40. package/dist/viewer/index.html +528 -68
  41. package/package.json +5 -3
  42. package/plugin/.claude-plugin/plugin.json +2 -2
  43. package/plugin/scripts/notification.mjs +6 -0
  44. package/plugin/scripts/notification.mjs.map +1 -1
  45. package/plugin/scripts/post-tool-failure.mjs +6 -0
  46. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  47. package/plugin/scripts/post-tool-use.mjs +35 -1
  48. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  49. package/plugin/scripts/pre-compact.mjs +6 -0
  50. package/plugin/scripts/pre-compact.mjs.map +1 -1
  51. package/plugin/scripts/pre-tool-use.mjs +6 -0
  52. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  53. package/plugin/scripts/prompt-submit.mjs +6 -0
  54. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  55. package/plugin/scripts/session-end.mjs +6 -0
  56. package/plugin/scripts/session-end.mjs.map +1 -1
  57. package/plugin/scripts/session-start.mjs +6 -0
  58. package/plugin/scripts/session-start.mjs.map +1 -1
  59. package/plugin/scripts/stop.mjs +6 -0
  60. package/plugin/scripts/stop.mjs.map +1 -1
  61. package/plugin/scripts/subagent-start.mjs +6 -0
  62. package/plugin/scripts/subagent-start.mjs.map +1 -1
  63. package/plugin/scripts/subagent-stop.mjs +6 -0
  64. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  65. package/plugin/scripts/task-completed.mjs +6 -0
  66. package/plugin/scripts/task-completed.mjs.map +1 -1
  67. package/dist/src-Dw_gJcCy.mjs.map +0 -1
  68. package/dist/tools-registry-BvWNlj6u.mjs.map +0 -1
  69. package/dist/transformers-BX_tgxdO.mjs +0 -38684
  70. package/dist/transformers-BX_tgxdO.mjs.map +0 -1
  71. package/dist/transformers-KMm1i9no.mjs +0 -38683
  72. package/dist/transformers-KMm1i9no.mjs.map +0 -1
@@ -1,20 +1,127 @@
1
1
  import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./cli.mjs";
2
- import { a as getEnvVar, c as isConsolidationEnabled, d as loadClaudeBridgeConfig, f as loadConfig, g as loadTeamConfig, h as loadSnapshotConfig, i as getConsolidationDecayDays, l as isContextInjectionEnabled, m as loadFallbackConfig, n as VERSION, p as loadEmbeddingConfig, r as detectEmbeddingProvider, s as isAutoCompressEnabled, t as getVisibleTools, u as isGraphExtractionEnabled } from "./tools-registry-BvWNlj6u.mjs";
2
+ import { _ as loadTeamConfig, a as getConsolidationDecayDays, c as isAutoCompressEnabled, d as isGraphExtractionEnabled, f as loadClaudeBridgeConfig, g as loadSnapshotConfig, h as loadFallbackConfig, i as detectEmbeddingProvider, l as isConsolidationEnabled, m as loadEmbeddingConfig, n as getVisibleTools, o as getEnvVar, p as loadConfig, r as VERSION, t as getAllTools, u as isContextInjectionEnabled } from "./tools-registry-m8Ofn9vV.mjs";
3
3
  import { execFile } from "node:child_process";
4
4
  import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { homedir } from "node:os";
8
8
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
9
+ import { lstat, mkdir, open, readFile, readdir, stat, unlink, utimes, writeFile } from "node:fs/promises";
9
10
  import { TriggerAction, registerWorker } from "iii-sdk";
10
11
  import Anthropic from "@anthropic-ai/sdk";
11
12
  import { z } from "zod";
12
13
  import { promisify } from "node:util";
13
14
  import { lookup } from "node:dns/promises";
14
15
  import { isIP } from "node:net";
15
- import { lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
16
16
  import { createServer } from "node:http";
17
17
 
18
+ //#region src/utils/image-store.ts
19
+ const IMAGES_DIR = join(homedir(), ".agentmemory", "images");
20
+ const DEFAULT_MAX_BYTES = 500 * 1024 * 1024;
21
+ function getMaxBytes() {
22
+ return Number(process.env.AGENTMEMORY_IMAGE_STORE_MAX_BYTES) || DEFAULT_MAX_BYTES;
23
+ }
24
+ function isManagedImagePath(filePath) {
25
+ const resolved = resolve(filePath);
26
+ const normalizedImagesDir = resolve(IMAGES_DIR);
27
+ return resolved.startsWith(normalizedImagesDir + sep) || resolved === normalizedImagesDir;
28
+ }
29
+ function contentHash(data) {
30
+ return createHash("sha256").update(data).digest("hex");
31
+ }
32
+ async function saveImageToDisk(base64Data) {
33
+ if (!base64Data) return {
34
+ filePath: "",
35
+ bytesWritten: 0
36
+ };
37
+ if (!existsSync(IMAGES_DIR)) await mkdir(IMAGES_DIR, { recursive: true });
38
+ let cleanBase64 = base64Data;
39
+ let ext = "png";
40
+ if (base64Data.startsWith("data:image/")) {
41
+ const commaIdx = base64Data.indexOf(",");
42
+ if (commaIdx !== -1) {
43
+ const meta = base64Data.substring(0, commaIdx);
44
+ if (meta.includes("jpeg") || meta.includes("jpg")) ext = "jpg";
45
+ else if (meta.includes("webp")) ext = "webp";
46
+ else if (meta.includes("gif")) ext = "gif";
47
+ cleanBase64 = base64Data.substring(commaIdx + 1);
48
+ }
49
+ } else if (base64Data.startsWith("/9j/")) ext = "jpg";
50
+ const filePath = join(IMAGES_DIR, `${contentHash(cleanBase64)}.${ext}`);
51
+ if (existsSync(filePath)) return {
52
+ filePath,
53
+ bytesWritten: 0
54
+ };
55
+ await writeFile(filePath, Buffer.from(cleanBase64, "base64"));
56
+ return {
57
+ filePath,
58
+ bytesWritten: (await stat(filePath)).size
59
+ };
60
+ }
61
+ async function deleteImage(filePath) {
62
+ if (!filePath) return { deletedBytes: 0 };
63
+ if (!isManagedImagePath(filePath)) return { deletedBytes: 0 };
64
+ try {
65
+ if (existsSync(filePath)) {
66
+ const size = (await stat(filePath)).size;
67
+ await unlink(filePath);
68
+ return { deletedBytes: size };
69
+ }
70
+ } catch (err) {
71
+ console.error("[agentmemory] Failed to delete image context:", err);
72
+ }
73
+ return { deletedBytes: 0 };
74
+ }
75
+ /** Touch an image file to update its mtime (marking it as recently used for LRU eviction) */
76
+ async function touchImage(filePath) {
77
+ if (!filePath || !isManagedImagePath(filePath)) return;
78
+ try {
79
+ if (existsSync(filePath)) {
80
+ const now = /* @__PURE__ */ new Date();
81
+ await utimes(filePath, now, now);
82
+ }
83
+ } catch (err) {}
84
+ }
85
+
86
+ //#endregion
87
+ //#region src/state/keyed-mutex.ts
88
+ const locks = /* @__PURE__ */ new Map();
89
+ function withKeyedLock(key, fn) {
90
+ const next = (locks.get(key) ?? Promise.resolve()).then(fn, fn);
91
+ const cleanup = next.then(() => {}, () => {});
92
+ locks.set(key, cleanup);
93
+ cleanup.then(() => {
94
+ if (locks.get(key) === cleanup) locks.delete(key);
95
+ });
96
+ return next;
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/functions/image-refs.ts
101
+ async function getImageRefCount(kv, filePath) {
102
+ const count = await kv.get(KV.imageRefs, filePath);
103
+ return count ? Number(count) : 0;
104
+ }
105
+ async function incrementImageRef(kv, filePath) {
106
+ return withKeyedLock(`imgRef:${filePath}`, async () => {
107
+ const current = await getImageRefCount(kv, filePath);
108
+ await kv.set(KV.imageRefs, filePath, current + 1);
109
+ await touchImage(filePath);
110
+ });
111
+ }
112
+ async function decrementImageRef(kv, sdk, filePath) {
113
+ return withKeyedLock(`imgRef:${filePath}`, async () => {
114
+ const current = await getImageRefCount(kv, filePath);
115
+ if (current <= 1) {
116
+ await kv.delete(KV.imageEmbeddings, filePath);
117
+ await kv.delete(KV.imageRefs, filePath);
118
+ const { deletedBytes } = await deleteImage(filePath);
119
+ if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
120
+ } else await kv.set(KV.imageRefs, filePath, current - 1);
121
+ });
122
+ }
123
+
124
+ //#endregion
18
125
  //#region src/providers/agent-sdk.ts
19
126
  var AgentSDKProvider = class {
20
127
  name = "agent-sdk";
@@ -25,18 +132,26 @@ var AgentSDKProvider = class {
25
132
  return this.query(systemPrompt, userPrompt);
26
133
  }
27
134
  async query(systemPrompt, userPrompt) {
28
- const { query } = await import("@anthropic-ai/claude-agent-sdk");
29
- const messages = query({
30
- prompt: userPrompt,
31
- options: {
32
- systemPrompt,
33
- maxTurns: 1,
34
- allowedTools: []
35
- }
36
- });
37
- let result = "";
38
- for await (const msg of messages) if (msg.type === "result") result = typeof msg.result === "string" ? msg.result : JSON.stringify(msg.result);
39
- return result;
135
+ if (process.env.AGENTMEMORY_SDK_CHILD === "1") return "";
136
+ const prev = process.env.AGENTMEMORY_SDK_CHILD;
137
+ process.env.AGENTMEMORY_SDK_CHILD = "1";
138
+ try {
139
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
140
+ const messages = query({
141
+ prompt: userPrompt,
142
+ options: {
143
+ systemPrompt,
144
+ maxTurns: 1,
145
+ allowedTools: []
146
+ }
147
+ });
148
+ let result = "";
149
+ for await (const msg of messages) if (msg.type === "result") result = msg.result ?? "";
150
+ return result;
151
+ } finally {
152
+ if (prev === void 0) delete process.env.AGENTMEMORY_SDK_CHILD;
153
+ else process.env.AGENTMEMORY_SDK_CHILD = prev;
154
+ }
40
155
  }
41
156
  };
42
157
 
@@ -61,6 +176,26 @@ var AnthropicProvider = class {
61
176
  async summarize(systemPrompt, userPrompt) {
62
177
  return this.call(systemPrompt, userPrompt);
63
178
  }
179
+ async describeImage(imageData, mimeType, prompt) {
180
+ return (await this.client.messages.create({
181
+ model: this.model,
182
+ max_tokens: this.maxTokens,
183
+ messages: [{
184
+ role: "user",
185
+ content: [{
186
+ type: "image",
187
+ source: {
188
+ type: "base64",
189
+ media_type: mimeType,
190
+ data: imageData
191
+ }
192
+ }, {
193
+ type: "text",
194
+ text: prompt
195
+ }]
196
+ }]
197
+ })).content.find((b) => b.type === "text")?.text ?? "";
198
+ }
64
199
  async call(systemPrompt, userPrompt) {
65
200
  return (await this.client.messages.create({
66
201
  model: this.model,
@@ -135,6 +270,25 @@ var MinimaxProvider = class {
135
270
  }
136
271
  };
137
272
 
273
+ //#endregion
274
+ //#region src/providers/noop.ts
275
+ /**
276
+ * Returns empty strings for every call. Used when no LLM API key is set
277
+ * AND the user has not opted into the agent-sdk fallback via
278
+ * AGENTMEMORY_ALLOW_AGENT_SDK=true. Callers (compress, summarize) must
279
+ * detect the empty result and short-circuit instead of spawning a
280
+ * provider session (#149 / Stop-hook recursion loop fix).
281
+ */
282
+ var NoopProvider = class {
283
+ name = "noop";
284
+ async compress() {
285
+ return "";
286
+ }
287
+ async summarize() {
288
+ return "";
289
+ }
290
+ };
291
+
138
292
  //#endregion
139
293
  //#region src/providers/openrouter.ts
140
294
  var OpenRouterProvider = class {
@@ -346,28 +500,67 @@ var GeminiEmbeddingProvider = class {
346
500
 
347
501
  //#endregion
348
502
  //#region src/providers/embedding/openai.ts
349
- const API_URL$3 = "https://api.openai.com/v1/embeddings";
503
+ const DEFAULT_BASE_URL = "https://api.openai.com";
504
+ const DEFAULT_MODEL$1 = "text-embedding-3-small";
505
+ /**
506
+ * Known OpenAI embedding model dimensions. Extend as new models ship.
507
+ * Override in any case via OPENAI_EMBEDDING_DIMENSIONS for custom or
508
+ * self-hosted OpenAI-compatible endpoints returning non-standard sizes.
509
+ */
510
+ const MODEL_DIMENSIONS = {
511
+ "text-embedding-3-small": 1536,
512
+ "text-embedding-3-large": 3072,
513
+ "text-embedding-ada-002": 1536
514
+ };
515
+ const DEFAULT_DIMENSIONS = MODEL_DIMENSIONS[DEFAULT_MODEL$1] ?? 1536;
516
+ function resolveDimensions(model, override) {
517
+ if (override !== void 0 && override.trim().length > 0) {
518
+ const parsed = parseInt(override, 10);
519
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`OPENAI_EMBEDDING_DIMENSIONS must be a positive integer, got: ${override}`);
520
+ return parsed;
521
+ }
522
+ return MODEL_DIMENSIONS[model] ?? DEFAULT_DIMENSIONS;
523
+ }
524
+ /**
525
+ * OpenAI-compatible embedding provider.
526
+ *
527
+ * Required env vars:
528
+ * OPENAI_API_KEY — API key
529
+ *
530
+ * Optional:
531
+ * OPENAI_BASE_URL — base URL without path (default: https://api.openai.com)
532
+ * OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
533
+ * OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
534
+ * custom / self-hosted models not in the
535
+ * MODEL_DIMENSIONS table above)
536
+ */
350
537
  var OpenAIEmbeddingProvider = class {
351
538
  name = "openai";
352
- dimensions = 1536;
539
+ dimensions;
353
540
  apiKey;
541
+ baseUrl;
542
+ model;
354
543
  constructor(apiKey) {
355
544
  this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
356
545
  if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
546
+ this.baseUrl = getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL;
547
+ this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
548
+ this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
357
549
  }
358
550
  async embed(text) {
359
551
  const [result] = await this.embedBatch([text]);
360
552
  return result;
361
553
  }
362
554
  async embedBatch(texts) {
363
- const response = await fetch(API_URL$3, {
555
+ const url = `${this.baseUrl}/v1/embeddings`;
556
+ const response = await fetch(url, {
364
557
  method: "POST",
365
558
  headers: {
366
559
  Authorization: `Bearer ${this.apiKey}`,
367
560
  "Content-Type": "application/json"
368
561
  },
369
562
  body: JSON.stringify({
370
- model: "text-embedding-3-small",
563
+ model: this.model,
371
564
  input: texts
372
565
  })
373
566
  });
@@ -508,7 +701,7 @@ var LocalEmbeddingProvider = class {
508
701
  if (this.extractor) return this.extractor;
509
702
  let transformers;
510
703
  try {
511
- transformers = await import("./transformers-KMm1i9no.mjs");
704
+ transformers = await import("@xenova/transformers");
512
705
  } catch {
513
706
  throw new Error("Install @xenova/transformers for local embeddings: npm install @xenova/transformers");
514
707
  }
@@ -517,8 +710,86 @@ var LocalEmbeddingProvider = class {
517
710
  }
518
711
  };
519
712
 
713
+ //#endregion
714
+ //#region src/providers/embedding/clip.ts
715
+ const DEFAULT_MODEL = "Xenova/clip-vit-base-patch32";
716
+ const DIMENSIONS = 512;
717
+ var ClipEmbeddingProvider = class {
718
+ name = "clip";
719
+ dimensions = DIMENSIONS;
720
+ textExtractor = null;
721
+ imageExtractor = null;
722
+ transformers = null;
723
+ modelId;
724
+ constructor(modelId = DEFAULT_MODEL) {
725
+ this.modelId = modelId;
726
+ }
727
+ async embed(text) {
728
+ const [vec] = await this.embedBatch([text]);
729
+ return vec;
730
+ }
731
+ async embedBatch(texts) {
732
+ return (await (await this.getTextExtractor())(texts, {
733
+ pooling: "mean",
734
+ normalize: true
735
+ })).tolist().map((v) => new Float32Array(v));
736
+ }
737
+ async embedImage(src) {
738
+ const image = await loadImage(await this.getTransformers(), src);
739
+ const output = await (await this.getImageExtractor())(image);
740
+ return normalize(output.data ?? new Float32Array(output.tolist()[0] || []));
741
+ }
742
+ async getTransformers() {
743
+ if (this.transformers) return this.transformers;
744
+ try {
745
+ this.transformers = await import("@xenova/transformers");
746
+ } catch {
747
+ throw new Error("Install @xenova/transformers for CLIP image embeddings: npm install @xenova/transformers");
748
+ }
749
+ return this.transformers;
750
+ }
751
+ async getTextExtractor() {
752
+ if (this.textExtractor) return this.textExtractor;
753
+ this.textExtractor = await (await this.getTransformers()).pipeline("feature-extraction", this.modelId);
754
+ return this.textExtractor;
755
+ }
756
+ async getImageExtractor() {
757
+ if (this.imageExtractor) return this.imageExtractor;
758
+ this.imageExtractor = await (await this.getTransformers()).pipeline("image-feature-extraction", this.modelId);
759
+ return this.imageExtractor;
760
+ }
761
+ };
762
+ async function loadImage(t, src) {
763
+ if (src.startsWith("data:")) {
764
+ const comma = src.indexOf(",");
765
+ const b64 = comma >= 0 ? src.slice(comma + 1) : src;
766
+ const buf = Buffer.from(b64, "base64");
767
+ const blob = new Blob([buf]);
768
+ return t.RawImage.fromBlob(blob);
769
+ }
770
+ const data = await readFile(src);
771
+ const blob = new Blob([data]);
772
+ return t.RawImage.fromBlob(blob);
773
+ }
774
+ function normalize(vec) {
775
+ let sum = 0;
776
+ for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i];
777
+ const norm = Math.sqrt(sum);
778
+ if (norm === 0) return vec;
779
+ const out = new Float32Array(vec.length);
780
+ for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;
781
+ return out;
782
+ }
783
+
520
784
  //#endregion
521
785
  //#region src/providers/embedding/index.ts
786
+ let imageEmbeddingProvider = null;
787
+ function createImageEmbeddingProvider() {
788
+ if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] !== "true") return null;
789
+ if (imageEmbeddingProvider) return imageEmbeddingProvider;
790
+ imageEmbeddingProvider = new ClipEmbeddingProvider();
791
+ return imageEmbeddingProvider;
792
+ }
522
793
  function createEmbeddingProvider() {
523
794
  const detected = detectEmbeddingProvider();
524
795
  if (!detected) return null;
@@ -570,6 +841,7 @@ function createBaseProvider(config) {
570
841
  return new OpenRouterProvider(geminiKey, config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
571
842
  }
572
843
  case "openrouter": return new OpenRouterProvider(requireEnvVar("OPENROUTER_API_KEY"), config.model, config.maxTokens, "https://openrouter.ai/api/v1/chat/completions");
844
+ case "noop": return new NoopProvider();
573
845
  default: return new AgentSDKProvider();
574
846
  }
575
847
  }
@@ -1105,7 +1377,7 @@ async function loadPipeline() {
1105
1377
  if (pipelineLoading) return pipelineLoading;
1106
1378
  pipelineLoading = (async () => {
1107
1379
  try {
1108
- const { pipeline: createPipeline } = await import("./transformers-KMm1i9no.mjs");
1380
+ const { pipeline: createPipeline } = await import("@xenova/transformers");
1109
1381
  pipeline = await createPipeline("text-classification", "Xenova/ms-marco-MiniLM-L-6-v2", { quantized: true });
1110
1382
  return pipeline;
1111
1383
  } catch {
@@ -1822,19 +2094,6 @@ function registerPrivacyFunction(sdk) {
1822
2094
  });
1823
2095
  }
1824
2096
 
1825
- //#endregion
1826
- //#region src/state/keyed-mutex.ts
1827
- const locks = /* @__PURE__ */ new Map();
1828
- function withKeyedLock(key, fn) {
1829
- const next = (locks.get(key) ?? Promise.resolve()).then(fn, fn);
1830
- const cleanup = next.then(() => {}, () => {});
1831
- locks.set(key, cleanup);
1832
- cleanup.then(() => {
1833
- if (locks.get(key) === cleanup) locks.delete(key);
1834
- });
1835
- return next;
1836
- }
1837
-
1838
2097
  //#endregion
1839
2098
  //#region src/functions/compress-synthetic.ts
1840
2099
  function inferType(toolName, hookType) {
@@ -1911,7 +2170,7 @@ function buildSyntheticCompression(raw) {
1911
2170
  inputStr,
1912
2171
  outputStr
1913
2172
  ].filter((s) => s.length > 0);
1914
- return {
2173
+ const result = {
1915
2174
  id: raw.id,
1916
2175
  sessionId: raw.sessionId,
1917
2176
  timestamp: raw.timestamp,
@@ -1925,6 +2184,9 @@ function buildSyntheticCompression(raw) {
1925
2184
  importance: 5,
1926
2185
  confidence: .3
1927
2186
  };
2187
+ if (raw.modality) result.modality = raw.modality;
2188
+ if (raw.imageData) result.imageData = raw.imageData;
2189
+ return result;
1928
2190
  }
1929
2191
 
1930
2192
  //#endregion
@@ -2172,6 +2434,24 @@ function registerSearchFunction(sdk, kv) {
2172
2434
 
2173
2435
  //#endregion
2174
2436
  //#region src/functions/observe.ts
2437
+ function extractImage(d) {
2438
+ if (!d) return void 0;
2439
+ if (typeof d === "string") {
2440
+ if (d.startsWith("data:image/") || d.startsWith("iVBORw0KGgo") || d.startsWith("/9j/")) return d;
2441
+ return;
2442
+ }
2443
+ if (typeof d === "object" && d !== null) {
2444
+ const obj = d;
2445
+ if (typeof obj["image_data"] === "string") return obj["image_data"];
2446
+ if (typeof obj["image_path"] === "string") return obj["image_path"];
2447
+ if (typeof obj["imageBase64"] === "string") return obj["imageBase64"];
2448
+ if (typeof obj["imagePath"] === "string") return obj["imagePath"];
2449
+ for (const key of Object.keys(obj)) {
2450
+ const match = extractImage(obj[key]);
2451
+ if (match) return match;
2452
+ }
2453
+ }
2454
+ }
2175
2455
  function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2176
2456
  sdk.registerFunction("mem::observe", async (payload) => {
2177
2457
  if (!payload?.sessionId || typeof payload.sessionId !== "string" || !payload.hookType || typeof payload.hookType !== "string" || !payload.timestamp || typeof payload.timestamp !== "string") return {
@@ -2203,6 +2483,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2203
2483
  hookType: payload.hookType,
2204
2484
  raw: sanitizedRaw
2205
2485
  };
2486
+ let extractedImage;
2206
2487
  if (typeof sanitizedRaw === "object" && sanitizedRaw !== null) {
2207
2488
  const d = sanitizedRaw;
2208
2489
  if (payload.hookType === "post_tool_use" || payload.hookType === "post_tool_failure") {
@@ -2211,7 +2492,13 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2211
2492
  raw.toolOutput = d["tool_output"] || d["error"];
2212
2493
  }
2213
2494
  if (payload.hookType === "prompt_submit") raw.userPrompt = d["prompt"];
2495
+ extractedImage = extractImage(sanitizedRaw);
2496
+ if (extractedImage) raw.modality = raw.toolInput || raw.toolOutput || raw.userPrompt ? "mixed" : "image";
2497
+ } else if (typeof sanitizedRaw === "string") {
2498
+ extractedImage = extractImage(sanitizedRaw);
2499
+ if (extractedImage) raw.modality = "image";
2214
2500
  }
2501
+ const pendingImageData = extractedImage;
2215
2502
  return withKeyedLock(`obs:${payload.sessionId}`, async () => {
2216
2503
  if (maxObservationsPerSession && maxObservationsPerSession > 0) {
2217
2504
  if ((await kv.list(KV.observations(payload.sessionId))).length >= maxObservationsPerSession) return {
@@ -2219,7 +2506,29 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2219
2506
  error: `Session observation limit reached (${maxObservationsPerSession})`
2220
2507
  };
2221
2508
  }
2222
- await kv.set(KV.observations(payload.sessionId), obsId, raw);
2509
+ if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
2510
+ const { saveImageToDisk } = await import("./image-store-DGvZMMrI.mjs");
2511
+ const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
2512
+ raw.imageData = filePath;
2513
+ const { incrementImageRef } = await import("./image-refs-CESf9ndJ.mjs");
2514
+ await incrementImageRef(kv, filePath);
2515
+ sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: bytesWritten });
2516
+ if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.triggerVoid("mem::vision-embed", {
2517
+ imageRef: filePath,
2518
+ sessionId: payload.sessionId,
2519
+ observationId: obsId
2520
+ });
2521
+ }
2522
+ try {
2523
+ await kv.set(KV.observations(payload.sessionId), obsId, raw);
2524
+ } catch (error) {
2525
+ if (raw.imageData) {
2526
+ const { deleteImage } = await import("./image-store-DGvZMMrI.mjs");
2527
+ const { deletedBytes } = await deleteImage(raw.imageData);
2528
+ if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
2529
+ }
2530
+ throw error;
2531
+ }
2223
2532
  if (dedupMap && dedupHash) dedupMap.record(dedupHash);
2224
2533
  await sdk.trigger({
2225
2534
  function_id: "stream::set",
@@ -2249,15 +2558,26 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2249
2558
  action: TriggerAction.Void()
2250
2559
  });
2251
2560
  const session = await kv.get(KV.sessions, payload.sessionId);
2252
- if (session) await kv.update(KV.sessions, payload.sessionId, [{
2253
- type: "set",
2254
- path: "updatedAt",
2255
- value: (/* @__PURE__ */ new Date()).toISOString()
2256
- }, {
2257
- type: "set",
2258
- path: "observationCount",
2259
- value: (session.observationCount || 0) + 1
2260
- }]);
2561
+ if (session) {
2562
+ const updates = [{
2563
+ type: "set",
2564
+ path: "updatedAt",
2565
+ value: (/* @__PURE__ */ new Date()).toISOString()
2566
+ }, {
2567
+ type: "set",
2568
+ path: "observationCount",
2569
+ value: (session.observationCount || 0) + 1
2570
+ }];
2571
+ if (!session.firstPrompt && typeof raw.userPrompt === "string") {
2572
+ const trimmed = raw.userPrompt.replace(/\s+/g, " ").trim();
2573
+ if (trimmed.length > 0) updates.push({
2574
+ type: "set",
2575
+ path: "firstPrompt",
2576
+ value: trimmed.slice(0, 200)
2577
+ });
2578
+ }
2579
+ await kv.update(KV.sessions, payload.sessionId, updates);
2580
+ }
2261
2581
  if (isAutoCompressEnabled()) await sdk.trigger({
2262
2582
  function_id: "mem::compress",
2263
2583
  payload: {
@@ -2309,64 +2629,777 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2309
2629
  }
2310
2630
 
2311
2631
  //#endregion
2312
- //#region src/prompts/compression.ts
2313
- 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.
2314
-
2315
- Output EXACTLY this XML format with no additional text:
2316
-
2317
- <observation>
2318
- <type>one of: file_read, file_write, file_edit, command_run, search, web_fetch, conversation, error, decision, discovery, subagent, notification, task, other</type>
2319
- <title>Short descriptive title (max 80 chars)</title>
2320
- <subtitle>One-line context (optional)</subtitle>
2321
- <facts>
2322
- <fact>Specific factual detail 1</fact>
2323
- <fact>Specific factual detail 2</fact>
2324
- </facts>
2325
- <narrative>2-3 sentence summary of what happened and why it matters</narrative>
2326
- <concepts>
2327
- <concept>technical concept or pattern</concept>
2328
- </concepts>
2329
- <files>
2330
- <file>path/to/file</file>
2331
- </files>
2332
- <importance>1-10 scale, 10 being critical architectural decision</importance>
2333
- </observation>
2632
+ //#region src/functions/image-quota-cleanup.ts
2633
+ const GRACE_PERIOD_MS = 3e4;
2634
+ function registerImageQuotaCleanup(sdk, kv) {
2635
+ sdk.registerFunction("mem::image-quota-cleanup", async () => {
2636
+ const now = Date.now();
2637
+ return withKeyedLock("system:cleanupLock", async () => {
2638
+ let totalSize = 0;
2639
+ const fileStats = [];
2640
+ try {
2641
+ const files = await readdir(IMAGES_DIR);
2642
+ for (const file of files) {
2643
+ if (file.startsWith(".")) continue;
2644
+ const filePath = join(IMAGES_DIR, file);
2645
+ const s = await stat(filePath);
2646
+ if (s.isFile()) {
2647
+ fileStats.push({
2648
+ filePath,
2649
+ size: s.size,
2650
+ mtimeMs: s.mtimeMs
2651
+ });
2652
+ totalSize += s.size;
2653
+ }
2654
+ }
2655
+ } catch {
2656
+ return {
2657
+ success: true,
2658
+ evicted: 0,
2659
+ freedBytes: 0
2660
+ };
2661
+ }
2662
+ const limit = getMaxBytes();
2663
+ if (totalSize <= limit) return {
2664
+ success: true,
2665
+ evicted: 0,
2666
+ freedBytes: 0,
2667
+ underQuota: true
2668
+ };
2669
+ fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
2670
+ let totalToFree = totalSize - limit;
2671
+ let evicted = 0;
2672
+ let freedBytes = 0;
2673
+ for (const f of fileStats) {
2674
+ if (totalToFree <= 0) break;
2675
+ if (now - f.mtimeMs < GRACE_PERIOD_MS) continue;
2676
+ await withKeyedLock(`imgRef:${f.filePath}`, async () => {
2677
+ let refCount;
2678
+ try {
2679
+ refCount = await getImageRefCount(kv, f.filePath);
2680
+ } catch (err) {
2681
+ logger.error("Failed to read refCount; skipping eviction", {
2682
+ filePath: f.filePath,
2683
+ error: err instanceof Error ? err.message : String(err)
2684
+ });
2685
+ return;
2686
+ }
2687
+ if (refCount > 0) return;
2688
+ const { deletedBytes } = await deleteImage(f.filePath);
2689
+ if (deletedBytes > 0) {
2690
+ sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
2691
+ totalToFree -= deletedBytes;
2692
+ freedBytes += deletedBytes;
2693
+ evicted++;
2694
+ }
2695
+ });
2696
+ }
2697
+ if (evicted > 0) {
2698
+ const freedMb = (freedBytes / (1024 * 1024)).toFixed(1);
2699
+ logger.info("Image quota cleanup complete", {
2700
+ evicted,
2701
+ freedMb
2702
+ });
2703
+ }
2704
+ return {
2705
+ success: true,
2706
+ evicted,
2707
+ freedBytes
2708
+ };
2709
+ });
2710
+ });
2711
+ }
2334
2712
 
2335
- Rules:
2336
- - Be concise but preserve ALL technically relevant details
2337
- - File paths must be exact
2338
- - Importance: 1-3 for routine reads, 4-6 for edits/commands, 7-9 for architectural decisions, 10 for breaking changes
2339
- - Concepts should be reusable search terms (e.g., "React hooks", "SQL migration", "auth middleware")
2340
- - Strip any secrets, tokens, or credentials from the output`;
2341
- function buildCompressionPrompt(observation) {
2342
- const parts = [`Timestamp: ${observation.timestamp}`, `Hook: ${observation.hookType}`];
2343
- if (observation.toolName) parts.push(`Tool: ${observation.toolName}`);
2344
- if (observation.toolInput) {
2345
- const input = typeof observation.toolInput === "string" ? observation.toolInput : JSON.stringify(observation.toolInput, null, 2);
2346
- parts.push(`Input:\n${truncate$1(input, 4e3)}`);
2347
- }
2348
- if (observation.toolOutput) {
2349
- const output = typeof observation.toolOutput === "string" ? observation.toolOutput : JSON.stringify(observation.toolOutput, null, 2);
2350
- parts.push(`Output:\n${truncate$1(output, 4e3)}`);
2713
+ //#endregion
2714
+ //#region src/functions/audit.ts
2715
+ async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
2716
+ const entry = {
2717
+ id: generateId("aud"),
2718
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2719
+ operation,
2720
+ userId,
2721
+ functionId,
2722
+ targetIds,
2723
+ details,
2724
+ qualityScore
2725
+ };
2726
+ await kv.set(KV.audit, entry.id, entry);
2727
+ return entry;
2728
+ }
2729
+ async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
2730
+ try {
2731
+ await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
2732
+ } catch (err) {
2733
+ try {
2734
+ logger.warn("audit write failed", {
2735
+ functionId,
2736
+ operation,
2737
+ targetIds,
2738
+ error: err instanceof Error ? err.message : String(err)
2739
+ });
2740
+ } catch {}
2351
2741
  }
2352
- if (observation.userPrompt) parts.push(`User prompt:\n${truncate$1(observation.userPrompt, 2e3)}`);
2353
- return parts.join("\n\n");
2354
2742
  }
2355
- function truncate$1(s, max) {
2356
- return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
2743
+ async function queryAudit(kv, filter) {
2744
+ let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
2745
+ if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
2746
+ if (filter?.dateFrom) {
2747
+ const from = new Date(filter.dateFrom).getTime();
2748
+ if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
2749
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
2750
+ }
2751
+ if (filter?.dateTo) {
2752
+ const to = new Date(filter.dateTo).getTime();
2753
+ if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
2754
+ entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
2755
+ }
2756
+ return entries.slice(0, filter?.limit || 100);
2357
2757
  }
2358
2758
 
2359
2759
  //#endregion
2360
- //#region src/prompts/xml.ts
2361
- const VALID_TAG = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
2362
- function getXmlTag(xml, tag) {
2363
- if (!VALID_TAG.test(tag)) return "";
2364
- const match = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
2365
- return match ? match[1].trim() : "";
2366
- }
2367
- function getXmlChildren(xml, parentTag, childTag) {
2368
- if (!VALID_TAG.test(parentTag) || !VALID_TAG.test(childTag)) return [];
2369
- const parentMatch = xml.match(new RegExp(`<${parentTag}>([\\s\\S]*?)</${parentTag}>`));
2760
+ //#region src/functions/vision-search.ts
2761
+ function registerVisionSearchFunctions(sdk, kv, imageProvider) {
2762
+ sdk.registerFunction("mem::vision-embed", async (data) => {
2763
+ if (!imageProvider?.embedImage) return {
2764
+ success: false,
2765
+ error: "image embeddings disabled (set AGENTMEMORY_IMAGE_EMBEDDINGS=true)"
2766
+ };
2767
+ if (!data?.imageRef || typeof data.imageRef !== "string") return {
2768
+ success: false,
2769
+ error: "imageRef required"
2770
+ };
2771
+ if (!isManagedImagePath(data.imageRef)) return {
2772
+ success: false,
2773
+ error: "imageRef must point to a file under the managed image store"
2774
+ };
2775
+ const refCount = await kv.get(KV.imageRefs, data.imageRef);
2776
+ if (!refCount || Number(refCount) < 1) return {
2777
+ success: false,
2778
+ error: "imageRef not registered in mem:image-refs"
2779
+ };
2780
+ try {
2781
+ const vec = await imageProvider.embedImage(data.imageRef);
2782
+ const stored = {
2783
+ imageRef: data.imageRef,
2784
+ vector: Array.from(vec),
2785
+ modelName: imageProvider.name,
2786
+ dimensions: imageProvider.dimensions,
2787
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2788
+ sessionId: data.sessionId,
2789
+ observationId: data.observationId
2790
+ };
2791
+ await kv.set(KV.imageEmbeddings, data.imageRef, stored);
2792
+ await recordAudit(kv, "vision_embed", "mem::vision-embed", [data.imageRef], {
2793
+ modelName: imageProvider.name,
2794
+ dimensions: stored.dimensions,
2795
+ sessionId: data.sessionId,
2796
+ observationId: data.observationId
2797
+ });
2798
+ return {
2799
+ success: true,
2800
+ imageRef: data.imageRef,
2801
+ dimensions: stored.dimensions
2802
+ };
2803
+ } catch (err) {
2804
+ const msg = err instanceof Error ? err.message : String(err);
2805
+ logger.warn("vision-embed failed", {
2806
+ imageRef: data.imageRef,
2807
+ error: msg
2808
+ });
2809
+ return {
2810
+ success: false,
2811
+ error: msg
2812
+ };
2813
+ }
2814
+ });
2815
+ sdk.registerFunction("mem::vision-search", async (data) => {
2816
+ if (!imageProvider?.embedImage) return {
2817
+ success: false,
2818
+ error: "image embeddings disabled (set AGENTMEMORY_IMAGE_EMBEDDINGS=true)"
2819
+ };
2820
+ const requestedTopK = typeof data?.topK === "number" && Number.isFinite(data.topK) ? Math.trunc(data.topK) : 10;
2821
+ const topK = Math.min(50, Math.max(1, requestedTopK));
2822
+ let queryVec = null;
2823
+ try {
2824
+ if (data?.queryText) queryVec = await imageProvider.embed(data.queryText);
2825
+ else if (data?.queryImageBase64) {
2826
+ const b64 = data.queryImageBase64.startsWith("data:") ? data.queryImageBase64 : `data:image/png;base64,${data.queryImageBase64}`;
2827
+ queryVec = await imageProvider.embedImage(b64);
2828
+ } else if (data?.queryImageRef) {
2829
+ if (!isManagedImagePath(data.queryImageRef)) return {
2830
+ success: false,
2831
+ error: "queryImageRef must point to a file under the managed image store"
2832
+ };
2833
+ const refCount = await kv.get(KV.imageRefs, data.queryImageRef);
2834
+ if (!refCount || Number(refCount) < 1) return {
2835
+ success: false,
2836
+ error: "queryImageRef not registered in mem:image-refs"
2837
+ };
2838
+ queryVec = await imageProvider.embedImage(data.queryImageRef);
2839
+ } else return {
2840
+ success: false,
2841
+ error: "queryText, queryImageRef, or queryImageBase64 required"
2842
+ };
2843
+ } catch (err) {
2844
+ return {
2845
+ success: false,
2846
+ error: `query embed failed: ${err instanceof Error ? err.message : String(err)}`
2847
+ };
2848
+ }
2849
+ if (!queryVec) return {
2850
+ success: false,
2851
+ error: "failed to build query vector"
2852
+ };
2853
+ const stored = await kv.list(KV.imageEmbeddings);
2854
+ const scored = (data?.sessionId ? stored.filter((s) => s.sessionId === data.sessionId) : stored).map((s) => ({
2855
+ imageRef: s.imageRef,
2856
+ score: cosine(queryVec, s.vector),
2857
+ sessionId: s.sessionId,
2858
+ observationId: s.observationId,
2859
+ updatedAt: s.updatedAt
2860
+ }));
2861
+ scored.sort((a, b) => b.score - a.score);
2862
+ return {
2863
+ success: true,
2864
+ results: scored.slice(0, topK),
2865
+ total: scored.length
2866
+ };
2867
+ });
2868
+ }
2869
+ function cosine(a, b) {
2870
+ if (a.length !== b.length) return 0;
2871
+ let dot = 0;
2872
+ let normA = 0;
2873
+ let normB = 0;
2874
+ for (let i = 0; i < a.length; i++) {
2875
+ dot += a[i] * b[i];
2876
+ normA += a[i] * a[i];
2877
+ normB += b[i] * b[i];
2878
+ }
2879
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
2880
+ return denom === 0 ? 0 : dot / denom;
2881
+ }
2882
+
2883
+ //#endregion
2884
+ //#region src/functions/slots.ts
2885
+ const DEFAULT_SIZE_LIMIT = 2e3;
2886
+ const DEFAULT_SLOTS = [
2887
+ {
2888
+ label: "persona",
2889
+ content: "",
2890
+ sizeLimit: 1e3,
2891
+ description: "How the agent should see itself: role, tone, behavioural guidelines.",
2892
+ pinned: true,
2893
+ readOnly: false,
2894
+ scope: "global"
2895
+ },
2896
+ {
2897
+ label: "user_preferences",
2898
+ content: "",
2899
+ sizeLimit: 2e3,
2900
+ description: "Coding style, tool preferences, naming conventions, and other habits the user wants preserved across sessions.",
2901
+ pinned: true,
2902
+ readOnly: false,
2903
+ scope: "global"
2904
+ },
2905
+ {
2906
+ label: "tool_guidelines",
2907
+ content: "",
2908
+ sizeLimit: 1500,
2909
+ description: "Rules the agent should follow when picking or sequencing tools (e.g. prefer X over Y, never run Z without confirmation).",
2910
+ pinned: true,
2911
+ readOnly: false,
2912
+ scope: "global"
2913
+ },
2914
+ {
2915
+ label: "project_context",
2916
+ content: "",
2917
+ sizeLimit: 3e3,
2918
+ description: "Architecture decisions, codebase conventions, build/test commands, and cross-cutting constraints for the current project.",
2919
+ pinned: true,
2920
+ readOnly: false,
2921
+ scope: "project"
2922
+ },
2923
+ {
2924
+ label: "guidance",
2925
+ content: "",
2926
+ sizeLimit: 1500,
2927
+ description: "Active advice for the next session: what to focus on, what to avoid, open risks.",
2928
+ pinned: true,
2929
+ readOnly: false,
2930
+ scope: "project"
2931
+ },
2932
+ {
2933
+ label: "pending_items",
2934
+ content: "",
2935
+ sizeLimit: 2e3,
2936
+ description: "Unfinished work, explicit TODOs, and promises made but not yet delivered.",
2937
+ pinned: true,
2938
+ readOnly: false,
2939
+ scope: "project"
2940
+ },
2941
+ {
2942
+ label: "session_patterns",
2943
+ content: "",
2944
+ sizeLimit: 1500,
2945
+ description: "Recurring behaviours and common struggles observed across recent sessions.",
2946
+ pinned: false,
2947
+ readOnly: false,
2948
+ scope: "project"
2949
+ },
2950
+ {
2951
+ label: "self_notes",
2952
+ content: "",
2953
+ sizeLimit: 1500,
2954
+ description: "Free-form notes the agent keeps for itself: hypotheses, dead ends, things to revisit.",
2955
+ pinned: false,
2956
+ readOnly: false,
2957
+ scope: "project"
2958
+ }
2959
+ ];
2960
+ function isSlotsEnabled() {
2961
+ return process.env["AGENTMEMORY_SLOTS"] === "true";
2962
+ }
2963
+ function isReflectEnabled() {
2964
+ return process.env["AGENTMEMORY_REFLECT"] === "true";
2965
+ }
2966
+ function scopeKv(scope) {
2967
+ return scope === "global" ? KV.globalSlots : KV.slots;
2968
+ }
2969
+ function nowIso() {
2970
+ return (/* @__PURE__ */ new Date()).toISOString();
2971
+ }
2972
+ function validateLabel(label) {
2973
+ if (typeof label !== "string") return null;
2974
+ const trimmed = label.trim();
2975
+ if (!trimmed || trimmed.length > 64) return null;
2976
+ if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) return null;
2977
+ return trimmed;
2978
+ }
2979
+ async function readSlot(kv, label) {
2980
+ const project = await kv.get(KV.slots, label);
2981
+ if (project) return {
2982
+ slot: project,
2983
+ scope: "project"
2984
+ };
2985
+ const global = await kv.get(KV.globalSlots, label);
2986
+ if (global) return {
2987
+ slot: global,
2988
+ scope: "global"
2989
+ };
2990
+ return {
2991
+ slot: null,
2992
+ scope: "project"
2993
+ };
2994
+ }
2995
+ async function readSlotInScope(kv, label, scope) {
2996
+ return kv.get(scopeKv(scope), label);
2997
+ }
2998
+ function validateScope(raw) {
2999
+ if (raw === void 0 || raw === null) return "project";
3000
+ if (raw === "project" || raw === "global") return raw;
3001
+ return null;
3002
+ }
3003
+ function validateSizeLimit(raw) {
3004
+ if (raw === void 0 || raw === null) return DEFAULT_SIZE_LIMIT;
3005
+ if (typeof raw !== "number") return null;
3006
+ if (!Number.isInteger(raw) || raw < 1 || raw > 2e4) return null;
3007
+ return raw;
3008
+ }
3009
+ async function seedDefaults(kv) {
3010
+ const ts = nowIso();
3011
+ for (const tmpl of DEFAULT_SLOTS) {
3012
+ const target = scopeKv(tmpl.scope);
3013
+ if (await kv.get(target, tmpl.label)) continue;
3014
+ const slot = {
3015
+ ...tmpl,
3016
+ createdAt: ts,
3017
+ updatedAt: ts
3018
+ };
3019
+ await kv.set(target, tmpl.label, slot);
3020
+ }
3021
+ }
3022
+ function registerSlotsFunctions(sdk, kv) {
3023
+ seedDefaults(kv).catch((err) => {
3024
+ logger.warn("slot defaults seed failed", { error: err instanceof Error ? err.message : String(err) });
3025
+ });
3026
+ sdk.registerFunction("mem::slot-list", async () => {
3027
+ const [project, global] = await Promise.all([kv.list(KV.slots), kv.list(KV.globalSlots)]);
3028
+ const merged = /* @__PURE__ */ new Map();
3029
+ for (const s of global) merged.set(s.label, s);
3030
+ for (const s of project) merged.set(s.label, s);
3031
+ return {
3032
+ success: true,
3033
+ slots: Array.from(merged.values()).sort((a, b) => a.label.localeCompare(b.label))
3034
+ };
3035
+ });
3036
+ sdk.registerFunction("mem::slot-get", async (data) => {
3037
+ const label = validateLabel(data?.label);
3038
+ if (!label) return {
3039
+ success: false,
3040
+ error: "label required (lowercase, starts with letter, [a-z0-9_])"
3041
+ };
3042
+ const { slot, scope } = await readSlot(kv, label);
3043
+ if (!slot) return {
3044
+ success: false,
3045
+ error: "slot not found"
3046
+ };
3047
+ return {
3048
+ success: true,
3049
+ slot,
3050
+ scope
3051
+ };
3052
+ });
3053
+ sdk.registerFunction("mem::slot-create", async (data) => {
3054
+ const label = validateLabel(data?.label);
3055
+ if (!label) return {
3056
+ success: false,
3057
+ error: "label required (lowercase, starts with letter, [a-z0-9_])"
3058
+ };
3059
+ const scope = validateScope(data?.scope);
3060
+ if (!scope) return {
3061
+ success: false,
3062
+ error: "scope must be 'project' or 'global'"
3063
+ };
3064
+ const sizeLimit = validateSizeLimit(data?.sizeLimit);
3065
+ if (sizeLimit === null) return {
3066
+ success: false,
3067
+ error: "sizeLimit must be an integer between 1 and 20000"
3068
+ };
3069
+ const content = typeof data?.content === "string" ? data.content : "";
3070
+ if (content.length > sizeLimit) return {
3071
+ success: false,
3072
+ error: `content exceeds sizeLimit (${content.length} > ${sizeLimit})`
3073
+ };
3074
+ const description = typeof data?.description === "string" ? data.description : "";
3075
+ const pinned = typeof data?.pinned === "boolean" ? data.pinned : true;
3076
+ return withKeyedLock(`slot:${label}`, async () => {
3077
+ if (await readSlotInScope(kv, label, scope)) return {
3078
+ success: false,
3079
+ error: `slot already exists in ${scope} scope`
3080
+ };
3081
+ const ts = nowIso();
3082
+ const slot = {
3083
+ label,
3084
+ content,
3085
+ sizeLimit,
3086
+ description,
3087
+ pinned,
3088
+ readOnly: false,
3089
+ scope,
3090
+ createdAt: ts,
3091
+ updatedAt: ts
3092
+ };
3093
+ await kv.set(scopeKv(scope), label, slot);
3094
+ await recordAudit(kv, "slot_create", "mem::slot-create", [label], {
3095
+ scope,
3096
+ sizeLimit: slot.sizeLimit,
3097
+ pinned: slot.pinned
3098
+ });
3099
+ return {
3100
+ success: true,
3101
+ slot
3102
+ };
3103
+ });
3104
+ });
3105
+ sdk.registerFunction("mem::slot-append", async (data) => {
3106
+ const label = validateLabel(data?.label);
3107
+ if (!label) return {
3108
+ success: false,
3109
+ error: "label required"
3110
+ };
3111
+ const text = typeof data?.text === "string" ? data.text : "";
3112
+ if (!text) return {
3113
+ success: false,
3114
+ error: "text required"
3115
+ };
3116
+ return withKeyedLock(`slot:${label}`, async () => {
3117
+ const { slot, scope } = await readSlot(kv, label);
3118
+ if (!slot) return {
3119
+ success: false,
3120
+ error: "slot not found (use mem::slot-create first)"
3121
+ };
3122
+ if (slot.readOnly) return {
3123
+ success: false,
3124
+ error: "slot is read-only"
3125
+ };
3126
+ const sep = slot.content && !slot.content.endsWith("\n") ? "\n" : "";
3127
+ const next = `${slot.content}${sep}${text}`;
3128
+ if (next.length > slot.sizeLimit) return {
3129
+ success: false,
3130
+ error: `append would exceed sizeLimit (${next.length} > ${slot.sizeLimit}). Use mem::slot-replace to compact first.`,
3131
+ currentSize: slot.content.length,
3132
+ sizeLimit: slot.sizeLimit
3133
+ };
3134
+ const updated = {
3135
+ ...slot,
3136
+ content: next,
3137
+ updatedAt: nowIso()
3138
+ };
3139
+ await kv.set(scopeKv(scope), label, updated);
3140
+ await recordAudit(kv, "slot_append", "mem::slot-append", [label], {
3141
+ scope,
3142
+ added: text.length,
3143
+ total: next.length
3144
+ });
3145
+ return {
3146
+ success: true,
3147
+ slot: updated,
3148
+ size: next.length
3149
+ };
3150
+ });
3151
+ });
3152
+ sdk.registerFunction("mem::slot-replace", async (data) => {
3153
+ const label = validateLabel(data?.label);
3154
+ if (!label) return {
3155
+ success: false,
3156
+ error: "label required"
3157
+ };
3158
+ if (typeof data?.content !== "string") return {
3159
+ success: false,
3160
+ error: "content required (string)"
3161
+ };
3162
+ return withKeyedLock(`slot:${label}`, async () => {
3163
+ const { slot, scope } = await readSlot(kv, label);
3164
+ if (!slot) return {
3165
+ success: false,
3166
+ error: "slot not found (use mem::slot-create first)"
3167
+ };
3168
+ if (slot.readOnly) return {
3169
+ success: false,
3170
+ error: "slot is read-only"
3171
+ };
3172
+ if (data.content.length > slot.sizeLimit) return {
3173
+ success: false,
3174
+ error: `content exceeds sizeLimit (${data.content.length} > ${slot.sizeLimit})`,
3175
+ sizeLimit: slot.sizeLimit
3176
+ };
3177
+ const updated = {
3178
+ ...slot,
3179
+ content: data.content,
3180
+ updatedAt: nowIso()
3181
+ };
3182
+ await kv.set(scopeKv(scope), label, updated);
3183
+ await recordAudit(kv, "slot_replace", "mem::slot-replace", [label], {
3184
+ scope,
3185
+ before: slot.content.length,
3186
+ after: data.content.length
3187
+ });
3188
+ return {
3189
+ success: true,
3190
+ slot: updated,
3191
+ size: data.content.length
3192
+ };
3193
+ });
3194
+ });
3195
+ sdk.registerFunction("mem::slot-delete", async (data) => {
3196
+ const label = validateLabel(data?.label);
3197
+ if (!label) return {
3198
+ success: false,
3199
+ error: "label required"
3200
+ };
3201
+ return withKeyedLock(`slot:${label}`, async () => {
3202
+ const { slot, scope } = await readSlot(kv, label);
3203
+ if (!slot) return {
3204
+ success: false,
3205
+ error: "slot not found"
3206
+ };
3207
+ if (slot.readOnly) return {
3208
+ success: false,
3209
+ error: "slot is read-only"
3210
+ };
3211
+ await kv.delete(scopeKv(scope), label);
3212
+ await recordAudit(kv, "slot_delete", "mem::slot-delete", [label], {
3213
+ scope,
3214
+ size: slot.content.length
3215
+ });
3216
+ return { success: true };
3217
+ });
3218
+ });
3219
+ sdk.registerFunction("mem::slot-reflect", async (data) => {
3220
+ if (!data?.sessionId || typeof data.sessionId !== "string") return {
3221
+ success: false,
3222
+ error: "sessionId required"
3223
+ };
3224
+ const max = typeof data.maxObservations === "number" && Number.isInteger(data.maxObservations) && data.maxObservations > 0 ? Math.min(200, data.maxObservations) : 50;
3225
+ const observations = await kv.list(KV.observations(data.sessionId));
3226
+ if (observations.length === 0) return {
3227
+ success: true,
3228
+ applied: 0,
3229
+ reason: "no observations for session"
3230
+ };
3231
+ const recent = observations.slice().sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || "")).slice(0, max);
3232
+ const pendingLines = [];
3233
+ const patternCounts = /* @__PURE__ */ new Map();
3234
+ const files = /* @__PURE__ */ new Set();
3235
+ for (const obs of recent) {
3236
+ const title = (obs.title || "").toLowerCase();
3237
+ if ((obs.narrative || "").toLowerCase().includes("todo") || title.includes("todo")) pendingLines.push(`- ${obs.title || obs.id}`);
3238
+ if (obs.type === "error") patternCounts.set("errors", (patternCounts.get("errors") ?? 0) + 1);
3239
+ if (obs.type === "command_run") patternCounts.set("commands", (patternCounts.get("commands") ?? 0) + 1);
3240
+ if (obs.files) for (const f of obs.files) files.add(f);
3241
+ }
3242
+ let applied = 0;
3243
+ if (pendingLines.length > 0) {
3244
+ if (await withKeyedLock(`slot:pending_items`, async () => {
3245
+ const { slot, scope } = await readSlot(kv, "pending_items");
3246
+ if (!slot) return false;
3247
+ const already = new Set(slot.content.split("\n"));
3248
+ const fresh = pendingLines.filter((line) => !already.has(line));
3249
+ if (fresh.length === 0) return false;
3250
+ const sep = slot.content && !slot.content.endsWith("\n") ? "\n" : "";
3251
+ const next = `${slot.content}${sep}${fresh.join("\n")}`;
3252
+ const truncated = next.length > slot.sizeLimit ? next.slice(next.length - slot.sizeLimit) : next;
3253
+ await kv.set(scopeKv(scope), "pending_items", {
3254
+ ...slot,
3255
+ content: truncated,
3256
+ updatedAt: nowIso()
3257
+ });
3258
+ return true;
3259
+ })) applied++;
3260
+ }
3261
+ if (patternCounts.size > 0) {
3262
+ if (await withKeyedLock(`slot:session_patterns`, async () => {
3263
+ const { slot, scope } = await readSlot(kv, "session_patterns");
3264
+ if (!slot) return false;
3265
+ const summary = [`last reflection: ${nowIso()}`, ...Array.from(patternCounts.entries()).map(([kind, count]) => `- ${kind}: ${count} in last ${recent.length} observations`)].join("\n");
3266
+ const next = summary.length > slot.sizeLimit ? summary.slice(0, slot.sizeLimit) : summary;
3267
+ await kv.set(scopeKv(scope), "session_patterns", {
3268
+ ...slot,
3269
+ content: next,
3270
+ updatedAt: nowIso()
3271
+ });
3272
+ return true;
3273
+ })) applied++;
3274
+ }
3275
+ if (files.size > 0) {
3276
+ if (await withKeyedLock(`slot:project_context`, async () => {
3277
+ const { slot, scope } = await readSlot(kv, "project_context");
3278
+ if (!slot) return false;
3279
+ const already = slot.content;
3280
+ const fresh = Array.from(files).filter((f) => !already.includes(f)).slice(0, 20);
3281
+ if (fresh.length === 0) return false;
3282
+ const header = already.length === 0 ? "Files touched in recent sessions:" : "";
3283
+ const nextRaw = `${already}${already && !already.endsWith("\n") ? "\n" : ""}${header ? header + "\n" : ""}${fresh.map((f) => `- ${f}`).join("\n")}`;
3284
+ const next = nextRaw.length > slot.sizeLimit ? nextRaw.slice(nextRaw.length - slot.sizeLimit) : nextRaw;
3285
+ await kv.set(scopeKv(scope), "project_context", {
3286
+ ...slot,
3287
+ content: next,
3288
+ updatedAt: nowIso()
3289
+ });
3290
+ return true;
3291
+ })) applied++;
3292
+ }
3293
+ if (applied > 0) await recordAudit(kv, "slot_reflect", "mem::slot-reflect", [data.sessionId], {
3294
+ observationCount: recent.length,
3295
+ slotsUpdated: applied
3296
+ });
3297
+ return {
3298
+ success: true,
3299
+ applied,
3300
+ observationsReviewed: recent.length
3301
+ };
3302
+ });
3303
+ }
3304
+
3305
+ //#endregion
3306
+ //#region src/functions/disk-size-manager.ts
3307
+ const DISK_SIZE_KEY = "system:currentDiskSize";
3308
+ function registerDiskSizeManager(sdk, kv) {
3309
+ sdk.registerFunction("mem::disk-size-delta", async (data) => {
3310
+ if (typeof data?.deltaBytes !== "number" || !isFinite(data.deltaBytes)) return {
3311
+ success: false,
3312
+ error: "deltaBytes must be a finite number"
3313
+ };
3314
+ return withKeyedLock(DISK_SIZE_KEY, async () => {
3315
+ let newTotal = (await kv.get(KV.state, DISK_SIZE_KEY) || 0) + data.deltaBytes;
3316
+ if (newTotal < 0) newTotal = 0;
3317
+ await kv.set(KV.state, DISK_SIZE_KEY, newTotal);
3318
+ if (data.deltaBytes > 0 && newTotal > getMaxBytes()) {
3319
+ sdk.triggerVoid("mem::image-quota-cleanup", {});
3320
+ logger.info("Disk quota exceeded, cleanup triggered", {
3321
+ currentBytes: newTotal,
3322
+ maxBytes: getMaxBytes()
3323
+ });
3324
+ }
3325
+ return {
3326
+ success: true,
3327
+ currentTotal: newTotal
3328
+ };
3329
+ });
3330
+ });
3331
+ }
3332
+
3333
+ //#endregion
3334
+ //#region src/prompts/compression.ts
3335
+ 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.
3336
+
3337
+ Output EXACTLY this XML format with no additional text:
3338
+
3339
+ <observation>
3340
+ <type>one of: file_read, file_write, file_edit, command_run, search, web_fetch, conversation, error, decision, discovery, subagent, notification, task, other</type>
3341
+ <title>Short descriptive title (max 80 chars)</title>
3342
+ <subtitle>One-line context (optional)</subtitle>
3343
+ <facts>
3344
+ <fact>Specific factual detail 1</fact>
3345
+ <fact>Specific factual detail 2</fact>
3346
+ </facts>
3347
+ <narrative>2-3 sentence summary of what happened and why it matters</narrative>
3348
+ <concepts>
3349
+ <concept>technical concept or pattern</concept>
3350
+ </concepts>
3351
+ <files>
3352
+ <file>path/to/file</file>
3353
+ </files>
3354
+ <importance>1-10 scale, 10 being critical architectural decision</importance>
3355
+ </observation>
3356
+
3357
+ Rules:
3358
+ - Be concise but preserve ALL technically relevant details
3359
+ - File paths must be exact
3360
+ - Importance: 1-3 for routine reads, 4-6 for edits/commands, 7-9 for architectural decisions, 10 for breaking changes
3361
+ - Concepts should be reusable search terms (e.g., "React hooks", "SQL migration", "auth middleware")
3362
+ - Strip any secrets, tokens, or credentials from the output`;
3363
+ function buildCompressionPrompt(observation) {
3364
+ const parts = [`Timestamp: ${observation.timestamp}`, `Hook: ${observation.hookType}`];
3365
+ if (observation.toolName) parts.push(`Tool: ${observation.toolName}`);
3366
+ if (observation.toolInput) {
3367
+ const input = typeof observation.toolInput === "string" ? observation.toolInput : JSON.stringify(observation.toolInput, null, 2);
3368
+ parts.push(`Input:\n${truncate$1(input, 4e3)}`);
3369
+ }
3370
+ if (observation.toolOutput) {
3371
+ const output = typeof observation.toolOutput === "string" ? observation.toolOutput : JSON.stringify(observation.toolOutput, null, 2);
3372
+ parts.push(`Output:\n${truncate$1(output, 4e3)}`);
3373
+ }
3374
+ if (observation.userPrompt) parts.push(`User prompt:\n${truncate$1(observation.userPrompt, 2e3)}`);
3375
+ return parts.join("\n\n");
3376
+ }
3377
+ function truncate$1(s, max) {
3378
+ return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
3379
+ }
3380
+
3381
+ //#endregion
3382
+ //#region src/prompts/vision.ts
3383
+ const VISION_DESCRIPTION_PROMPT = `Describe what this image shows in the context of software development. Extract:
3384
+ - What type of image this is (screenshot, diagram, mockup, terminal output, error, etc.)
3385
+ - Key entities visible (files, components, UI elements, error messages)
3386
+ - Relationships or flow shown
3387
+ - Any decisions, errors, or state visible
3388
+ - Text content visible in the image
3389
+
3390
+ Be concise but preserve all technically relevant details. Output plain text, no XML.`;
3391
+
3392
+ //#endregion
3393
+ //#region src/prompts/xml.ts
3394
+ const VALID_TAG = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
3395
+ function getXmlTag(xml, tag) {
3396
+ if (!VALID_TAG.test(tag)) return "";
3397
+ const match = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
3398
+ return match ? match[1].trim() : "";
3399
+ }
3400
+ function getXmlChildren(xml, parentTag, childTag) {
3401
+ if (!VALID_TAG.test(parentTag) || !VALID_TAG.test(childTag)) return [];
3402
+ const parentMatch = xml.match(new RegExp(`<${parentTag}>([\\s\\S]*?)</${parentTag}>`));
2370
3403
  if (!parentMatch) return [];
2371
3404
  const items = [];
2372
3405
  const re = new RegExp(`<${childTag}>([\\s\\S]*?)</${childTag}>`, "g");
@@ -2589,6 +3622,7 @@ const VALID_TYPES$1 = new Set([
2589
3622
  "subagent",
2590
3623
  "notification",
2591
3624
  "task",
3625
+ "image",
2592
3626
  "other"
2593
3627
  ]);
2594
3628
  function parseCompressionXml(xml) {
@@ -2609,11 +3643,32 @@ function parseCompressionXml(xml) {
2609
3643
  function registerCompressFunction(sdk, kv, provider, metricsStore) {
2610
3644
  sdk.registerFunction("mem::compress", async (data) => {
2611
3645
  const startMs = Date.now();
3646
+ let imageDescription;
3647
+ const hasImage = data.raw.modality === "image" || data.raw.modality === "mixed";
3648
+ if (hasImage && data.raw.imageData && provider.describeImage) try {
3649
+ let base64Data = data.raw.imageData;
3650
+ let mimeType = "image/png";
3651
+ if (!data.raw.imageData.startsWith("/9j/") && !data.raw.imageData.startsWith("iVBOR")) {
3652
+ if (!isManagedImagePath(data.raw.imageData)) throw new Error(`Refusing to read image outside managed store: ${data.raw.imageData}`);
3653
+ base64Data = readFileSync(data.raw.imageData).toString("base64");
3654
+ if (data.raw.imageData.endsWith(".jpg") || data.raw.imageData.endsWith(".jpeg")) mimeType = "image/jpeg";
3655
+ else if (data.raw.imageData.endsWith(".webp")) mimeType = "image/webp";
3656
+ else if (data.raw.imageData.endsWith(".gif")) mimeType = "image/gif";
3657
+ }
3658
+ imageDescription = await provider.describeImage(base64Data, mimeType, VISION_DESCRIPTION_PROMPT);
3659
+ logger.info("Image described by vision model", { obsId: data.observationId });
3660
+ } catch (err) {
3661
+ const msg = err instanceof Error ? err.message : String(err);
3662
+ logger.warn("Vision model call failed, falling back to text-only compression", {
3663
+ obsId: data.observationId,
3664
+ error: msg
3665
+ });
3666
+ }
2612
3667
  const prompt = buildCompressionPrompt({
2613
3668
  hookType: data.raw.hookType,
2614
3669
  toolName: data.raw.toolName,
2615
3670
  toolInput: data.raw.toolInput,
2616
- toolOutput: data.raw.toolOutput,
3671
+ toolOutput: imageDescription ? `[Image Description]: ${imageDescription}\n\n${data.raw.toolOutput ?? ""}` : data.raw.toolOutput,
2617
3672
  userPrompt: data.raw.userPrompt,
2618
3673
  timestamp: data.raw.timestamp
2619
3674
  });
@@ -2650,7 +3705,10 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
2650
3705
  sessionId: data.sessionId,
2651
3706
  timestamp: data.raw.timestamp,
2652
3707
  ...parsed,
2653
- confidence: qualityScore / 100
3708
+ confidence: qualityScore / 100,
3709
+ ...hasImage ? { modality: data.raw.modality } : {},
3710
+ ...imageDescription ? { imageDescription } : {},
3711
+ ...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
2654
3712
  };
2655
3713
  await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
2656
3714
  getSearchIndex().add(compressed);
@@ -2844,52 +3902,6 @@ function buildSummaryPrompt(observations) {
2844
3902
  return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
2845
3903
  }
2846
3904
 
2847
- //#endregion
2848
- //#region src/functions/audit.ts
2849
- async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
2850
- const entry = {
2851
- id: generateId("aud"),
2852
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2853
- operation,
2854
- userId,
2855
- functionId,
2856
- targetIds,
2857
- details,
2858
- qualityScore
2859
- };
2860
- await kv.set(KV.audit, entry.id, entry);
2861
- return entry;
2862
- }
2863
- async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
2864
- try {
2865
- await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
2866
- } catch (err) {
2867
- try {
2868
- logger.warn("audit write failed", {
2869
- functionId,
2870
- operation,
2871
- targetIds,
2872
- error: err instanceof Error ? err.message : String(err)
2873
- });
2874
- } catch {}
2875
- }
2876
- }
2877
- async function queryAudit(kv, filter) {
2878
- let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
2879
- if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
2880
- if (filter?.dateFrom) {
2881
- const from = new Date(filter.dateFrom).getTime();
2882
- if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
2883
- entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
2884
- }
2885
- if (filter?.dateTo) {
2886
- const to = new Date(filter.dateTo).getTime();
2887
- if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
2888
- entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
2889
- }
2890
- return entries.slice(0, filter?.limit || 100);
2891
- }
2892
-
2893
3905
  //#endregion
2894
3906
  //#region src/functions/summarize.ts
2895
3907
  function parseSummaryXml(xml, sessionId, project, obsCount) {
@@ -2931,9 +3943,33 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
2931
3943
  error: "no_observations"
2932
3944
  };
2933
3945
  }
3946
+ if (provider.name === "noop") {
3947
+ logger.info("Summarize skipped — no LLM provider configured", { sessionId });
3948
+ return {
3949
+ success: false,
3950
+ error: "no_provider",
3951
+ reason: "No LLM provider key set; Summarize is a no-op. Set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env to enable."
3952
+ };
3953
+ }
2934
3954
  try {
2935
3955
  const prompt = buildSummaryPrompt(compressed);
2936
- const summary = parseSummaryXml(await provider.summarize(SUMMARY_SYSTEM, prompt), sessionId, session.project, compressed.length);
3956
+ const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
3957
+ if (!response || !response.trim()) {
3958
+ const latencyMs = Date.now() - startMs;
3959
+ if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
3960
+ logger.warn("Empty provider response on summarize", {
3961
+ sessionId,
3962
+ provider: provider.name,
3963
+ promptBytes: prompt.length,
3964
+ systemBytes: SUMMARY_SYSTEM.length,
3965
+ observationCount: compressed.length
3966
+ });
3967
+ return {
3968
+ success: false,
3969
+ error: "empty_provider_response"
3970
+ };
3971
+ }
3972
+ const summary = parseSummaryXml(response, sessionId, session.project, compressed.length);
2937
3973
  if (!summary) {
2938
3974
  const latencyMs = Date.now() - startMs;
2939
3975
  if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
@@ -3520,14 +4556,20 @@ function registerRememberFunction(sdk, kv) {
3520
4556
  const deletedMemoryIds = [];
3521
4557
  const deletedObservationIds = [];
3522
4558
  let deletedSession = false;
4559
+ const { decrementImageRef } = await import("./image-refs-CESf9ndJ.mjs");
3523
4560
  if (data.memoryId) {
4561
+ const mem = await kv.get(KV.memories, data.memoryId);
3524
4562
  await kv.delete(KV.memories, data.memoryId);
4563
+ if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
3525
4564
  await deleteAccessLog(kv, data.memoryId);
3526
4565
  deletedMemoryIds.push(data.memoryId);
3527
4566
  deleted++;
3528
4567
  }
3529
4568
  if (data.sessionId && data.observationIds && data.observationIds.length > 0) for (const obsId of data.observationIds) {
4569
+ const obs = await kv.get(KV.observations(data.sessionId), obsId);
3530
4570
  await kv.delete(KV.observations(data.sessionId), obsId);
4571
+ if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
4572
+ if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
3531
4573
  deletedObservationIds.push(obsId);
3532
4574
  deleted++;
3533
4575
  }
@@ -3535,6 +4577,8 @@ function registerRememberFunction(sdk, kv) {
3535
4577
  const observations = await kv.list(KV.observations(data.sessionId));
3536
4578
  for (const obs of observations) {
3537
4579
  await kv.delete(KV.observations(data.sessionId), obs.id);
4580
+ if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
4581
+ if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
3538
4582
  deletedObservationIds.push(obs.id);
3539
4583
  deleted++;
3540
4584
  }
@@ -3571,6 +4615,7 @@ const DEFAULTS$1 = {
3571
4615
  function registerEvictFunction(sdk, kv) {
3572
4616
  sdk.registerFunction("mem::evict", async (data) => {
3573
4617
  const dryRun = data?.dryRun ?? false;
4618
+ const { decrementImageRef } = await import("./image-refs-CESf9ndJ.mjs");
3574
4619
  const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
3575
4620
  const cfg = {
3576
4621
  ...DEFAULTS$1,
@@ -3629,6 +4674,8 @@ function registerEvictFunction(sdk, kv) {
3629
4674
  });
3630
4675
  continue;
3631
4676
  }
4677
+ if (o.imageData) await decrementImageRef(kv, sdk, o.imageData);
4678
+ if (o.imageRef && o.imageRef !== o.imageData) await decrementImageRef(kv, sdk, o.imageRef);
3632
4679
  await recordAudit(kv, "delete", "mem::evict", [o.id], {
3633
4680
  resource: "observation",
3634
4681
  reason: "low_importance_old_observation",
@@ -3658,6 +4705,8 @@ function registerEvictFunction(sdk, kv) {
3658
4705
  });
3659
4706
  continue;
3660
4707
  }
4708
+ if (o.imageData) await decrementImageRef(kv, sdk, o.imageData);
4709
+ if (o.imageRef && o.imageRef !== o.imageData) await decrementImageRef(kv, sdk, o.imageRef);
3661
4710
  await recordAudit(kv, "delete", "mem::evict", [o.id], {
3662
4711
  resource: "observation",
3663
4712
  reason: "project_observation_cap",
@@ -3687,6 +4736,7 @@ function registerEvictFunction(sdk, kv) {
3687
4736
  });
3688
4737
  continue;
3689
4738
  }
4739
+ if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
3690
4740
  await recordAudit(kv, "delete", "mem::evict", [mem.id], {
3691
4741
  resource: "memory",
3692
4742
  reason: "expired_memory",
@@ -3710,6 +4760,7 @@ function registerEvictFunction(sdk, kv) {
3710
4760
  });
3711
4761
  continue;
3712
4762
  }
4763
+ if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
3713
4764
  await recordAudit(kv, "delete", "mem::evict", [mem.id], {
3714
4765
  resource: "memory",
3715
4766
  reason: "old_non_latest_memory",
@@ -4167,6 +5218,7 @@ function registerAutoForgetFunction(sdk, kv) {
4167
5218
  sdk.registerFunction("mem::auto-forget", async (data) => {
4168
5219
  const dryRun = data?.dryRun ?? false;
4169
5220
  const now = Date.now();
5221
+ const { decrementImageRef } = await import("./image-refs-CESf9ndJ.mjs");
4170
5222
  const result = {
4171
5223
  ttlExpired: [],
4172
5224
  contradictions: [],
@@ -4180,6 +5232,7 @@ function registerAutoForgetFunction(sdk, kv) {
4180
5232
  result.ttlExpired.push(mem.id);
4181
5233
  deletedIds.add(mem.id);
4182
5234
  if (!dryRun) {
5235
+ if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
4183
5236
  await kv.delete(KV.memories, mem.id);
4184
5237
  await recordAudit(kv, "delete", "mem::auto-forget", [mem.id], {
4185
5238
  resource: "memory",
@@ -4248,13 +5301,23 @@ function registerAutoForgetFunction(sdk, kv) {
4248
5301
  if (now - new Date(obs.timestamp).getTime() > 180 * MS_PER_DAY && (obs.importance ?? 5) <= 2) {
4249
5302
  result.lowValueObs.push(obs.id);
4250
5303
  if (!dryRun) {
4251
- await kv.delete(KV.observations(sessions[i].id), obs.id).catch(() => {});
4252
- await recordAudit(kv, "delete", "mem::auto-forget", [obs.id], {
4253
- resource: "observation",
4254
- reason: "auto-forget low-value observation",
4255
- sessionId: sessions[i].id,
4256
- timestamp: obs.timestamp
4257
- });
5304
+ let deletedOk = false;
5305
+ try {
5306
+ await kv.delete(KV.observations(sessions[i].id), obs.id);
5307
+ deletedOk = true;
5308
+ } catch {
5309
+ deletedOk = false;
5310
+ }
5311
+ if (deletedOk) {
5312
+ if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
5313
+ if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
5314
+ await recordAudit(kv, "delete", "mem::auto-forget", [obs.id], {
5315
+ resource: "observation",
5316
+ reason: "auto-forget low-value observation",
5317
+ sessionId: sessions[i].id,
5318
+ timestamp: obs.timestamp
5319
+ });
5320
+ }
4258
5321
  }
4259
5322
  }
4260
5323
  }
@@ -4385,7 +5448,9 @@ function registerExportImportFunction(sdk, kv) {
4385
5448
  "0.8.12",
4386
5449
  "0.8.13",
4387
5450
  "0.9.0",
4388
- "0.9.1"
5451
+ "0.9.1",
5452
+ "0.9.2",
5453
+ "0.9.3"
4389
5454
  ]).has(importData.version)) return {
4390
5455
  success: false,
4391
5456
  error: `Unsupported export version: ${importData.version}`
@@ -10742,6 +11807,7 @@ function registerRetentionFunctions(sdk, kv) {
10742
11807
  const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
10743
11808
  const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
10744
11809
  const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
11810
+ const { decrementImageRef } = await import("./image-refs-CESf9ndJ.mjs");
10745
11811
  const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
10746
11812
  if (data?.dryRun) return {
10747
11813
  success: true,
@@ -10773,6 +11839,8 @@ function registerRetentionFunctions(sdk, kv) {
10773
11839
  resolvedSource = "semantic";
10774
11840
  }
10775
11841
  if (!scope || !resolvedSource) continue;
11842
+ const mem = await kv.get(scope, candidate.memoryId);
11843
+ if (mem && mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
10776
11844
  await kv.delete(scope, candidate.memoryId);
10777
11845
  await kv.delete(KV.retentionScores, candidate.memoryId);
10778
11846
  await deleteAccessLog(kv, candidate.memoryId);
@@ -11210,6 +12278,92 @@ function rawFromCompressed(obs) {
11210
12278
  }
11211
12279
  };
11212
12280
  }
12281
+ 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];
12282
+ async function deriveCrystalAndLessons(kv, sessionId, project, rawObs, compressed, firstPrompt) {
12283
+ if (rawObs.length === 0) return;
12284
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
12285
+ const files = /* @__PURE__ */ new Set();
12286
+ const tools = /* @__PURE__ */ new Set();
12287
+ for (const c of compressed) {
12288
+ for (const f of c.files || []) files.add(f);
12289
+ if (c.type && c.type !== "conversation" && c.title) tools.add(c.title);
12290
+ }
12291
+ const assistantTexts = [];
12292
+ const userPrompts = [];
12293
+ for (const r of rawObs) {
12294
+ if (typeof r.assistantResponse === "string" && r.assistantResponse.trim()) assistantTexts.push(r.assistantResponse);
12295
+ if (typeof r.userPrompt === "string" && r.userPrompt.trim()) userPrompts.push(r.userPrompt);
12296
+ }
12297
+ const lessonMatches = /* @__PURE__ */ new Map();
12298
+ for (const text of assistantTexts.concat(userPrompts).slice(0, 200)) for (const pat of LESSON_PATTERNS) {
12299
+ pat.lastIndex = 0;
12300
+ let m;
12301
+ while ((m = pat.exec(text)) !== null && lessonMatches.size < 40) {
12302
+ const snippet = m[0].replace(/\s+/g, " ").trim();
12303
+ if (snippet.length >= 20 && snippet.length <= 220) {
12304
+ const key = snippet.toLowerCase();
12305
+ if (!lessonMatches.has(key)) lessonMatches.set(key, snippet);
12306
+ }
12307
+ }
12308
+ }
12309
+ const lessonEntries = Array.from(lessonMatches.values()).slice(0, 20);
12310
+ const lessonIds = [];
12311
+ for (const content of lessonEntries) {
12312
+ const lessonId = fingerprintId("lesson", content.trim().toLowerCase());
12313
+ try {
12314
+ const existing = await kv.get(KV.lessons, lessonId);
12315
+ if (existing) {
12316
+ const existingSources = existing.sourceIds || [];
12317
+ const mergedSources = existingSources.includes(sessionId) ? existingSources : [...existingSources, sessionId];
12318
+ const existingTags = existing.tags || [];
12319
+ const mergedTags = existingTags.includes("auto-import") ? existingTags : [...existingTags, "auto-import"];
12320
+ const merged = {
12321
+ ...existing,
12322
+ sourceIds: mergedSources,
12323
+ tags: mergedTags,
12324
+ reinforcements: (existing.reinforcements || 0) + 1,
12325
+ updatedAt: createdAt,
12326
+ lastReinforcedAt: createdAt
12327
+ };
12328
+ await kv.set(KV.lessons, lessonId, merged);
12329
+ } else {
12330
+ const lesson = {
12331
+ id: lessonId,
12332
+ content,
12333
+ context: firstPrompt || project,
12334
+ confidence: .4,
12335
+ reinforcements: 0,
12336
+ source: "consolidation",
12337
+ sourceIds: [sessionId],
12338
+ project,
12339
+ tags: ["auto-import"],
12340
+ createdAt,
12341
+ updatedAt: createdAt,
12342
+ decayRate: .05
12343
+ };
12344
+ await kv.set(KV.lessons, lessonId, lesson);
12345
+ }
12346
+ lessonIds.push(lessonId);
12347
+ } catch {}
12348
+ }
12349
+ const crystalId = fingerprintId("crystal", sessionId);
12350
+ const narrativePreview = firstPrompt ? firstPrompt.slice(0, 300) : compressed.slice(0, 5).map((c) => c.narrative || c.title).filter(Boolean).join(" · ").slice(0, 300);
12351
+ try {
12352
+ const existingCrystal = await kv.get(KV.crystals, crystalId);
12353
+ const crystal = {
12354
+ id: crystalId,
12355
+ narrative: narrativePreview || `Session ${sessionId.slice(0, 12)} (${rawObs.length} observations)`,
12356
+ keyOutcomes: Array.from(tools).slice(0, 8),
12357
+ filesAffected: Array.from(files).slice(0, 20),
12358
+ lessons: lessonIds,
12359
+ sourceActionIds: existingCrystal?.sourceActionIds ?? [],
12360
+ sessionId,
12361
+ project,
12362
+ createdAt: existingCrystal?.createdAt ?? createdAt
12363
+ };
12364
+ await kv.set(KV.crystals, crystalId, crystal);
12365
+ } catch {}
12366
+ }
11213
12367
  function isRawShape(o) {
11214
12368
  if (!o || typeof o !== "object") return false;
11215
12369
  return typeof o.hookType === "string";
@@ -11320,6 +12474,8 @@ function registerReplayFunctions(sdk, kv) {
11320
12474
  }
11321
12475
  const parsed = parseJsonlText(text, generateId("sess"));
11322
12476
  if (parsed.observations.length === 0) continue;
12477
+ const firstPromptObs = parsed.observations.find((o) => typeof o.userPrompt === "string" && o.userPrompt.trim().length > 0);
12478
+ const firstPrompt = firstPromptObs?.userPrompt ? firstPromptObs.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
11323
12479
  const existing = await kv.get(KV.sessions, parsed.sessionId);
11324
12480
  if (existing) {
11325
12481
  existing.observationCount = (existing.observationCount || 0) + parsed.observations.length;
@@ -11327,6 +12483,7 @@ function registerReplayFunctions(sdk, kv) {
11327
12483
  if (existing.status === "active") existing.status = "completed";
11328
12484
  const existingTags = existing.tags || [];
11329
12485
  if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
12486
+ if (!existing.firstPrompt && firstPrompt) existing.firstPrompt = firstPrompt;
11330
12487
  await kv.set(KV.sessions, existing.id, existing);
11331
12488
  } else {
11332
12489
  const session = {
@@ -11337,13 +12494,22 @@ function registerReplayFunctions(sdk, kv) {
11337
12494
  endedAt: parsed.endedAt,
11338
12495
  status: "completed",
11339
12496
  observationCount: parsed.observations.length,
11340
- tags: ["jsonl-import"]
12497
+ tags: ["jsonl-import"],
12498
+ firstPrompt
11341
12499
  };
11342
12500
  await kv.set(KV.sessions, session.id, session);
11343
12501
  }
11344
- await Promise.all(parsed.observations.map((obs) => kv.set(KV.observations(parsed.sessionId), obs.id, obs)));
12502
+ const searchIndex = getSearchIndex();
12503
+ const compressed = [];
12504
+ await Promise.all(parsed.observations.map(async (obs) => {
12505
+ const synthetic = buildSyntheticCompression(obs);
12506
+ compressed.push(synthetic);
12507
+ await kv.set(KV.observations(parsed.sessionId), obs.id, synthetic);
12508
+ searchIndex.add(synthetic);
12509
+ }));
11345
12510
  observationCount += parsed.observations.length;
11346
12511
  sessionIds.push(parsed.sessionId);
12512
+ await deriveCrystalAndLessons(kv, parsed.sessionId, parsed.project, parsed.observations, compressed, firstPrompt);
11347
12513
  }
11348
12514
  await safeAudit(kv, "import", "mem::replay::import-jsonl", sessionIds, {
11349
12515
  source: "jsonl",
@@ -11377,6 +12543,7 @@ function evaluateHealth(snapshot, config = {}) {
11377
12543
  ...config
11378
12544
  };
11379
12545
  const alerts = [];
12546
+ const notes = [];
11380
12547
  let critical = false;
11381
12548
  let degraded = false;
11382
12549
  if (snapshot.connectionState === "disconnected" || snapshot.connectionState === "failed") {
@@ -11410,10 +12577,11 @@ function evaluateHealth(snapshot, config = {}) {
11410
12577
  } else if (memPercent > cfg.memoryWarnPercent && rssAboveFloor) {
11411
12578
  alerts.push(`memory_warn_${Math.round(memPercent)}%_rss${memMb}mb`);
11412
12579
  degraded = true;
11413
- } else if (memPercent > cfg.memoryWarnPercent) alerts.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
12580
+ } else if (memPercent > cfg.memoryWarnPercent) notes.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
11414
12581
  return {
11415
12582
  status: critical ? "critical" : degraded ? "degraded" : "healthy",
11416
- alerts
12583
+ alerts,
12584
+ notes
11417
12585
  };
11418
12586
  }
11419
12587
 
@@ -11490,6 +12658,7 @@ function registerHealthMonitor(sdk, kv) {
11490
12658
  const evaluated = evaluateHealth(snapshot);
11491
12659
  snapshot.status = evaluated.status;
11492
12660
  snapshot.alerts = evaluated.alerts;
12661
+ snapshot.notes = evaluated.notes;
11493
12662
  await kv.set(KV.health, "latest", snapshot).catch(() => {});
11494
12663
  return snapshot;
11495
12664
  }
@@ -11579,6 +12748,28 @@ function requireConfiguredSecret(secret, feature) {
11579
12748
  body: { error: `${feature} requires AGENTMEMORY_SECRET` }
11580
12749
  };
11581
12750
  }
12751
+ function flagDisabledResponse(opts) {
12752
+ return {
12753
+ status_code: 503,
12754
+ body: opts
12755
+ };
12756
+ }
12757
+ function graphDisabledResponse() {
12758
+ return flagDisabledResponse({
12759
+ error: "Knowledge graph not enabled",
12760
+ flag: "GRAPH_EXTRACTION_ENABLED",
12761
+ enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and restart. Requires an LLM provider key.",
12762
+ docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
12763
+ });
12764
+ }
12765
+ function consolidationDisabledResponse() {
12766
+ return flagDisabledResponse({
12767
+ error: "Consolidation pipeline not enabled",
12768
+ flag: "CONSOLIDATION_ENABLED",
12769
+ enableHow: "Set CONSOLIDATION_ENABLED=true and restart. Requires an LLM provider key.",
12770
+ docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
12771
+ });
12772
+ }
11582
12773
  function asNonEmptyString$1(value) {
11583
12774
  if (typeof value !== "string") return null;
11584
12775
  const trimmed = value.trim();
@@ -11630,6 +12821,78 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
11630
12821
  http_method: "GET"
11631
12822
  }
11632
12823
  });
12824
+ sdk.registerFunction("api::config-flags", async (req) => {
12825
+ const authErr = checkAuth(req, secret);
12826
+ if (authErr) return authErr;
12827
+ const env = process.env;
12828
+ return {
12829
+ status_code: 200,
12830
+ body: {
12831
+ version: VERSION,
12832
+ provider: env["ANTHROPIC_API_KEY"] || env["GEMINI_API_KEY"] || env["OPENROUTER_API_KEY"] || env["MINIMAX_API_KEY"] ? "llm" : "noop",
12833
+ embeddingProvider: detectEmbeddingProvider() ? "embeddings" : "none",
12834
+ flags: [
12835
+ {
12836
+ key: "GRAPH_EXTRACTION_ENABLED",
12837
+ label: "Knowledge graph extraction",
12838
+ enabled: isGraphExtractionEnabled(),
12839
+ default: false,
12840
+ affects: ["Graph", "Dashboard"],
12841
+ needsLlm: true,
12842
+ description: "Extracts entities and relations from observations into a knowledge graph.",
12843
+ enableHow: "Set GRAPH_EXTRACTION_ENABLED=true and provide an LLM key, then restart.",
12844
+ docsHref: "https://github.com/rohitg00/agentmemory#knowledge-graph"
12845
+ },
12846
+ {
12847
+ key: "CONSOLIDATION_ENABLED",
12848
+ label: "Memory consolidation",
12849
+ enabled: isConsolidationEnabled(),
12850
+ default: false,
12851
+ affects: [
12852
+ "Dashboard",
12853
+ "Memories",
12854
+ "Crystals"
12855
+ ],
12856
+ needsLlm: true,
12857
+ description: "Periodically summarizes sessions into semantic facts + procedures.",
12858
+ enableHow: "Set CONSOLIDATION_ENABLED=true and provide an LLM key, then restart.",
12859
+ docsHref: "https://github.com/rohitg00/agentmemory#consolidation"
12860
+ },
12861
+ {
12862
+ key: "AGENTMEMORY_AUTO_COMPRESS",
12863
+ label: "LLM-powered observation compression",
12864
+ enabled: isAutoCompressEnabled(),
12865
+ default: false,
12866
+ affects: ["Memories", "Timeline"],
12867
+ needsLlm: true,
12868
+ description: "Every observation is compressed by the LLM for richer summaries (costs tokens). OFF uses zero-LLM synthetic compression.",
12869
+ enableHow: "Set AGENTMEMORY_AUTO_COMPRESS=true and provide an LLM key.",
12870
+ docsHref: "https://github.com/rohitg00/agentmemory/issues/138"
12871
+ },
12872
+ {
12873
+ key: "AGENTMEMORY_INJECT_CONTEXT",
12874
+ label: "In-conversation context injection",
12875
+ enabled: isContextInjectionEnabled(),
12876
+ default: false,
12877
+ affects: ["Hooks"],
12878
+ needsLlm: false,
12879
+ description: "Hooks write recalled context into Claude Code's conversation. OFF captures in the background without injecting.",
12880
+ enableHow: "Set AGENTMEMORY_INJECT_CONTEXT=true and restart.",
12881
+ docsHref: "https://github.com/rohitg00/agentmemory/issues/143"
12882
+ }
12883
+ ]
12884
+ }
12885
+ };
12886
+ });
12887
+ sdk.registerTrigger({
12888
+ type: "http",
12889
+ function_id: "api::config-flags",
12890
+ config: {
12891
+ api_path: "/agentmemory/config/flags",
12892
+ http_method: "GET",
12893
+ middleware_function_ids: ["middleware::api-auth"]
12894
+ }
12895
+ });
11633
12896
  sdk.registerFunction("api::health", async (req) => {
11634
12897
  const health = await getLatestHealth(kv);
11635
12898
  const functionMetrics = metricsStore ? await metricsStore.getAll() : [];
@@ -11833,9 +13096,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
11833
13096
  sdk.registerFunction("api::replay::sessions", async (req) => {
11834
13097
  const authErr = checkAuth(req, secret);
11835
13098
  if (authErr) return authErr;
13099
+ const sessions = await kv.list(KV.sessions);
13100
+ sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
11836
13101
  return {
11837
13102
  status_code: 200,
11838
- body: await sdk.trigger({ function_id: "mem::replay::sessions" })
13103
+ body: {
13104
+ success: true,
13105
+ sessions
13106
+ }
11839
13107
  };
11840
13108
  });
11841
13109
  sdk.registerTrigger({
@@ -12445,10 +13713,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12445
13713
  })
12446
13714
  };
12447
13715
  } catch {
12448
- return {
12449
- status_code: 404,
12450
- body: { error: "Knowledge graph not enabled" }
12451
- };
13716
+ return graphDisabledResponse();
12452
13717
  }
12453
13718
  });
12454
13719
  sdk.registerTrigger({
@@ -12471,10 +13736,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12471
13736
  })
12472
13737
  };
12473
13738
  } catch {
12474
- return {
12475
- status_code: 404,
12476
- body: { error: "Knowledge graph not enabled" }
12477
- };
13739
+ return graphDisabledResponse();
12478
13740
  }
12479
13741
  });
12480
13742
  sdk.registerTrigger({
@@ -12501,10 +13763,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12501
13763
  })
12502
13764
  };
12503
13765
  } catch {
12504
- return {
12505
- status_code: 404,
12506
- body: { error: "Knowledge graph not enabled" }
12507
- };
13766
+ return graphDisabledResponse();
12508
13767
  }
12509
13768
  });
12510
13769
  sdk.registerTrigger({
@@ -12527,10 +13786,7 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12527
13786
  })
12528
13787
  };
12529
13788
  } catch {
12530
- return {
12531
- status_code: 404,
12532
- body: { error: "Consolidation pipeline not enabled" }
12533
- };
13789
+ return consolidationDisabledResponse();
12534
13790
  }
12535
13791
  });
12536
13792
  sdk.registerTrigger({
@@ -12630,13 +13886,16 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12630
13886
  const parsedLimit = parseOptionalInt(req.query_params?.["limit"]);
12631
13887
  return {
12632
13888
  status_code: 200,
12633
- body: await sdk.trigger({
12634
- function_id: "mem::audit-query",
12635
- payload: {
12636
- operation: req.query_params?.["operation"],
12637
- limit: parsedLimit ?? 50
12638
- }
12639
- })
13889
+ body: {
13890
+ entries: await sdk.trigger({
13891
+ function_id: "mem::audit-query",
13892
+ payload: {
13893
+ operation: req.query_params?.["operation"],
13894
+ limit: parsedLimit ?? 50
13895
+ }
13896
+ }),
13897
+ success: true
13898
+ }
12640
13899
  };
12641
13900
  });
12642
13901
  sdk.registerTrigger({
@@ -12683,157 +13942,489 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12683
13942
  });
12684
13943
  sdk.registerTrigger({
12685
13944
  type: "http",
12686
- function_id: "api::governance-bulk",
13945
+ function_id: "api::governance-bulk",
13946
+ config: {
13947
+ api_path: "/agentmemory/governance/bulk-delete",
13948
+ http_method: "POST"
13949
+ }
13950
+ });
13951
+ sdk.registerFunction("api::snapshots", async (req) => {
13952
+ const authErr = checkAuth(req, secret);
13953
+ if (authErr) return authErr;
13954
+ try {
13955
+ return {
13956
+ status_code: 200,
13957
+ body: await sdk.trigger({
13958
+ function_id: "mem::snapshot-list",
13959
+ payload: {}
13960
+ })
13961
+ };
13962
+ } catch {
13963
+ return {
13964
+ status_code: 404,
13965
+ body: { error: "Snapshots not enabled" }
13966
+ };
13967
+ }
13968
+ });
13969
+ sdk.registerTrigger({
13970
+ type: "http",
13971
+ function_id: "api::snapshots",
13972
+ config: {
13973
+ api_path: "/agentmemory/snapshots",
13974
+ http_method: "GET"
13975
+ }
13976
+ });
13977
+ sdk.registerFunction("api::snapshot-create", async (req) => {
13978
+ const authErr = checkAuth(req, secret);
13979
+ if (authErr) return authErr;
13980
+ try {
13981
+ return {
13982
+ status_code: 201,
13983
+ body: await sdk.trigger({
13984
+ function_id: "mem::snapshot-create",
13985
+ payload: req.body || {}
13986
+ })
13987
+ };
13988
+ } catch {
13989
+ return {
13990
+ status_code: 404,
13991
+ body: { error: "Snapshots not enabled" }
13992
+ };
13993
+ }
13994
+ });
13995
+ sdk.registerTrigger({
13996
+ type: "http",
13997
+ function_id: "api::snapshot-create",
13998
+ config: {
13999
+ api_path: "/agentmemory/snapshot/create",
14000
+ http_method: "POST"
14001
+ }
14002
+ });
14003
+ sdk.registerFunction("api::snapshot-restore", async (req) => {
14004
+ const authErr = checkAuth(req, secret);
14005
+ if (authErr) return authErr;
14006
+ if (!req.body?.commitHash) return {
14007
+ status_code: 400,
14008
+ body: { error: "commitHash is required" }
14009
+ };
14010
+ try {
14011
+ return {
14012
+ status_code: 200,
14013
+ body: await sdk.trigger({
14014
+ function_id: "mem::snapshot-restore",
14015
+ payload: req.body
14016
+ })
14017
+ };
14018
+ } catch {
14019
+ return {
14020
+ status_code: 404,
14021
+ body: { error: "Snapshots not enabled" }
14022
+ };
14023
+ }
14024
+ });
14025
+ sdk.registerTrigger({
14026
+ type: "http",
14027
+ function_id: "api::snapshot-restore",
14028
+ config: {
14029
+ api_path: "/agentmemory/snapshot/restore",
14030
+ http_method: "POST"
14031
+ }
14032
+ });
14033
+ sdk.registerFunction("api::memories", async (req) => {
14034
+ const authErr = checkAuth(req, secret);
14035
+ if (authErr) return authErr;
14036
+ const memories = await kv.list(KV.memories);
14037
+ return {
14038
+ status_code: 200,
14039
+ body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
14040
+ };
14041
+ });
14042
+ sdk.registerTrigger({
14043
+ type: "http",
14044
+ function_id: "api::memories",
14045
+ config: {
14046
+ api_path: "/agentmemory/memories",
14047
+ http_method: "GET"
14048
+ }
14049
+ });
14050
+ sdk.registerFunction("api::semantic-list", async (req) => {
14051
+ const authErr = checkAuth(req, secret);
14052
+ if (authErr) return authErr;
14053
+ return {
14054
+ status_code: 200,
14055
+ body: { semantic: await kv.list(KV.semantic) }
14056
+ };
14057
+ });
14058
+ sdk.registerTrigger({
14059
+ type: "http",
14060
+ function_id: "api::semantic-list",
14061
+ config: {
14062
+ api_path: "/agentmemory/semantic",
14063
+ http_method: "GET"
14064
+ }
14065
+ });
14066
+ sdk.registerFunction("api::procedural-list", async (req) => {
14067
+ const authErr = checkAuth(req, secret);
14068
+ if (authErr) return authErr;
14069
+ return {
14070
+ status_code: 200,
14071
+ body: { procedural: await kv.list(KV.procedural) }
14072
+ };
14073
+ });
14074
+ sdk.registerTrigger({
14075
+ type: "http",
14076
+ function_id: "api::procedural-list",
14077
+ config: {
14078
+ api_path: "/agentmemory/procedural",
14079
+ http_method: "GET"
14080
+ }
14081
+ });
14082
+ sdk.registerFunction("api::relations-list", async (req) => {
14083
+ const authErr = checkAuth(req, secret);
14084
+ if (authErr) return authErr;
14085
+ return {
14086
+ status_code: 200,
14087
+ body: { relations: await kv.list(KV.relations) }
14088
+ };
14089
+ });
14090
+ sdk.registerTrigger({
14091
+ type: "http",
14092
+ function_id: "api::relations-list",
14093
+ config: {
14094
+ api_path: "/agentmemory/relations",
14095
+ http_method: "GET"
14096
+ }
14097
+ });
14098
+ sdk.registerFunction("api::vision-search", async (req) => {
14099
+ const authErr = checkAuth(req, secret);
14100
+ if (authErr) return authErr;
14101
+ const body = req.body ?? {};
14102
+ const queryText = asNonEmptyString$1(body["queryText"]);
14103
+ const queryImageRef = asNonEmptyString$1(body["queryImageRef"]);
14104
+ const queryImageBase64 = asNonEmptyString$1(body["queryImageBase64"]);
14105
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14106
+ if (!queryText && !queryImageRef && !queryImageBase64) return {
14107
+ status_code: 400,
14108
+ body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
14109
+ };
14110
+ const topKParsed = parseOptionalPositiveInt(body["topK"]);
14111
+ if (topKParsed === null) return {
14112
+ status_code: 400,
14113
+ body: { error: "topK must be a positive integer" }
14114
+ };
14115
+ const payload = {};
14116
+ if (queryText) payload["queryText"] = queryText;
14117
+ if (queryImageRef) payload["queryImageRef"] = queryImageRef;
14118
+ if (queryImageBase64) payload["queryImageBase64"] = queryImageBase64;
14119
+ if (sessionId) payload["sessionId"] = sessionId;
14120
+ if (topKParsed !== void 0) payload["topK"] = Math.min(50, topKParsed);
14121
+ const result = await sdk.trigger({
14122
+ function_id: "mem::vision-search",
14123
+ payload
14124
+ });
14125
+ const resp = result;
14126
+ if (resp?.success === false) return {
14127
+ status_code: resp.error?.includes("disabled") ? 503 : 400,
14128
+ body: resp
14129
+ };
14130
+ return {
14131
+ status_code: 200,
14132
+ body: result
14133
+ };
14134
+ });
14135
+ sdk.registerTrigger({
14136
+ type: "http",
14137
+ function_id: "api::vision-search",
14138
+ config: {
14139
+ api_path: "/agentmemory/vision-search",
14140
+ http_method: "POST"
14141
+ }
14142
+ });
14143
+ sdk.registerFunction("api::vision-embed", async (req) => {
14144
+ const authErr = checkAuth(req, secret);
14145
+ if (authErr) return authErr;
14146
+ const body = req.body ?? {};
14147
+ const imageRef = asNonEmptyString$1(body["imageRef"]);
14148
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14149
+ const observationId = asNonEmptyString$1(body["observationId"]);
14150
+ if (!imageRef) return {
14151
+ status_code: 400,
14152
+ body: { error: "imageRef is required" }
14153
+ };
14154
+ const payload = { imageRef };
14155
+ if (sessionId) payload["sessionId"] = sessionId;
14156
+ if (observationId) payload["observationId"] = observationId;
14157
+ const result = await sdk.trigger({
14158
+ function_id: "mem::vision-embed",
14159
+ payload
14160
+ });
14161
+ const resp = result;
14162
+ if (resp?.success === false) return {
14163
+ status_code: resp.error?.includes("disabled") ? 503 : 400,
14164
+ body: resp
14165
+ };
14166
+ return {
14167
+ status_code: 200,
14168
+ body: result
14169
+ };
14170
+ });
14171
+ sdk.registerTrigger({
14172
+ type: "http",
14173
+ function_id: "api::vision-embed",
12687
14174
  config: {
12688
- api_path: "/agentmemory/governance/bulk-delete",
14175
+ api_path: "/agentmemory/vision-embed",
12689
14176
  http_method: "POST"
12690
14177
  }
12691
14178
  });
12692
- sdk.registerFunction("api::snapshots", async (req) => {
14179
+ sdk.registerFunction("api::slot-list", async (req) => {
12693
14180
  const authErr = checkAuth(req, secret);
12694
14181
  if (authErr) return authErr;
12695
- try {
12696
- return {
12697
- status_code: 200,
12698
- body: await sdk.trigger({
12699
- function_id: "mem::snapshot-list",
12700
- payload: {}
12701
- })
12702
- };
12703
- } catch {
12704
- return {
12705
- status_code: 404,
12706
- body: { error: "Snapshots not enabled" }
12707
- };
12708
- }
14182
+ return {
14183
+ status_code: 200,
14184
+ body: await sdk.trigger({
14185
+ function_id: "mem::slot-list",
14186
+ payload: {}
14187
+ })
14188
+ };
12709
14189
  });
12710
14190
  sdk.registerTrigger({
12711
14191
  type: "http",
12712
- function_id: "api::snapshots",
14192
+ function_id: "api::slot-list",
12713
14193
  config: {
12714
- api_path: "/agentmemory/snapshots",
14194
+ api_path: "/agentmemory/slots",
12715
14195
  http_method: "GET"
12716
14196
  }
12717
14197
  });
12718
- sdk.registerFunction("api::snapshot-create", async (req) => {
14198
+ sdk.registerFunction("api::slot-get", async (req) => {
12719
14199
  const authErr = checkAuth(req, secret);
12720
14200
  if (authErr) return authErr;
12721
- try {
12722
- return {
12723
- status_code: 201,
12724
- body: await sdk.trigger({
12725
- function_id: "mem::snapshot-create",
12726
- payload: req.body || {}
12727
- })
12728
- };
12729
- } catch {
12730
- return {
12731
- status_code: 404,
12732
- body: { error: "Snapshots not enabled" }
12733
- };
12734
- }
14201
+ const label = asNonEmptyString$1(req.query_params?.["label"]);
14202
+ if (!label) return {
14203
+ status_code: 400,
14204
+ body: { error: "label query param required" }
14205
+ };
14206
+ const result = await sdk.trigger({
14207
+ function_id: "mem::slot-get",
14208
+ payload: { label }
14209
+ });
14210
+ const resp = result;
14211
+ if (resp?.success === false) return {
14212
+ status_code: resp.error?.includes("not found") ? 404 : 400,
14213
+ body: resp
14214
+ };
14215
+ return {
14216
+ status_code: 200,
14217
+ body: result
14218
+ };
12735
14219
  });
12736
14220
  sdk.registerTrigger({
12737
14221
  type: "http",
12738
- function_id: "api::snapshot-create",
14222
+ function_id: "api::slot-get",
12739
14223
  config: {
12740
- api_path: "/agentmemory/snapshot/create",
12741
- http_method: "POST"
14224
+ api_path: "/agentmemory/slot",
14225
+ http_method: "GET"
12742
14226
  }
12743
14227
  });
12744
- sdk.registerFunction("api::snapshot-restore", async (req) => {
14228
+ sdk.registerFunction("api::slot-create", async (req) => {
12745
14229
  const authErr = checkAuth(req, secret);
12746
14230
  if (authErr) return authErr;
12747
- if (!req.body?.commitHash) return {
14231
+ const body = req.body ?? {};
14232
+ const label = asNonEmptyString$1(body["label"]);
14233
+ if (!label) return {
12748
14234
  status_code: 400,
12749
- body: { error: "commitHash is required" }
14235
+ body: { error: "label required" }
14236
+ };
14237
+ if (body["content"] !== void 0 && typeof body["content"] !== "string") return {
14238
+ status_code: 400,
14239
+ body: { error: "content must be a string" }
14240
+ };
14241
+ if (body["description"] !== void 0 && typeof body["description"] !== "string") return {
14242
+ status_code: 400,
14243
+ body: { error: "description must be a string" }
14244
+ };
14245
+ if (body["pinned"] !== void 0 && typeof body["pinned"] !== "boolean") return {
14246
+ status_code: 400,
14247
+ body: { error: "pinned must be a boolean" }
14248
+ };
14249
+ if (body["scope"] !== void 0 && body["scope"] !== "project" && body["scope"] !== "global") return {
14250
+ status_code: 400,
14251
+ body: { error: "scope must be 'project' or 'global'" }
14252
+ };
14253
+ const sizeLimit = parseOptionalPositiveInt(body["sizeLimit"]);
14254
+ if (sizeLimit === null) return {
14255
+ status_code: 400,
14256
+ body: { error: "sizeLimit must be a positive integer" }
14257
+ };
14258
+ if (sizeLimit !== void 0 && sizeLimit > 2e4) return {
14259
+ status_code: 400,
14260
+ body: { error: "sizeLimit must be <= 20000" }
14261
+ };
14262
+ const payload = { label };
14263
+ if (typeof body["content"] === "string") payload["content"] = body["content"];
14264
+ if (typeof body["description"] === "string") payload["description"] = body["description"];
14265
+ if (sizeLimit !== void 0) payload["sizeLimit"] = sizeLimit;
14266
+ if (typeof body["pinned"] === "boolean") payload["pinned"] = body["pinned"];
14267
+ if (body["scope"] === "project" || body["scope"] === "global") payload["scope"] = body["scope"];
14268
+ const result = await sdk.trigger({
14269
+ function_id: "mem::slot-create",
14270
+ payload
14271
+ });
14272
+ const resp = result;
14273
+ if (resp?.success === false) return {
14274
+ status_code: resp.error?.includes("exists") ? 409 : 400,
14275
+ body: resp
14276
+ };
14277
+ return {
14278
+ status_code: 201,
14279
+ body: result
12750
14280
  };
12751
- try {
12752
- return {
12753
- status_code: 200,
12754
- body: await sdk.trigger({
12755
- function_id: "mem::snapshot-restore",
12756
- payload: req.body
12757
- })
12758
- };
12759
- } catch {
12760
- return {
12761
- status_code: 404,
12762
- body: { error: "Snapshots not enabled" }
12763
- };
12764
- }
12765
14281
  });
12766
14282
  sdk.registerTrigger({
12767
14283
  type: "http",
12768
- function_id: "api::snapshot-restore",
14284
+ function_id: "api::slot-create",
12769
14285
  config: {
12770
- api_path: "/agentmemory/snapshot/restore",
14286
+ api_path: "/agentmemory/slot",
12771
14287
  http_method: "POST"
12772
14288
  }
12773
14289
  });
12774
- sdk.registerFunction("api::memories", async (req) => {
14290
+ sdk.registerFunction("api::slot-append", async (req) => {
12775
14291
  const authErr = checkAuth(req, secret);
12776
14292
  if (authErr) return authErr;
12777
- const memories = await kv.list(KV.memories);
14293
+ const body = req.body ?? {};
14294
+ const label = asNonEmptyString$1(body["label"]);
14295
+ const text = typeof body["text"] === "string" ? body["text"] : null;
14296
+ if (!label || !text) return {
14297
+ status_code: 400,
14298
+ body: { error: "label and text required" }
14299
+ };
14300
+ const result = await sdk.trigger({
14301
+ function_id: "mem::slot-append",
14302
+ payload: {
14303
+ label,
14304
+ text
14305
+ }
14306
+ });
14307
+ const resp = result;
14308
+ if (resp?.success === false) {
14309
+ const notFound = resp.error?.includes("not found");
14310
+ const overLimit = resp.error?.includes("exceed");
14311
+ return {
14312
+ status_code: notFound ? 404 : overLimit ? 413 : 400,
14313
+ body: resp
14314
+ };
14315
+ }
12778
14316
  return {
12779
14317
  status_code: 200,
12780
- body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
14318
+ body: result
12781
14319
  };
12782
14320
  });
12783
14321
  sdk.registerTrigger({
12784
14322
  type: "http",
12785
- function_id: "api::memories",
14323
+ function_id: "api::slot-append",
12786
14324
  config: {
12787
- api_path: "/agentmemory/memories",
12788
- http_method: "GET"
14325
+ api_path: "/agentmemory/slot/append",
14326
+ http_method: "POST"
12789
14327
  }
12790
14328
  });
12791
- sdk.registerFunction("api::semantic-list", async (req) => {
14329
+ sdk.registerFunction("api::slot-replace", async (req) => {
12792
14330
  const authErr = checkAuth(req, secret);
12793
14331
  if (authErr) return authErr;
14332
+ const body = req.body ?? {};
14333
+ const label = asNonEmptyString$1(body["label"]);
14334
+ const content = body["content"];
14335
+ if (!label || typeof content !== "string") return {
14336
+ status_code: 400,
14337
+ body: { error: "label and content (string) required" }
14338
+ };
14339
+ const result = await sdk.trigger({
14340
+ function_id: "mem::slot-replace",
14341
+ payload: {
14342
+ label,
14343
+ content
14344
+ }
14345
+ });
14346
+ const resp = result;
14347
+ if (resp?.success === false) {
14348
+ const notFound = resp.error?.includes("not found");
14349
+ const overLimit = resp.error?.includes("exceed");
14350
+ return {
14351
+ status_code: notFound ? 404 : overLimit ? 413 : 400,
14352
+ body: resp
14353
+ };
14354
+ }
12794
14355
  return {
12795
14356
  status_code: 200,
12796
- body: { semantic: await kv.list(KV.semantic) }
14357
+ body: result
12797
14358
  };
12798
14359
  });
12799
14360
  sdk.registerTrigger({
12800
14361
  type: "http",
12801
- function_id: "api::semantic-list",
14362
+ function_id: "api::slot-replace",
12802
14363
  config: {
12803
- api_path: "/agentmemory/semantic",
12804
- http_method: "GET"
14364
+ api_path: "/agentmemory/slot/replace",
14365
+ http_method: "POST"
12805
14366
  }
12806
14367
  });
12807
- sdk.registerFunction("api::procedural-list", async (req) => {
14368
+ sdk.registerFunction("api::slot-delete", async (req) => {
12808
14369
  const authErr = checkAuth(req, secret);
12809
14370
  if (authErr) return authErr;
14371
+ const label = asNonEmptyString$1(req.query_params?.["label"]);
14372
+ if (!label) return {
14373
+ status_code: 400,
14374
+ body: { error: "label query param required" }
14375
+ };
14376
+ const result = await sdk.trigger({
14377
+ function_id: "mem::slot-delete",
14378
+ payload: { label }
14379
+ });
14380
+ const resp = result;
14381
+ if (resp?.success === false) return {
14382
+ status_code: resp.error?.includes("not found") ? 404 : 400,
14383
+ body: resp
14384
+ };
12810
14385
  return {
12811
14386
  status_code: 200,
12812
- body: { procedural: await kv.list(KV.procedural) }
14387
+ body: result
12813
14388
  };
12814
14389
  });
12815
14390
  sdk.registerTrigger({
12816
14391
  type: "http",
12817
- function_id: "api::procedural-list",
14392
+ function_id: "api::slot-delete",
12818
14393
  config: {
12819
- api_path: "/agentmemory/procedural",
12820
- http_method: "GET"
14394
+ api_path: "/agentmemory/slot",
14395
+ http_method: "DELETE"
12821
14396
  }
12822
14397
  });
12823
- sdk.registerFunction("api::relations-list", async (req) => {
14398
+ sdk.registerFunction("api::slot-reflect", async (req) => {
12824
14399
  const authErr = checkAuth(req, secret);
12825
14400
  if (authErr) return authErr;
14401
+ const body = req.body ?? {};
14402
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14403
+ if (!sessionId) return {
14404
+ status_code: 400,
14405
+ body: { error: "sessionId required" }
14406
+ };
14407
+ const maxObservations = parseOptionalPositiveInt(body["maxObservations"]);
14408
+ if (maxObservations === null) return {
14409
+ status_code: 400,
14410
+ body: { error: "maxObservations must be a positive integer" }
14411
+ };
14412
+ const payload = { sessionId };
14413
+ if (maxObservations !== void 0) payload["maxObservations"] = maxObservations;
12826
14414
  return {
12827
14415
  status_code: 200,
12828
- body: { relations: await kv.list(KV.relations) }
14416
+ body: await sdk.trigger({
14417
+ function_id: "mem::slot-reflect",
14418
+ payload
14419
+ })
12829
14420
  };
12830
14421
  });
12831
14422
  sdk.registerTrigger({
12832
14423
  type: "http",
12833
- function_id: "api::relations-list",
14424
+ function_id: "api::slot-reflect",
12834
14425
  config: {
12835
- api_path: "/agentmemory/relations",
12836
- http_method: "GET"
14426
+ api_path: "/agentmemory/slot/reflect",
14427
+ http_method: "POST"
12837
14428
  }
12838
14429
  });
12839
14430
  sdk.registerFunction("api::action-create", async (req) => {
@@ -13070,9 +14661,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13070
14661
  sdk.registerFunction("api::routine-create", async (req) => {
13071
14662
  const authErr = checkAuth(req, secret);
13072
14663
  if (authErr) return authErr;
13073
- if (!req.body?.name) return {
14664
+ if (!req.body?.name || !req.body?.steps) return {
13074
14665
  status_code: 400,
13075
- body: { error: "name is required" }
14666
+ body: { error: "name and steps are required" }
13076
14667
  };
13077
14668
  return {
13078
14669
  status_code: 201,
@@ -14321,10 +15912,21 @@ function registerEventTriggers(sdk, kv) {
14321
15912
  function_id: "event::observation",
14322
15913
  config: { topic: "agentmemory.observation" }
14323
15914
  });
14324
- sdk.registerFunction("event::session::stopped", async (data) => sdk.trigger({
14325
- function_id: "mem::summarize",
14326
- payload: data
14327
- }));
15915
+ sdk.registerFunction("event::session::stopped", async (data) => {
15916
+ const summary = await sdk.trigger({
15917
+ function_id: "mem::summarize",
15918
+ payload: data
15919
+ });
15920
+ if (isReflectEnabled()) try {
15921
+ sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
15922
+ } catch (err) {
15923
+ logger.warn("slot-reflect triggerVoid failed", {
15924
+ sessionId: data.sessionId,
15925
+ error: err instanceof Error ? err.message : String(err)
15926
+ });
15927
+ }
15928
+ return summary;
15929
+ });
14328
15930
  sdk.registerTrigger({
14329
15931
  type: "durable:subscriber",
14330
15932
  function_id: "event::session::stopped",
@@ -14576,6 +16178,34 @@ function registerMcpEndpoints(sdk, kv, secret) {
14576
16178
  }] }
14577
16179
  };
14578
16180
  }
16181
+ case "memory_vision_search": {
16182
+ const queryText = typeof args.queryText === "string" ? args.queryText : void 0;
16183
+ const queryImageRef = typeof args.queryImageRef === "string" ? args.queryImageRef : void 0;
16184
+ const queryImageBase64 = typeof args.queryImageBase64 === "string" ? args.queryImageBase64 : void 0;
16185
+ if (!queryText && !queryImageRef && !queryImageBase64) return {
16186
+ status_code: 400,
16187
+ body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
16188
+ };
16189
+ const topK = Math.max(1, Math.min(50, asNumber(args.topK, 10) ?? 10));
16190
+ const sessionId = typeof args.sessionId === "string" ? args.sessionId : void 0;
16191
+ const result = await sdk.trigger({
16192
+ function_id: "mem::vision-search",
16193
+ payload: {
16194
+ queryText,
16195
+ queryImageRef,
16196
+ queryImageBase64,
16197
+ topK,
16198
+ sessionId
16199
+ }
16200
+ });
16201
+ return {
16202
+ status_code: 200,
16203
+ body: { content: [{
16204
+ type: "text",
16205
+ text: JSON.stringify(result, null, 2)
16206
+ }] }
16207
+ };
16208
+ }
14579
16209
  case "memory_timeline": {
14580
16210
  if (typeof args.anchor !== "string" || !args.anchor.trim()) return {
14581
16211
  status_code: 400,
@@ -15441,6 +17071,123 @@ function registerMcpEndpoints(sdk, kv, secret) {
15441
17071
  }] }
15442
17072
  };
15443
17073
  }
17074
+ case "memory_slot_list": {
17075
+ const result = await sdk.trigger({
17076
+ function_id: "mem::slot-list",
17077
+ payload: {}
17078
+ });
17079
+ return {
17080
+ status_code: 200,
17081
+ body: { content: [{
17082
+ type: "text",
17083
+ text: JSON.stringify(result, null, 2)
17084
+ }] }
17085
+ };
17086
+ }
17087
+ case "memory_slot_get": {
17088
+ const label = asNonEmptyString(args.label);
17089
+ if (!label) return {
17090
+ status_code: 400,
17091
+ body: { error: "label required" }
17092
+ };
17093
+ const result = await sdk.trigger({
17094
+ function_id: "mem::slot-get",
17095
+ payload: { label }
17096
+ });
17097
+ return {
17098
+ status_code: 200,
17099
+ body: { content: [{
17100
+ type: "text",
17101
+ text: JSON.stringify(result, null, 2)
17102
+ }] }
17103
+ };
17104
+ }
17105
+ case "memory_slot_create": {
17106
+ const label = asNonEmptyString(args.label);
17107
+ if (!label) return {
17108
+ status_code: 400,
17109
+ body: { error: "label required" }
17110
+ };
17111
+ const payload = { label };
17112
+ if (typeof args.content === "string") payload.content = args.content;
17113
+ if (typeof args.description === "string") payload.description = args.description;
17114
+ if (typeof args.sizeLimit === "number") payload.sizeLimit = args.sizeLimit;
17115
+ if (args.pinned === false || args.pinned === "false") payload.pinned = false;
17116
+ else if (args.pinned === true || args.pinned === "true") payload.pinned = true;
17117
+ if (args.scope === "global" || args.scope === "project") payload.scope = args.scope;
17118
+ const result = await sdk.trigger({
17119
+ function_id: "mem::slot-create",
17120
+ payload
17121
+ });
17122
+ return {
17123
+ status_code: 200,
17124
+ body: { content: [{
17125
+ type: "text",
17126
+ text: JSON.stringify(result, null, 2)
17127
+ }] }
17128
+ };
17129
+ }
17130
+ case "memory_slot_append": {
17131
+ const label = asNonEmptyString(args.label);
17132
+ const text = typeof args.text === "string" ? args.text : null;
17133
+ if (!label || !text) return {
17134
+ status_code: 400,
17135
+ body: { error: "label and text required" }
17136
+ };
17137
+ const result = await sdk.trigger({
17138
+ function_id: "mem::slot-append",
17139
+ payload: {
17140
+ label,
17141
+ text
17142
+ }
17143
+ });
17144
+ return {
17145
+ status_code: 200,
17146
+ body: { content: [{
17147
+ type: "text",
17148
+ text: JSON.stringify(result, null, 2)
17149
+ }] }
17150
+ };
17151
+ }
17152
+ case "memory_slot_replace": {
17153
+ const label = asNonEmptyString(args.label);
17154
+ if (!label || typeof args.content !== "string") return {
17155
+ status_code: 400,
17156
+ body: { error: "label and content (string) required" }
17157
+ };
17158
+ const result = await sdk.trigger({
17159
+ function_id: "mem::slot-replace",
17160
+ payload: {
17161
+ label,
17162
+ content: args.content
17163
+ }
17164
+ });
17165
+ return {
17166
+ status_code: 200,
17167
+ body: { content: [{
17168
+ type: "text",
17169
+ text: JSON.stringify(result, null, 2)
17170
+ }] }
17171
+ };
17172
+ }
17173
+ case "memory_slot_delete": {
17174
+ const label = asNonEmptyString(args.label);
17175
+ if (!label) return {
17176
+ status_code: 400,
17177
+ body: { error: "label required" }
17178
+ };
17179
+ const result = await sdk.trigger({
17180
+ function_id: "mem::slot-delete",
17181
+ payload: { label }
17182
+ });
17183
+ return {
17184
+ status_code: 200,
17185
+ body: { content: [{
17186
+ type: "text",
17187
+ text: JSON.stringify(result, null, 2)
17188
+ }] }
17189
+ };
17190
+ }
15444
17191
  default: return {
15445
17192
  status_code: 400,
15446
17193
  body: { error: `Unknown tool: ${name}` }
@@ -16095,11 +17842,13 @@ async function main() {
16095
17842
  const fallbackConfig = loadFallbackConfig();
16096
17843
  const provider = fallbackConfig.providers.length > 0 ? createFallbackProvider(config.provider, fallbackConfig) : createProvider(config.provider);
16097
17844
  const embeddingProvider = createEmbeddingProvider();
17845
+ const imageEmbeddingProvider = createImageEmbeddingProvider();
16098
17846
  console.log(`[agentmemory] Starting worker v${VERSION}...`);
16099
17847
  console.log(`[agentmemory] Engine: ${config.engineUrl}`);
16100
17848
  console.log(`[agentmemory] Provider: ${config.provider.provider} (${config.provider.model})`);
16101
17849
  if (embeddingProvider) console.log(`[agentmemory] Embedding provider: ${embeddingProvider.name} (${embeddingProvider.dimensions} dims)`);
16102
17850
  else console.log(`[agentmemory] Embedding provider: none (BM25-only mode)`);
17851
+ if (imageEmbeddingProvider) console.log(`[agentmemory] Image embedding provider: ${imageEmbeddingProvider.name} (${imageEmbeddingProvider.dimensions} dims) — vision-search active`);
16103
17852
  console.log(`[agentmemory] REST API: http://localhost:${config.restPort}/agentmemory/*`);
16104
17853
  console.log(`[agentmemory] Streams: ws://localhost:${config.streamsPort}`);
16105
17854
  const sdk = registerWorker(config.engineUrl, {
@@ -16118,6 +17867,10 @@ async function main() {
16118
17867
  initMetrics(hasGetMeter(sdk) ? sdk.getMeter.bind(sdk) : void 0);
16119
17868
  registerPrivacyFunction(sdk);
16120
17869
  registerObserveFunction(sdk, kv, dedupMap, config.maxObservationsPerSession);
17870
+ registerImageQuotaCleanup(sdk, kv);
17871
+ registerVisionSearchFunctions(sdk, kv, imageEmbeddingProvider);
17872
+ if (isSlotsEnabled()) registerSlotsFunctions(sdk, kv);
17873
+ registerDiskSizeManager(sdk, kv);
16121
17874
  registerCompressFunction(sdk, kv, provider, metricsStore);
16122
17875
  registerSearchFunction(sdk, kv);
16123
17876
  registerContextFunction(sdk, kv, config.tokenBudget);
@@ -16184,6 +17937,7 @@ async function main() {
16184
17937
  registerReplayFunctions(sdk, kv);
16185
17938
  console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
16186
17939
  console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
17940
+ if (isSlotsEnabled()) console.log(`[agentmemory] Slots: enabled (pinned editable memory). Reflect on Stop hook: ${isReflectEnabled() ? "on" : "off"}`);
16187
17941
  const snapshotConfig = loadSnapshotConfig();
16188
17942
  if (snapshotConfig.enabled) {
16189
17943
  registerSnapshotFunction(sdk, kv, snapshotConfig.dir);
@@ -16221,7 +17975,7 @@ async function main() {
16221
17975
  }
16222
17976
  }
16223
17977
  console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
16224
- console.log(`[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`);
17978
+ console.log(`[agentmemory] Endpoints: 107 REST + ${getAllTools().length} MCP tools + 6 MCP resources + 3 MCP prompts`);
16225
17979
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
16226
17980
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
16227
17981
  const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);
@@ -16287,5 +18041,5 @@ main().catch((err) => {
16287
18041
  });
16288
18042
 
16289
18043
  //#endregion
16290
- export { };
16291
- //# sourceMappingURL=src-Dw_gJcCy.mjs.map
18044
+ export { deleteImage as a, saveImageToDisk as c, IMAGES_DIR as i, touchImage as l, getImageRefCount as n, getMaxBytes as o, incrementImageRef as r, isManagedImagePath as s, decrementImageRef as t };
18045
+ //# sourceMappingURL=src-3Snd7D3T.mjs.map