@agentmemory/agentmemory 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +42 -15
  2. package/dist/cli.mjs +60 -25
  3. package/dist/cli.mjs.map +1 -1
  4. package/dist/hooks/notification.mjs +6 -0
  5. package/dist/hooks/notification.mjs.map +1 -1
  6. package/dist/hooks/post-tool-failure.mjs +6 -0
  7. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  8. package/dist/hooks/post-tool-use.mjs +35 -1
  9. package/dist/hooks/post-tool-use.mjs.map +1 -1
  10. package/dist/hooks/pre-compact.mjs +6 -0
  11. package/dist/hooks/pre-compact.mjs.map +1 -1
  12. package/dist/hooks/pre-tool-use.mjs +6 -0
  13. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  14. package/dist/hooks/prompt-submit.mjs +6 -0
  15. package/dist/hooks/prompt-submit.mjs.map +1 -1
  16. package/dist/hooks/session-end.mjs +6 -0
  17. package/dist/hooks/session-end.mjs.map +1 -1
  18. package/dist/hooks/session-start.mjs +6 -0
  19. package/dist/hooks/session-start.mjs.map +1 -1
  20. package/dist/hooks/stop.mjs +6 -0
  21. package/dist/hooks/stop.mjs.map +1 -1
  22. package/dist/hooks/subagent-start.mjs +6 -0
  23. package/dist/hooks/subagent-start.mjs.map +1 -1
  24. package/dist/hooks/subagent-stop.mjs +6 -0
  25. package/dist/hooks/subagent-stop.mjs.map +1 -1
  26. package/dist/hooks/task-completed.mjs +6 -0
  27. package/dist/hooks/task-completed.mjs.map +1 -1
  28. package/dist/image-refs-Dq5wcV-a.mjs +3 -0
  29. package/dist/image-store-BLOkD0xV.mjs +3 -0
  30. package/dist/index.mjs +2054 -144
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/{src-B3pEsBSb.mjs → src-tmuZyobT.mjs} +1974 -253
  33. package/dist/src-tmuZyobT.mjs.map +1 -0
  34. package/dist/{standalone-DXc-BEqr.mjs → standalone-BiwX0rdC.mjs} +2 -2
  35. package/dist/{standalone-DXc-BEqr.mjs.map → standalone-BiwX0rdC.mjs.map} +1 -1
  36. package/dist/standalone.mjs +136 -2
  37. package/dist/standalone.mjs.map +1 -1
  38. package/dist/{tools-registry-DXIK5CxQ.mjs → tools-registry-CHH84gIQ.mjs} +166 -12
  39. package/dist/tools-registry-CHH84gIQ.mjs.map +1 -0
  40. package/dist/viewer/index.html +249 -62
  41. package/package.json +5 -3
  42. package/plugin/.claude-plugin/plugin.json +2 -2
  43. package/plugin/scripts/notification.mjs +6 -0
  44. package/plugin/scripts/notification.mjs.map +1 -1
  45. package/plugin/scripts/post-tool-failure.mjs +6 -0
  46. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  47. package/plugin/scripts/post-tool-use.mjs +35 -1
  48. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  49. package/plugin/scripts/pre-compact.mjs +6 -0
  50. package/plugin/scripts/pre-compact.mjs.map +1 -1
  51. package/plugin/scripts/pre-tool-use.mjs +6 -0
  52. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  53. package/plugin/scripts/prompt-submit.mjs +6 -0
  54. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  55. package/plugin/scripts/session-end.mjs +6 -0
  56. package/plugin/scripts/session-end.mjs.map +1 -1
  57. package/plugin/scripts/session-start.mjs +6 -0
  58. package/plugin/scripts/session-start.mjs.map +1 -1
  59. package/plugin/scripts/stop.mjs +6 -0
  60. package/plugin/scripts/stop.mjs.map +1 -1
  61. package/plugin/scripts/subagent-start.mjs +6 -0
  62. package/plugin/scripts/subagent-start.mjs.map +1 -1
  63. package/plugin/scripts/subagent-stop.mjs +6 -0
  64. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  65. package/plugin/scripts/task-completed.mjs +6 -0
  66. package/plugin/scripts/task-completed.mjs.map +1 -1
  67. package/dist/src-B3pEsBSb.mjs.map +0 -1
  68. package/dist/tools-registry-DXIK5CxQ.mjs.map +0 -1
  69. package/dist/transformers-BX_tgxdO.mjs +0 -38684
  70. package/dist/transformers-BX_tgxdO.mjs.map +0 -1
  71. package/dist/transformers-KMm1i9no.mjs +0 -38683
  72. package/dist/transformers-KMm1i9no.mjs.map +0 -1
@@ -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-DXIK5CxQ.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-CHH84gIQ.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-BLOkD0xV.mjs");
2511
+ const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
2512
+ raw.imageData = filePath;
2513
+ const { incrementImageRef } = await import("./image-refs-Dq5wcV-a.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-BLOkD0xV.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-Dq5wcV-a.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-Dq5wcV-a.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-Dq5wcV-a.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
  }
@@ -4384,7 +5447,9 @@ function registerExportImportFunction(sdk, kv) {
4384
5447
  "0.8.11",
4385
5448
  "0.8.12",
4386
5449
  "0.8.13",
4387
- "0.9.0"
5450
+ "0.9.0",
5451
+ "0.9.1",
5452
+ "0.9.2"
4388
5453
  ]).has(importData.version)) return {
4389
5454
  success: false,
4390
5455
  error: `Unsupported export version: ${importData.version}`
@@ -10741,6 +11806,7 @@ function registerRetentionFunctions(sdk, kv) {
10741
11806
  const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
10742
11807
  const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
10743
11808
  const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
11809
+ const { decrementImageRef } = await import("./image-refs-Dq5wcV-a.mjs");
10744
11810
  const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
10745
11811
  if (data?.dryRun) return {
10746
11812
  success: true,
@@ -10772,6 +11838,8 @@ function registerRetentionFunctions(sdk, kv) {
10772
11838
  resolvedSource = "semantic";
10773
11839
  }
10774
11840
  if (!scope || !resolvedSource) continue;
11841
+ const mem = await kv.get(scope, candidate.memoryId);
11842
+ if (mem && mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
10775
11843
  await kv.delete(scope, candidate.memoryId);
10776
11844
  await kv.delete(KV.retentionScores, candidate.memoryId);
10777
11845
  await deleteAccessLog(kv, candidate.memoryId);
@@ -11209,6 +12277,92 @@ function rawFromCompressed(obs) {
11209
12277
  }
11210
12278
  };
11211
12279
  }
12280
+ 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];
12281
+ async function deriveCrystalAndLessons(kv, sessionId, project, rawObs, compressed, firstPrompt) {
12282
+ if (rawObs.length === 0) return;
12283
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
12284
+ const files = /* @__PURE__ */ new Set();
12285
+ const tools = /* @__PURE__ */ new Set();
12286
+ for (const c of compressed) {
12287
+ for (const f of c.files || []) files.add(f);
12288
+ if (c.type && c.type !== "conversation" && c.title) tools.add(c.title);
12289
+ }
12290
+ const assistantTexts = [];
12291
+ const userPrompts = [];
12292
+ for (const r of rawObs) {
12293
+ if (typeof r.assistantResponse === "string" && r.assistantResponse.trim()) assistantTexts.push(r.assistantResponse);
12294
+ if (typeof r.userPrompt === "string" && r.userPrompt.trim()) userPrompts.push(r.userPrompt);
12295
+ }
12296
+ const lessonMatches = /* @__PURE__ */ new Map();
12297
+ for (const text of assistantTexts.concat(userPrompts).slice(0, 200)) for (const pat of LESSON_PATTERNS) {
12298
+ pat.lastIndex = 0;
12299
+ let m;
12300
+ while ((m = pat.exec(text)) !== null && lessonMatches.size < 40) {
12301
+ const snippet = m[0].replace(/\s+/g, " ").trim();
12302
+ if (snippet.length >= 20 && snippet.length <= 220) {
12303
+ const key = snippet.toLowerCase();
12304
+ if (!lessonMatches.has(key)) lessonMatches.set(key, snippet);
12305
+ }
12306
+ }
12307
+ }
12308
+ const lessonEntries = Array.from(lessonMatches.values()).slice(0, 20);
12309
+ const lessonIds = [];
12310
+ for (const content of lessonEntries) {
12311
+ const lessonId = fingerprintId("lesson", content.trim().toLowerCase());
12312
+ try {
12313
+ const existing = await kv.get(KV.lessons, lessonId);
12314
+ if (existing) {
12315
+ const existingSources = existing.sourceIds || [];
12316
+ const mergedSources = existingSources.includes(sessionId) ? existingSources : [...existingSources, sessionId];
12317
+ const existingTags = existing.tags || [];
12318
+ const mergedTags = existingTags.includes("auto-import") ? existingTags : [...existingTags, "auto-import"];
12319
+ const merged = {
12320
+ ...existing,
12321
+ sourceIds: mergedSources,
12322
+ tags: mergedTags,
12323
+ reinforcements: (existing.reinforcements || 0) + 1,
12324
+ updatedAt: createdAt,
12325
+ lastReinforcedAt: createdAt
12326
+ };
12327
+ await kv.set(KV.lessons, lessonId, merged);
12328
+ } else {
12329
+ const lesson = {
12330
+ id: lessonId,
12331
+ content,
12332
+ context: firstPrompt || project,
12333
+ confidence: .4,
12334
+ reinforcements: 0,
12335
+ source: "consolidation",
12336
+ sourceIds: [sessionId],
12337
+ project,
12338
+ tags: ["auto-import"],
12339
+ createdAt,
12340
+ updatedAt: createdAt,
12341
+ decayRate: .05
12342
+ };
12343
+ await kv.set(KV.lessons, lessonId, lesson);
12344
+ }
12345
+ lessonIds.push(lessonId);
12346
+ } catch {}
12347
+ }
12348
+ const crystalId = fingerprintId("crystal", sessionId);
12349
+ const narrativePreview = firstPrompt ? firstPrompt.slice(0, 300) : compressed.slice(0, 5).map((c) => c.narrative || c.title).filter(Boolean).join(" · ").slice(0, 300);
12350
+ try {
12351
+ const existingCrystal = await kv.get(KV.crystals, crystalId);
12352
+ const crystal = {
12353
+ id: crystalId,
12354
+ narrative: narrativePreview || `Session ${sessionId.slice(0, 12)} (${rawObs.length} observations)`,
12355
+ keyOutcomes: Array.from(tools).slice(0, 8),
12356
+ filesAffected: Array.from(files).slice(0, 20),
12357
+ lessons: lessonIds,
12358
+ sourceActionIds: existingCrystal?.sourceActionIds ?? [],
12359
+ sessionId,
12360
+ project,
12361
+ createdAt: existingCrystal?.createdAt ?? createdAt
12362
+ };
12363
+ await kv.set(KV.crystals, crystalId, crystal);
12364
+ } catch {}
12365
+ }
11212
12366
  function isRawShape(o) {
11213
12367
  if (!o || typeof o !== "object") return false;
11214
12368
  return typeof o.hookType === "string";
@@ -11319,6 +12473,8 @@ function registerReplayFunctions(sdk, kv) {
11319
12473
  }
11320
12474
  const parsed = parseJsonlText(text, generateId("sess"));
11321
12475
  if (parsed.observations.length === 0) continue;
12476
+ const firstPromptObs = parsed.observations.find((o) => typeof o.userPrompt === "string" && o.userPrompt.trim().length > 0);
12477
+ const firstPrompt = firstPromptObs?.userPrompt ? firstPromptObs.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
11322
12478
  const existing = await kv.get(KV.sessions, parsed.sessionId);
11323
12479
  if (existing) {
11324
12480
  existing.observationCount = (existing.observationCount || 0) + parsed.observations.length;
@@ -11326,6 +12482,7 @@ function registerReplayFunctions(sdk, kv) {
11326
12482
  if (existing.status === "active") existing.status = "completed";
11327
12483
  const existingTags = existing.tags || [];
11328
12484
  if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
12485
+ if (!existing.firstPrompt && firstPrompt) existing.firstPrompt = firstPrompt;
11329
12486
  await kv.set(KV.sessions, existing.id, existing);
11330
12487
  } else {
11331
12488
  const session = {
@@ -11336,13 +12493,22 @@ function registerReplayFunctions(sdk, kv) {
11336
12493
  endedAt: parsed.endedAt,
11337
12494
  status: "completed",
11338
12495
  observationCount: parsed.observations.length,
11339
- tags: ["jsonl-import"]
12496
+ tags: ["jsonl-import"],
12497
+ firstPrompt
11340
12498
  };
11341
12499
  await kv.set(KV.sessions, session.id, session);
11342
12500
  }
11343
- await Promise.all(parsed.observations.map((obs) => kv.set(KV.observations(parsed.sessionId), obs.id, obs)));
12501
+ const searchIndex = getSearchIndex();
12502
+ const compressed = [];
12503
+ await Promise.all(parsed.observations.map(async (obs) => {
12504
+ const synthetic = buildSyntheticCompression(obs);
12505
+ compressed.push(synthetic);
12506
+ await kv.set(KV.observations(parsed.sessionId), obs.id, synthetic);
12507
+ searchIndex.add(synthetic);
12508
+ }));
11344
12509
  observationCount += parsed.observations.length;
11345
12510
  sessionIds.push(parsed.sessionId);
12511
+ await deriveCrystalAndLessons(kv, parsed.sessionId, parsed.project, parsed.observations, compressed, firstPrompt);
11346
12512
  }
11347
12513
  await safeAudit(kv, "import", "mem::replay::import-jsonl", sessionIds, {
11348
12514
  source: "jsonl",
@@ -11376,6 +12542,7 @@ function evaluateHealth(snapshot, config = {}) {
11376
12542
  ...config
11377
12543
  };
11378
12544
  const alerts = [];
12545
+ const notes = [];
11379
12546
  let critical = false;
11380
12547
  let degraded = false;
11381
12548
  if (snapshot.connectionState === "disconnected" || snapshot.connectionState === "failed") {
@@ -11409,10 +12576,11 @@ function evaluateHealth(snapshot, config = {}) {
11409
12576
  } else if (memPercent > cfg.memoryWarnPercent && rssAboveFloor) {
11410
12577
  alerts.push(`memory_warn_${Math.round(memPercent)}%_rss${memMb}mb`);
11411
12578
  degraded = true;
11412
- } else if (memPercent > cfg.memoryWarnPercent) alerts.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
12579
+ } else if (memPercent > cfg.memoryWarnPercent) notes.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
11413
12580
  return {
11414
12581
  status: critical ? "critical" : degraded ? "degraded" : "healthy",
11415
- alerts
12582
+ alerts,
12583
+ notes
11416
12584
  };
11417
12585
  }
11418
12586
 
@@ -11489,6 +12657,7 @@ function registerHealthMonitor(sdk, kv) {
11489
12657
  const evaluated = evaluateHealth(snapshot);
11490
12658
  snapshot.status = evaluated.status;
11491
12659
  snapshot.alerts = evaluated.alerts;
12660
+ snapshot.notes = evaluated.notes;
11492
12661
  await kv.set(KV.health, "latest", snapshot).catch(() => {});
11493
12662
  return snapshot;
11494
12663
  }
@@ -11531,6 +12700,7 @@ function buildViewerCsp(nonce) {
11531
12700
 
11532
12701
  //#endregion
11533
12702
  //#region src/viewer/document.ts
12703
+ const VIEWER_VERSION_PLACEHOLDER = "__AGENTMEMORY_VERSION__";
11534
12704
  function loadViewerTemplate() {
11535
12705
  const base = dirname(fileURLToPath(import.meta.url));
11536
12706
  const candidates = [
@@ -11549,7 +12719,7 @@ function renderViewerDocument() {
11549
12719
  const nonce = createViewerNonce();
11550
12720
  return {
11551
12721
  found: true,
11552
- html: template.replaceAll(VIEWER_NONCE_PLACEHOLDER, nonce),
12722
+ html: template.replaceAll(VIEWER_NONCE_PLACEHOLDER, nonce).replaceAll(VIEWER_VERSION_PLACEHOLDER, VERSION),
11553
12723
  csp: buildViewerCsp(nonce)
11554
12724
  };
11555
12725
  }
@@ -11831,9 +13001,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
11831
13001
  sdk.registerFunction("api::replay::sessions", async (req) => {
11832
13002
  const authErr = checkAuth(req, secret);
11833
13003
  if (authErr) return authErr;
13004
+ const sessions = await kv.list(KV.sessions);
13005
+ sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
11834
13006
  return {
11835
13007
  status_code: 200,
11836
- body: await sdk.trigger({ function_id: "mem::replay::sessions" })
13008
+ body: {
13009
+ success: true,
13010
+ sessions
13011
+ }
11837
13012
  };
11838
13013
  });
11839
13014
  sdk.registerTrigger({
@@ -12616,174 +13791,557 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
12616
13791
  });
12617
13792
  sdk.registerTrigger({
12618
13793
  type: "http",
12619
- function_id: "api::team-profile",
13794
+ function_id: "api::team-profile",
13795
+ config: {
13796
+ api_path: "/agentmemory/team/profile",
13797
+ http_method: "GET"
13798
+ }
13799
+ });
13800
+ sdk.registerFunction("api::audit", async (req) => {
13801
+ const authErr = checkAuth(req, secret);
13802
+ if (authErr) return authErr;
13803
+ const parsedLimit = parseOptionalInt(req.query_params?.["limit"]);
13804
+ return {
13805
+ status_code: 200,
13806
+ body: {
13807
+ entries: await sdk.trigger({
13808
+ function_id: "mem::audit-query",
13809
+ payload: {
13810
+ operation: req.query_params?.["operation"],
13811
+ limit: parsedLimit ?? 50
13812
+ }
13813
+ }),
13814
+ success: true
13815
+ }
13816
+ };
13817
+ });
13818
+ sdk.registerTrigger({
13819
+ type: "http",
13820
+ function_id: "api::audit",
13821
+ config: {
13822
+ api_path: "/agentmemory/audit",
13823
+ http_method: "GET"
13824
+ }
13825
+ });
13826
+ sdk.registerFunction("api::governance-delete", async (req) => {
13827
+ const authErr = checkAuth(req, secret);
13828
+ if (authErr) return authErr;
13829
+ if (!req.body?.memoryIds || !Array.isArray(req.body.memoryIds)) return {
13830
+ status_code: 400,
13831
+ body: { error: "memoryIds array is required" }
13832
+ };
13833
+ return {
13834
+ status_code: 200,
13835
+ body: await sdk.trigger({
13836
+ function_id: "mem::governance-delete",
13837
+ payload: req.body
13838
+ })
13839
+ };
13840
+ });
13841
+ sdk.registerTrigger({
13842
+ type: "http",
13843
+ function_id: "api::governance-delete",
13844
+ config: {
13845
+ api_path: "/agentmemory/governance/memories",
13846
+ http_method: "DELETE"
13847
+ }
13848
+ });
13849
+ sdk.registerFunction("api::governance-bulk", async (req) => {
13850
+ const authErr = checkAuth(req, secret);
13851
+ if (authErr) return authErr;
13852
+ return {
13853
+ status_code: 200,
13854
+ body: await sdk.trigger({
13855
+ function_id: "mem::governance-bulk",
13856
+ payload: req.body || {}
13857
+ })
13858
+ };
13859
+ });
13860
+ sdk.registerTrigger({
13861
+ type: "http",
13862
+ function_id: "api::governance-bulk",
13863
+ config: {
13864
+ api_path: "/agentmemory/governance/bulk-delete",
13865
+ http_method: "POST"
13866
+ }
13867
+ });
13868
+ sdk.registerFunction("api::snapshots", async (req) => {
13869
+ const authErr = checkAuth(req, secret);
13870
+ if (authErr) return authErr;
13871
+ try {
13872
+ return {
13873
+ status_code: 200,
13874
+ body: await sdk.trigger({
13875
+ function_id: "mem::snapshot-list",
13876
+ payload: {}
13877
+ })
13878
+ };
13879
+ } catch {
13880
+ return {
13881
+ status_code: 404,
13882
+ body: { error: "Snapshots not enabled" }
13883
+ };
13884
+ }
13885
+ });
13886
+ sdk.registerTrigger({
13887
+ type: "http",
13888
+ function_id: "api::snapshots",
13889
+ config: {
13890
+ api_path: "/agentmemory/snapshots",
13891
+ http_method: "GET"
13892
+ }
13893
+ });
13894
+ sdk.registerFunction("api::snapshot-create", async (req) => {
13895
+ const authErr = checkAuth(req, secret);
13896
+ if (authErr) return authErr;
13897
+ try {
13898
+ return {
13899
+ status_code: 201,
13900
+ body: await sdk.trigger({
13901
+ function_id: "mem::snapshot-create",
13902
+ payload: req.body || {}
13903
+ })
13904
+ };
13905
+ } catch {
13906
+ return {
13907
+ status_code: 404,
13908
+ body: { error: "Snapshots not enabled" }
13909
+ };
13910
+ }
13911
+ });
13912
+ sdk.registerTrigger({
13913
+ type: "http",
13914
+ function_id: "api::snapshot-create",
13915
+ config: {
13916
+ api_path: "/agentmemory/snapshot/create",
13917
+ http_method: "POST"
13918
+ }
13919
+ });
13920
+ sdk.registerFunction("api::snapshot-restore", async (req) => {
13921
+ const authErr = checkAuth(req, secret);
13922
+ if (authErr) return authErr;
13923
+ if (!req.body?.commitHash) return {
13924
+ status_code: 400,
13925
+ body: { error: "commitHash is required" }
13926
+ };
13927
+ try {
13928
+ return {
13929
+ status_code: 200,
13930
+ body: await sdk.trigger({
13931
+ function_id: "mem::snapshot-restore",
13932
+ payload: req.body
13933
+ })
13934
+ };
13935
+ } catch {
13936
+ return {
13937
+ status_code: 404,
13938
+ body: { error: "Snapshots not enabled" }
13939
+ };
13940
+ }
13941
+ });
13942
+ sdk.registerTrigger({
13943
+ type: "http",
13944
+ function_id: "api::snapshot-restore",
13945
+ config: {
13946
+ api_path: "/agentmemory/snapshot/restore",
13947
+ http_method: "POST"
13948
+ }
13949
+ });
13950
+ sdk.registerFunction("api::memories", async (req) => {
13951
+ const authErr = checkAuth(req, secret);
13952
+ if (authErr) return authErr;
13953
+ const memories = await kv.list(KV.memories);
13954
+ return {
13955
+ status_code: 200,
13956
+ body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
13957
+ };
13958
+ });
13959
+ sdk.registerTrigger({
13960
+ type: "http",
13961
+ function_id: "api::memories",
13962
+ config: {
13963
+ api_path: "/agentmemory/memories",
13964
+ http_method: "GET"
13965
+ }
13966
+ });
13967
+ sdk.registerFunction("api::semantic-list", async (req) => {
13968
+ const authErr = checkAuth(req, secret);
13969
+ if (authErr) return authErr;
13970
+ return {
13971
+ status_code: 200,
13972
+ body: { semantic: await kv.list(KV.semantic) }
13973
+ };
13974
+ });
13975
+ sdk.registerTrigger({
13976
+ type: "http",
13977
+ function_id: "api::semantic-list",
13978
+ config: {
13979
+ api_path: "/agentmemory/semantic",
13980
+ http_method: "GET"
13981
+ }
13982
+ });
13983
+ sdk.registerFunction("api::procedural-list", async (req) => {
13984
+ const authErr = checkAuth(req, secret);
13985
+ if (authErr) return authErr;
13986
+ return {
13987
+ status_code: 200,
13988
+ body: { procedural: await kv.list(KV.procedural) }
13989
+ };
13990
+ });
13991
+ sdk.registerTrigger({
13992
+ type: "http",
13993
+ function_id: "api::procedural-list",
13994
+ config: {
13995
+ api_path: "/agentmemory/procedural",
13996
+ http_method: "GET"
13997
+ }
13998
+ });
13999
+ sdk.registerFunction("api::relations-list", async (req) => {
14000
+ const authErr = checkAuth(req, secret);
14001
+ if (authErr) return authErr;
14002
+ return {
14003
+ status_code: 200,
14004
+ body: { relations: await kv.list(KV.relations) }
14005
+ };
14006
+ });
14007
+ sdk.registerTrigger({
14008
+ type: "http",
14009
+ function_id: "api::relations-list",
14010
+ config: {
14011
+ api_path: "/agentmemory/relations",
14012
+ http_method: "GET"
14013
+ }
14014
+ });
14015
+ sdk.registerFunction("api::vision-search", async (req) => {
14016
+ const authErr = checkAuth(req, secret);
14017
+ if (authErr) return authErr;
14018
+ const body = req.body ?? {};
14019
+ const queryText = asNonEmptyString$1(body["queryText"]);
14020
+ const queryImageRef = asNonEmptyString$1(body["queryImageRef"]);
14021
+ const queryImageBase64 = asNonEmptyString$1(body["queryImageBase64"]);
14022
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14023
+ if (!queryText && !queryImageRef && !queryImageBase64) return {
14024
+ status_code: 400,
14025
+ body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
14026
+ };
14027
+ const topKParsed = parseOptionalPositiveInt(body["topK"]);
14028
+ if (topKParsed === null) return {
14029
+ status_code: 400,
14030
+ body: { error: "topK must be a positive integer" }
14031
+ };
14032
+ const payload = {};
14033
+ if (queryText) payload["queryText"] = queryText;
14034
+ if (queryImageRef) payload["queryImageRef"] = queryImageRef;
14035
+ if (queryImageBase64) payload["queryImageBase64"] = queryImageBase64;
14036
+ if (sessionId) payload["sessionId"] = sessionId;
14037
+ if (topKParsed !== void 0) payload["topK"] = Math.min(50, topKParsed);
14038
+ const result = await sdk.trigger({
14039
+ function_id: "mem::vision-search",
14040
+ payload
14041
+ });
14042
+ const resp = result;
14043
+ if (resp?.success === false) return {
14044
+ status_code: resp.error?.includes("disabled") ? 503 : 400,
14045
+ body: resp
14046
+ };
14047
+ return {
14048
+ status_code: 200,
14049
+ body: result
14050
+ };
14051
+ });
14052
+ sdk.registerTrigger({
14053
+ type: "http",
14054
+ function_id: "api::vision-search",
14055
+ config: {
14056
+ api_path: "/agentmemory/vision-search",
14057
+ http_method: "POST"
14058
+ }
14059
+ });
14060
+ sdk.registerFunction("api::vision-embed", async (req) => {
14061
+ const authErr = checkAuth(req, secret);
14062
+ if (authErr) return authErr;
14063
+ const body = req.body ?? {};
14064
+ const imageRef = asNonEmptyString$1(body["imageRef"]);
14065
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14066
+ const observationId = asNonEmptyString$1(body["observationId"]);
14067
+ if (!imageRef) return {
14068
+ status_code: 400,
14069
+ body: { error: "imageRef is required" }
14070
+ };
14071
+ const payload = { imageRef };
14072
+ if (sessionId) payload["sessionId"] = sessionId;
14073
+ if (observationId) payload["observationId"] = observationId;
14074
+ const result = await sdk.trigger({
14075
+ function_id: "mem::vision-embed",
14076
+ payload
14077
+ });
14078
+ const resp = result;
14079
+ if (resp?.success === false) return {
14080
+ status_code: resp.error?.includes("disabled") ? 503 : 400,
14081
+ body: resp
14082
+ };
14083
+ return {
14084
+ status_code: 200,
14085
+ body: result
14086
+ };
14087
+ });
14088
+ sdk.registerTrigger({
14089
+ type: "http",
14090
+ function_id: "api::vision-embed",
12620
14091
  config: {
12621
- api_path: "/agentmemory/team/profile",
12622
- http_method: "GET"
14092
+ api_path: "/agentmemory/vision-embed",
14093
+ http_method: "POST"
12623
14094
  }
12624
14095
  });
12625
- sdk.registerFunction("api::audit", async (req) => {
14096
+ sdk.registerFunction("api::slot-list", async (req) => {
12626
14097
  const authErr = checkAuth(req, secret);
12627
14098
  if (authErr) return authErr;
12628
- const parsedLimit = parseOptionalInt(req.query_params?.["limit"]);
12629
14099
  return {
12630
14100
  status_code: 200,
12631
14101
  body: await sdk.trigger({
12632
- function_id: "mem::audit-query",
12633
- payload: {
12634
- operation: req.query_params?.["operation"],
12635
- limit: parsedLimit ?? 50
12636
- }
14102
+ function_id: "mem::slot-list",
14103
+ payload: {}
12637
14104
  })
12638
14105
  };
12639
14106
  });
12640
14107
  sdk.registerTrigger({
12641
14108
  type: "http",
12642
- function_id: "api::audit",
14109
+ function_id: "api::slot-list",
12643
14110
  config: {
12644
- api_path: "/agentmemory/audit",
14111
+ api_path: "/agentmemory/slots",
12645
14112
  http_method: "GET"
12646
14113
  }
12647
14114
  });
12648
- sdk.registerFunction("api::governance-delete", async (req) => {
14115
+ sdk.registerFunction("api::slot-get", async (req) => {
12649
14116
  const authErr = checkAuth(req, secret);
12650
14117
  if (authErr) return authErr;
12651
- if (!req.body?.memoryIds || !Array.isArray(req.body.memoryIds)) return {
14118
+ const label = asNonEmptyString$1(req.query_params?.["label"]);
14119
+ if (!label) return {
12652
14120
  status_code: 400,
12653
- body: { error: "memoryIds array is required" }
14121
+ body: { error: "label query param required" }
14122
+ };
14123
+ const result = await sdk.trigger({
14124
+ function_id: "mem::slot-get",
14125
+ payload: { label }
14126
+ });
14127
+ const resp = result;
14128
+ if (resp?.success === false) return {
14129
+ status_code: resp.error?.includes("not found") ? 404 : 400,
14130
+ body: resp
12654
14131
  };
12655
14132
  return {
12656
14133
  status_code: 200,
12657
- body: await sdk.trigger({
12658
- function_id: "mem::governance-delete",
12659
- payload: req.body
12660
- })
14134
+ body: result
12661
14135
  };
12662
14136
  });
12663
14137
  sdk.registerTrigger({
12664
14138
  type: "http",
12665
- function_id: "api::governance-delete",
14139
+ function_id: "api::slot-get",
12666
14140
  config: {
12667
- api_path: "/agentmemory/governance/memories",
12668
- http_method: "DELETE"
14141
+ api_path: "/agentmemory/slot",
14142
+ http_method: "GET"
12669
14143
  }
12670
14144
  });
12671
- sdk.registerFunction("api::governance-bulk", async (req) => {
14145
+ sdk.registerFunction("api::slot-create", async (req) => {
12672
14146
  const authErr = checkAuth(req, secret);
12673
14147
  if (authErr) return authErr;
14148
+ const body = req.body ?? {};
14149
+ const label = asNonEmptyString$1(body["label"]);
14150
+ if (!label) return {
14151
+ status_code: 400,
14152
+ body: { error: "label required" }
14153
+ };
14154
+ if (body["content"] !== void 0 && typeof body["content"] !== "string") return {
14155
+ status_code: 400,
14156
+ body: { error: "content must be a string" }
14157
+ };
14158
+ if (body["description"] !== void 0 && typeof body["description"] !== "string") return {
14159
+ status_code: 400,
14160
+ body: { error: "description must be a string" }
14161
+ };
14162
+ if (body["pinned"] !== void 0 && typeof body["pinned"] !== "boolean") return {
14163
+ status_code: 400,
14164
+ body: { error: "pinned must be a boolean" }
14165
+ };
14166
+ if (body["scope"] !== void 0 && body["scope"] !== "project" && body["scope"] !== "global") return {
14167
+ status_code: 400,
14168
+ body: { error: "scope must be 'project' or 'global'" }
14169
+ };
14170
+ const sizeLimit = parseOptionalPositiveInt(body["sizeLimit"]);
14171
+ if (sizeLimit === null) return {
14172
+ status_code: 400,
14173
+ body: { error: "sizeLimit must be a positive integer" }
14174
+ };
14175
+ if (sizeLimit !== void 0 && sizeLimit > 2e4) return {
14176
+ status_code: 400,
14177
+ body: { error: "sizeLimit must be <= 20000" }
14178
+ };
14179
+ const payload = { label };
14180
+ if (typeof body["content"] === "string") payload["content"] = body["content"];
14181
+ if (typeof body["description"] === "string") payload["description"] = body["description"];
14182
+ if (sizeLimit !== void 0) payload["sizeLimit"] = sizeLimit;
14183
+ if (typeof body["pinned"] === "boolean") payload["pinned"] = body["pinned"];
14184
+ if (body["scope"] === "project" || body["scope"] === "global") payload["scope"] = body["scope"];
14185
+ const result = await sdk.trigger({
14186
+ function_id: "mem::slot-create",
14187
+ payload
14188
+ });
14189
+ const resp = result;
14190
+ if (resp?.success === false) return {
14191
+ status_code: resp.error?.includes("exists") ? 409 : 400,
14192
+ body: resp
14193
+ };
12674
14194
  return {
12675
- status_code: 200,
12676
- body: await sdk.trigger({
12677
- function_id: "mem::governance-bulk",
12678
- payload: req.body || {}
12679
- })
14195
+ status_code: 201,
14196
+ body: result
12680
14197
  };
12681
14198
  });
12682
14199
  sdk.registerTrigger({
12683
14200
  type: "http",
12684
- function_id: "api::governance-bulk",
14201
+ function_id: "api::slot-create",
12685
14202
  config: {
12686
- api_path: "/agentmemory/governance/bulk-delete",
14203
+ api_path: "/agentmemory/slot",
12687
14204
  http_method: "POST"
12688
14205
  }
12689
14206
  });
12690
- sdk.registerFunction("api::snapshots", async (req) => {
14207
+ sdk.registerFunction("api::slot-append", async (req) => {
12691
14208
  const authErr = checkAuth(req, secret);
12692
14209
  if (authErr) return authErr;
12693
- try {
12694
- return {
12695
- status_code: 200,
12696
- body: await sdk.trigger({
12697
- function_id: "mem::snapshot-list",
12698
- payload: {}
12699
- })
12700
- };
12701
- } catch {
14210
+ const body = req.body ?? {};
14211
+ const label = asNonEmptyString$1(body["label"]);
14212
+ const text = typeof body["text"] === "string" ? body["text"] : null;
14213
+ if (!label || !text) return {
14214
+ status_code: 400,
14215
+ body: { error: "label and text required" }
14216
+ };
14217
+ const result = await sdk.trigger({
14218
+ function_id: "mem::slot-append",
14219
+ payload: {
14220
+ label,
14221
+ text
14222
+ }
14223
+ });
14224
+ const resp = result;
14225
+ if (resp?.success === false) {
14226
+ const notFound = resp.error?.includes("not found");
14227
+ const overLimit = resp.error?.includes("exceed");
12702
14228
  return {
12703
- status_code: 404,
12704
- body: { error: "Snapshots not enabled" }
14229
+ status_code: notFound ? 404 : overLimit ? 413 : 400,
14230
+ body: resp
12705
14231
  };
12706
14232
  }
14233
+ return {
14234
+ status_code: 200,
14235
+ body: result
14236
+ };
12707
14237
  });
12708
14238
  sdk.registerTrigger({
12709
14239
  type: "http",
12710
- function_id: "api::snapshots",
14240
+ function_id: "api::slot-append",
12711
14241
  config: {
12712
- api_path: "/agentmemory/snapshots",
12713
- http_method: "GET"
14242
+ api_path: "/agentmemory/slot/append",
14243
+ http_method: "POST"
12714
14244
  }
12715
14245
  });
12716
- sdk.registerFunction("api::snapshot-create", async (req) => {
14246
+ sdk.registerFunction("api::slot-replace", async (req) => {
12717
14247
  const authErr = checkAuth(req, secret);
12718
14248
  if (authErr) return authErr;
12719
- try {
12720
- return {
12721
- status_code: 201,
12722
- body: await sdk.trigger({
12723
- function_id: "mem::snapshot-create",
12724
- payload: req.body || {}
12725
- })
12726
- };
12727
- } catch {
14249
+ const body = req.body ?? {};
14250
+ const label = asNonEmptyString$1(body["label"]);
14251
+ const content = body["content"];
14252
+ if (!label || typeof content !== "string") return {
14253
+ status_code: 400,
14254
+ body: { error: "label and content (string) required" }
14255
+ };
14256
+ const result = await sdk.trigger({
14257
+ function_id: "mem::slot-replace",
14258
+ payload: {
14259
+ label,
14260
+ content
14261
+ }
14262
+ });
14263
+ const resp = result;
14264
+ if (resp?.success === false) {
14265
+ const notFound = resp.error?.includes("not found");
14266
+ const overLimit = resp.error?.includes("exceed");
12728
14267
  return {
12729
- status_code: 404,
12730
- body: { error: "Snapshots not enabled" }
14268
+ status_code: notFound ? 404 : overLimit ? 413 : 400,
14269
+ body: resp
12731
14270
  };
12732
14271
  }
14272
+ return {
14273
+ status_code: 200,
14274
+ body: result
14275
+ };
12733
14276
  });
12734
14277
  sdk.registerTrigger({
12735
14278
  type: "http",
12736
- function_id: "api::snapshot-create",
14279
+ function_id: "api::slot-replace",
12737
14280
  config: {
12738
- api_path: "/agentmemory/snapshot/create",
14281
+ api_path: "/agentmemory/slot/replace",
12739
14282
  http_method: "POST"
12740
14283
  }
12741
14284
  });
12742
- sdk.registerFunction("api::snapshot-restore", async (req) => {
14285
+ sdk.registerFunction("api::slot-delete", async (req) => {
12743
14286
  const authErr = checkAuth(req, secret);
12744
14287
  if (authErr) return authErr;
12745
- if (!req.body?.commitHash) return {
14288
+ const label = asNonEmptyString$1(req.query_params?.["label"]);
14289
+ if (!label) return {
12746
14290
  status_code: 400,
12747
- body: { error: "commitHash is required" }
14291
+ body: { error: "label query param required" }
14292
+ };
14293
+ const result = await sdk.trigger({
14294
+ function_id: "mem::slot-delete",
14295
+ payload: { label }
14296
+ });
14297
+ const resp = result;
14298
+ if (resp?.success === false) return {
14299
+ status_code: resp.error?.includes("not found") ? 404 : 400,
14300
+ body: resp
14301
+ };
14302
+ return {
14303
+ status_code: 200,
14304
+ body: result
12748
14305
  };
12749
- try {
12750
- return {
12751
- status_code: 200,
12752
- body: await sdk.trigger({
12753
- function_id: "mem::snapshot-restore",
12754
- payload: req.body
12755
- })
12756
- };
12757
- } catch {
12758
- return {
12759
- status_code: 404,
12760
- body: { error: "Snapshots not enabled" }
12761
- };
12762
- }
12763
14306
  });
12764
14307
  sdk.registerTrigger({
12765
14308
  type: "http",
12766
- function_id: "api::snapshot-restore",
14309
+ function_id: "api::slot-delete",
12767
14310
  config: {
12768
- api_path: "/agentmemory/snapshot/restore",
12769
- http_method: "POST"
14311
+ api_path: "/agentmemory/slot",
14312
+ http_method: "DELETE"
12770
14313
  }
12771
14314
  });
12772
- sdk.registerFunction("api::memories", async (req) => {
14315
+ sdk.registerFunction("api::slot-reflect", async (req) => {
12773
14316
  const authErr = checkAuth(req, secret);
12774
14317
  if (authErr) return authErr;
12775
- const memories = await kv.list(KV.memories);
14318
+ const body = req.body ?? {};
14319
+ const sessionId = asNonEmptyString$1(body["sessionId"]);
14320
+ if (!sessionId) return {
14321
+ status_code: 400,
14322
+ body: { error: "sessionId required" }
14323
+ };
14324
+ const maxObservations = parseOptionalPositiveInt(body["maxObservations"]);
14325
+ if (maxObservations === null) return {
14326
+ status_code: 400,
14327
+ body: { error: "maxObservations must be a positive integer" }
14328
+ };
14329
+ const payload = { sessionId };
14330
+ if (maxObservations !== void 0) payload["maxObservations"] = maxObservations;
12776
14331
  return {
12777
14332
  status_code: 200,
12778
- body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
14333
+ body: await sdk.trigger({
14334
+ function_id: "mem::slot-reflect",
14335
+ payload
14336
+ })
12779
14337
  };
12780
14338
  });
12781
14339
  sdk.registerTrigger({
12782
14340
  type: "http",
12783
- function_id: "api::memories",
14341
+ function_id: "api::slot-reflect",
12784
14342
  config: {
12785
- api_path: "/agentmemory/memories",
12786
- http_method: "GET"
14343
+ api_path: "/agentmemory/slot/reflect",
14344
+ http_method: "POST"
12787
14345
  }
12788
14346
  });
12789
14347
  sdk.registerFunction("api::action-create", async (req) => {
@@ -13020,9 +14578,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
13020
14578
  sdk.registerFunction("api::routine-create", async (req) => {
13021
14579
  const authErr = checkAuth(req, secret);
13022
14580
  if (authErr) return authErr;
13023
- if (!req.body?.name) return {
14581
+ if (!req.body?.name || !req.body?.steps) return {
13024
14582
  status_code: 400,
13025
- body: { error: "name is required" }
14583
+ body: { error: "name and steps are required" }
13026
14584
  };
13027
14585
  return {
13028
14586
  status_code: 201,
@@ -14271,10 +15829,21 @@ function registerEventTriggers(sdk, kv) {
14271
15829
  function_id: "event::observation",
14272
15830
  config: { topic: "agentmemory.observation" }
14273
15831
  });
14274
- sdk.registerFunction("event::session::stopped", async (data) => sdk.trigger({
14275
- function_id: "mem::summarize",
14276
- payload: data
14277
- }));
15832
+ sdk.registerFunction("event::session::stopped", async (data) => {
15833
+ const summary = await sdk.trigger({
15834
+ function_id: "mem::summarize",
15835
+ payload: data
15836
+ });
15837
+ if (isReflectEnabled()) try {
15838
+ sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
15839
+ } catch (err) {
15840
+ logger.warn("slot-reflect triggerVoid failed", {
15841
+ sessionId: data.sessionId,
15842
+ error: err instanceof Error ? err.message : String(err)
15843
+ });
15844
+ }
15845
+ return summary;
15846
+ });
14278
15847
  sdk.registerTrigger({
14279
15848
  type: "durable:subscriber",
14280
15849
  function_id: "event::session::stopped",
@@ -14526,6 +16095,34 @@ function registerMcpEndpoints(sdk, kv, secret) {
14526
16095
  }] }
14527
16096
  };
14528
16097
  }
16098
+ case "memory_vision_search": {
16099
+ const queryText = typeof args.queryText === "string" ? args.queryText : void 0;
16100
+ const queryImageRef = typeof args.queryImageRef === "string" ? args.queryImageRef : void 0;
16101
+ const queryImageBase64 = typeof args.queryImageBase64 === "string" ? args.queryImageBase64 : void 0;
16102
+ if (!queryText && !queryImageRef && !queryImageBase64) return {
16103
+ status_code: 400,
16104
+ body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
16105
+ };
16106
+ const topK = Math.max(1, Math.min(50, asNumber(args.topK, 10) ?? 10));
16107
+ const sessionId = typeof args.sessionId === "string" ? args.sessionId : void 0;
16108
+ const result = await sdk.trigger({
16109
+ function_id: "mem::vision-search",
16110
+ payload: {
16111
+ queryText,
16112
+ queryImageRef,
16113
+ queryImageBase64,
16114
+ topK,
16115
+ sessionId
16116
+ }
16117
+ });
16118
+ return {
16119
+ status_code: 200,
16120
+ body: { content: [{
16121
+ type: "text",
16122
+ text: JSON.stringify(result, null, 2)
16123
+ }] }
16124
+ };
16125
+ }
14529
16126
  case "memory_timeline": {
14530
16127
  if (typeof args.anchor !== "string" || !args.anchor.trim()) return {
14531
16128
  status_code: 400,
@@ -15391,6 +16988,123 @@ function registerMcpEndpoints(sdk, kv, secret) {
15391
16988
  }] }
15392
16989
  };
15393
16990
  }
16991
+ case "memory_slot_list": {
16992
+ const result = await sdk.trigger({
16993
+ function_id: "mem::slot-list",
16994
+ payload: {}
16995
+ });
16996
+ return {
16997
+ status_code: 200,
16998
+ body: { content: [{
16999
+ type: "text",
17000
+ text: JSON.stringify(result, null, 2)
17001
+ }] }
17002
+ };
17003
+ }
17004
+ case "memory_slot_get": {
17005
+ const label = asNonEmptyString(args.label);
17006
+ if (!label) return {
17007
+ status_code: 400,
17008
+ body: { error: "label required" }
17009
+ };
17010
+ const result = await sdk.trigger({
17011
+ function_id: "mem::slot-get",
17012
+ payload: { label }
17013
+ });
17014
+ return {
17015
+ status_code: 200,
17016
+ body: { content: [{
17017
+ type: "text",
17018
+ text: JSON.stringify(result, null, 2)
17019
+ }] }
17020
+ };
17021
+ }
17022
+ case "memory_slot_create": {
17023
+ const label = asNonEmptyString(args.label);
17024
+ if (!label) return {
17025
+ status_code: 400,
17026
+ body: { error: "label required" }
17027
+ };
17028
+ const payload = { label };
17029
+ if (typeof args.content === "string") payload.content = args.content;
17030
+ if (typeof args.description === "string") payload.description = args.description;
17031
+ if (typeof args.sizeLimit === "number") payload.sizeLimit = args.sizeLimit;
17032
+ if (args.pinned === false || args.pinned === "false") payload.pinned = false;
17033
+ else if (args.pinned === true || args.pinned === "true") payload.pinned = true;
17034
+ if (args.scope === "global" || args.scope === "project") payload.scope = args.scope;
17035
+ const result = await sdk.trigger({
17036
+ function_id: "mem::slot-create",
17037
+ payload
17038
+ });
17039
+ return {
17040
+ status_code: 200,
17041
+ body: { content: [{
17042
+ type: "text",
17043
+ text: JSON.stringify(result, null, 2)
17044
+ }] }
17045
+ };
17046
+ }
17047
+ case "memory_slot_append": {
17048
+ const label = asNonEmptyString(args.label);
17049
+ const text = typeof args.text === "string" ? args.text : null;
17050
+ if (!label || !text) return {
17051
+ status_code: 400,
17052
+ body: { error: "label and text required" }
17053
+ };
17054
+ const result = await sdk.trigger({
17055
+ function_id: "mem::slot-append",
17056
+ payload: {
17057
+ label,
17058
+ text
17059
+ }
17060
+ });
17061
+ return {
17062
+ status_code: 200,
17063
+ body: { content: [{
17064
+ type: "text",
17065
+ text: JSON.stringify(result, null, 2)
17066
+ }] }
17067
+ };
17068
+ }
17069
+ case "memory_slot_replace": {
17070
+ const label = asNonEmptyString(args.label);
17071
+ if (!label || typeof args.content !== "string") return {
17072
+ status_code: 400,
17073
+ body: { error: "label and content (string) required" }
17074
+ };
17075
+ const result = await sdk.trigger({
17076
+ function_id: "mem::slot-replace",
17077
+ payload: {
17078
+ label,
17079
+ content: args.content
17080
+ }
17081
+ });
17082
+ return {
17083
+ status_code: 200,
17084
+ body: { content: [{
17085
+ type: "text",
17086
+ text: JSON.stringify(result, null, 2)
17087
+ }] }
17088
+ };
17089
+ }
17090
+ case "memory_slot_delete": {
17091
+ const label = asNonEmptyString(args.label);
17092
+ if (!label) return {
17093
+ status_code: 400,
17094
+ body: { error: "label required" }
17095
+ };
17096
+ const result = await sdk.trigger({
17097
+ function_id: "mem::slot-delete",
17098
+ payload: { label }
17099
+ });
17100
+ return {
17101
+ status_code: 200,
17102
+ body: { content: [{
17103
+ type: "text",
17104
+ text: JSON.stringify(result, null, 2)
17105
+ }] }
17106
+ };
17107
+ }
15394
17108
  default: return {
15395
17109
  status_code: 400,
15396
17110
  body: { error: `Unknown tool: ${name}` }
@@ -16045,11 +17759,13 @@ async function main() {
16045
17759
  const fallbackConfig = loadFallbackConfig();
16046
17760
  const provider = fallbackConfig.providers.length > 0 ? createFallbackProvider(config.provider, fallbackConfig) : createProvider(config.provider);
16047
17761
  const embeddingProvider = createEmbeddingProvider();
17762
+ const imageEmbeddingProvider = createImageEmbeddingProvider();
16048
17763
  console.log(`[agentmemory] Starting worker v${VERSION}...`);
16049
17764
  console.log(`[agentmemory] Engine: ${config.engineUrl}`);
16050
17765
  console.log(`[agentmemory] Provider: ${config.provider.provider} (${config.provider.model})`);
16051
17766
  if (embeddingProvider) console.log(`[agentmemory] Embedding provider: ${embeddingProvider.name} (${embeddingProvider.dimensions} dims)`);
16052
17767
  else console.log(`[agentmemory] Embedding provider: none (BM25-only mode)`);
17768
+ if (imageEmbeddingProvider) console.log(`[agentmemory] Image embedding provider: ${imageEmbeddingProvider.name} (${imageEmbeddingProvider.dimensions} dims) — vision-search active`);
16053
17769
  console.log(`[agentmemory] REST API: http://localhost:${config.restPort}/agentmemory/*`);
16054
17770
  console.log(`[agentmemory] Streams: ws://localhost:${config.streamsPort}`);
16055
17771
  const sdk = registerWorker(config.engineUrl, {
@@ -16068,6 +17784,10 @@ async function main() {
16068
17784
  initMetrics(hasGetMeter(sdk) ? sdk.getMeter.bind(sdk) : void 0);
16069
17785
  registerPrivacyFunction(sdk);
16070
17786
  registerObserveFunction(sdk, kv, dedupMap, config.maxObservationsPerSession);
17787
+ registerImageQuotaCleanup(sdk, kv);
17788
+ registerVisionSearchFunctions(sdk, kv, imageEmbeddingProvider);
17789
+ if (isSlotsEnabled()) registerSlotsFunctions(sdk, kv);
17790
+ registerDiskSizeManager(sdk, kv);
16071
17791
  registerCompressFunction(sdk, kv, provider, metricsStore);
16072
17792
  registerSearchFunction(sdk, kv);
16073
17793
  registerContextFunction(sdk, kv, config.tokenBudget);
@@ -16134,6 +17854,7 @@ async function main() {
16134
17854
  registerReplayFunctions(sdk, kv);
16135
17855
  console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
16136
17856
  console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
17857
+ if (isSlotsEnabled()) console.log(`[agentmemory] Slots: enabled (pinned editable memory). Reflect on Stop hook: ${isReflectEnabled() ? "on" : "off"}`);
16137
17858
  const snapshotConfig = loadSnapshotConfig();
16138
17859
  if (snapshotConfig.enabled) {
16139
17860
  registerSnapshotFunction(sdk, kv, snapshotConfig.dir);
@@ -16171,7 +17892,7 @@ async function main() {
16171
17892
  }
16172
17893
  }
16173
17894
  console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
16174
- console.log(`[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`);
17895
+ console.log(`[agentmemory] Endpoints: 107 REST + ${getAllTools().length} MCP tools + 6 MCP resources + 3 MCP prompts`);
16175
17896
  const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
16176
17897
  const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
16177
17898
  const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);
@@ -16237,5 +17958,5 @@ main().catch((err) => {
16237
17958
  });
16238
17959
 
16239
17960
  //#endregion
16240
- export { };
16241
- //# sourceMappingURL=src-B3pEsBSb.mjs.map
17961
+ 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 };
17962
+ //# sourceMappingURL=src-tmuZyobT.mjs.map