@alfe.ai/openclaw-memory-cloud 0.0.38 → 0.0.39

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 (58) hide show
  1. package/README.md +37 -0
  2. package/dist/index.cjs +2 -0
  3. package/dist/index.d.cts +2 -0
  4. package/dist/index.d.ts +2 -60
  5. package/dist/index.js +2 -396
  6. package/dist/plugin.cjs +2 -0
  7. package/dist/plugin.d.cts +59 -0
  8. package/dist/plugin.d.cts.map +1 -0
  9. package/dist/plugin.d.ts +59 -0
  10. package/dist/plugin.d.ts.map +1 -0
  11. package/dist/plugin.js +2 -0
  12. package/dist/plugin2.cjs +1107 -0
  13. package/dist/plugin2.d.cts +2 -0
  14. package/dist/plugin2.d.ts +2 -0
  15. package/dist/plugin2.js +1104 -0
  16. package/dist/plugin2.js.map +1 -0
  17. package/openclaw.plugin.json +6 -5
  18. package/package.json +27 -7
  19. package/.turbo/turbo-build.log +0 -4
  20. package/CHANGELOG.md +0 -320
  21. package/dist/auto-capture.d.ts +0 -45
  22. package/dist/auto-capture.d.ts.map +0 -1
  23. package/dist/auto-capture.js +0 -101
  24. package/dist/auto-capture.js.map +0 -1
  25. package/dist/auto-recall.d.ts +0 -22
  26. package/dist/auto-recall.d.ts.map +0 -1
  27. package/dist/auto-recall.js +0 -63
  28. package/dist/auto-recall.js.map +0 -1
  29. package/dist/formatter.d.ts +0 -7
  30. package/dist/formatter.d.ts.map +0 -1
  31. package/dist/formatter.js +0 -27
  32. package/dist/formatter.js.map +0 -1
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/index.js.map +0 -1
  35. package/dist/ingest-epoch.d.ts +0 -38
  36. package/dist/ingest-epoch.d.ts.map +0 -1
  37. package/dist/ingest-epoch.js +0 -66
  38. package/dist/ingest-epoch.js.map +0 -1
  39. package/dist/session-backfill.d.ts +0 -54
  40. package/dist/session-backfill.d.ts.map +0 -1
  41. package/dist/session-backfill.js +0 -192
  42. package/dist/session-backfill.js.map +0 -1
  43. package/dist/types.d.ts +0 -113
  44. package/dist/types.d.ts.map +0 -1
  45. package/dist/types.js +0 -2
  46. package/dist/types.js.map +0 -1
  47. package/src/__tests__/auto-capture.test.ts +0 -115
  48. package/src/__tests__/ingest-epoch.test.ts +0 -67
  49. package/src/__tests__/session-backfill.test.ts +0 -289
  50. package/src/auto-capture.ts +0 -108
  51. package/src/auto-recall.ts +0 -66
  52. package/src/formatter.ts +0 -30
  53. package/src/index.ts +0 -464
  54. package/src/ingest-epoch.ts +0 -78
  55. package/src/session-backfill.ts +0 -220
  56. package/src/types.ts +0 -93
  57. package/sst-env.d.ts +0 -10
  58. package/tsconfig.json +0 -20
@@ -0,0 +1,1107 @@
1
+ let node_module = require("node:module");
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let node_path = require("node:path");
4
+ let _sinclair_typebox = require("@sinclair/typebox");
5
+ let _alfe_ai_config = require("@alfe.ai/config");
6
+ let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
7
+ let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
8
+ let node_fs = require("node:fs");
9
+ let node_crypto = require("node:crypto");
10
+ let node_os = require("node:os");
11
+ //#region src/auto-capture.ts
12
+ /**
13
+ * Debounces and flushes live conversation capture independently per session.
14
+ *
15
+ * OpenClaw can interleave hooks from multiple sessions in one daemon process,
16
+ * so a process-wide current session or queue is not safe. Every public method
17
+ * therefore requires the canonical hook context session key.
18
+ */
19
+ var AutoCapture = class AutoCapture {
20
+ static MAX_QUEUE_SIZE = 500;
21
+ sessions = /* @__PURE__ */ new Map();
22
+ /** Global per boot so evicting one session cannot reset its index sequence. */
23
+ messageIndex = 0;
24
+ constructor(client, config, logger, ingestEpoch) {
25
+ this.client = client;
26
+ this.config = config;
27
+ this.logger = logger;
28
+ this.ingestEpoch = ingestEpoch;
29
+ }
30
+ trackMessage(sessionKey, role, content, metadata) {
31
+ if (!this.config.autoCapture || content.length === 0) return;
32
+ const state = this.getSession(sessionKey);
33
+ if (metadata) state.metadata = mergeMetadata(state.metadata, metadata);
34
+ if (state.messages.length >= AutoCapture.MAX_QUEUE_SIZE) {
35
+ state.messages.shift();
36
+ this.logger.warn("Memory capture queue full; dropped oldest message", { queueSize: AutoCapture.MAX_QUEUE_SIZE });
37
+ }
38
+ state.messages.push({
39
+ role,
40
+ content: content.slice(0, this.config.captureMaxChars),
41
+ index: this.messageIndex++,
42
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
43
+ });
44
+ this.resetTimer(sessionKey, state);
45
+ }
46
+ async flush(sessionKey) {
47
+ const state = this.sessions.get(sessionKey);
48
+ if (!state || state.messages.length === 0) return;
49
+ if (state.flushPromise) {
50
+ await state.flushPromise;
51
+ return;
52
+ }
53
+ state.flushPromise = this.flushSerially(sessionKey, state);
54
+ try {
55
+ await state.flushPromise;
56
+ } finally {
57
+ state.flushPromise = null;
58
+ if (state.messages.length === 0) this.deleteSession(sessionKey, state);
59
+ }
60
+ }
61
+ async onAgentEnd(sessionKey) {
62
+ const state = this.sessions.get(sessionKey);
63
+ if (!state) return;
64
+ this.clearTimer(state);
65
+ await this.flush(sessionKey);
66
+ }
67
+ destroy() {
68
+ for (const state of this.sessions.values()) this.clearTimer(state);
69
+ this.sessions.clear();
70
+ }
71
+ getSession(sessionKey) {
72
+ const existing = this.sessions.get(sessionKey);
73
+ if (existing) return existing;
74
+ const state = {
75
+ messages: [],
76
+ metadata: {},
77
+ timer: null,
78
+ flushPromise: null
79
+ };
80
+ this.sessions.set(sessionKey, state);
81
+ return state;
82
+ }
83
+ async flushSerially(sessionKey, state) {
84
+ while (state.messages.length > 0) {
85
+ const batch = state.messages.splice(0, state.messages.length);
86
+ if (batch.length === 0) return;
87
+ this.logger.debug("Flushing memory capture batch", { messageCount: batch.length });
88
+ try {
89
+ await this.client.memoryIngest(sessionKey, batch, state.metadata, this.ingestEpoch);
90
+ } catch {
91
+ state.messages.unshift(...batch);
92
+ this.logger.warn("Memory capture flush failed; retained batch for retry", { messageCount: batch.length });
93
+ return;
94
+ }
95
+ }
96
+ }
97
+ resetTimer(sessionKey, state) {
98
+ this.clearTimer(state);
99
+ state.timer = setTimeout(() => {
100
+ state.timer = null;
101
+ this.flush(sessionKey);
102
+ }, this.config.idleFlushSeconds * 1e3);
103
+ state.timer.unref();
104
+ }
105
+ clearTimer(state) {
106
+ if (state.timer) clearTimeout(state.timer);
107
+ state.timer = null;
108
+ }
109
+ deleteSession(sessionKey, state) {
110
+ this.clearTimer(state);
111
+ if (this.sessions.get(sessionKey) === state) this.sessions.delete(sessionKey);
112
+ }
113
+ };
114
+ function mergeMetadata(current, next) {
115
+ return {
116
+ ...current.channelId ? { channelId: current.channelId } : {},
117
+ ...current.userId ? { userId: current.userId } : {},
118
+ ...current.userName ? { userName: current.userName } : {},
119
+ ...next.channelId ? { channelId: next.channelId } : {},
120
+ ...next.userId ? { userId: next.userId } : {},
121
+ ...next.userName ? { userName: next.userName } : {}
122
+ };
123
+ }
124
+ //#endregion
125
+ //#region src/file-boundary.ts
126
+ const MAX_LEARN_TEXT_CHARS = 1e5;
127
+ var WorkspaceFileError = class extends Error {
128
+ name = "WorkspaceFileError";
129
+ };
130
+ async function readWorkspaceText(workspacePath, relativePath) {
131
+ validateRelativePath(relativePath);
132
+ const workspaceReal = await (0, node_fs_promises.realpath)(workspacePath);
133
+ const requestedPath = (0, node_path.resolve)(workspaceReal, relativePath);
134
+ let componentPath = workspaceReal;
135
+ for (const component of relativePath.split("/")) {
136
+ componentPath = (0, node_path.join)(componentPath, component);
137
+ if ((await (0, node_fs_promises.lstat)(componentPath)).isSymbolicLink()) throw new WorkspaceFileError("Workspace path must not contain symlinks");
138
+ }
139
+ const requestedStat = await (0, node_fs_promises.lstat)(requestedPath);
140
+ if (requestedStat.isSymbolicLink() || !requestedStat.isFile()) throw new WorkspaceFileError("Workspace path must identify a regular file");
141
+ const fileReal = await (0, node_fs_promises.realpath)(requestedPath);
142
+ if (!isInside(workspaceReal, fileReal)) throw new WorkspaceFileError("Workspace path escapes the configured workspace");
143
+ let handle;
144
+ try {
145
+ handle = await (0, node_fs_promises.open)(fileReal, node_fs.constants.O_RDONLY | noFollowFlag$2());
146
+ const opened = await handle.stat();
147
+ if (!opened.isFile() || opened.size > 409600) throw new WorkspaceFileError("Workspace file exceeds the 400 KiB read limit");
148
+ const text = await handle.readFile("utf8");
149
+ if (text.length > 1e5) throw new WorkspaceFileError("Workspace file exceeds the 100,000 character learn limit");
150
+ return text;
151
+ } finally {
152
+ await handle?.close();
153
+ }
154
+ }
155
+ function validateRelativePath(value) {
156
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) throw new WorkspaceFileError("path must be a non-empty workspace-relative path");
157
+ if ((0, node_path.isAbsolute)(value) || value.includes("\\") || value.includes("//") || hasControlCharacters$1(value) || value.split("/").some((part) => part === "" || part === "." || part === "..")) throw new WorkspaceFileError("path must remain inside the configured workspace");
158
+ }
159
+ function hasControlCharacters$1(value) {
160
+ for (const character of value) {
161
+ const code = character.charCodeAt(0);
162
+ if (code < 32 || code === 127) return true;
163
+ }
164
+ return false;
165
+ }
166
+ function isInside(root, candidate) {
167
+ const rel = (0, node_path.relative)(root, candidate);
168
+ return rel !== ".." && !rel.startsWith(`..${node_path.sep}`) && !(0, node_path.isAbsolute)(rel);
169
+ }
170
+ function noFollowFlag$2() {
171
+ return "O_NOFOLLOW" in node_fs.constants ? node_fs.constants.O_NOFOLLOW : 0;
172
+ }
173
+ //#endregion
174
+ //#region src/boundary.ts
175
+ const MAX_QUERY_CHARS = 4e3;
176
+ const MAX_STORE_TEXT_CHARS = 5e3;
177
+ const DEFAULT_CONFIG = {
178
+ autoCapture: true,
179
+ autoRecall: true,
180
+ captureMaxChars: 500,
181
+ idleFlushSeconds: 60,
182
+ backfillSessions: true,
183
+ backfillMaxSessions: 50
184
+ };
185
+ const TAGS = new Set([
186
+ "fact",
187
+ "decision",
188
+ "preference",
189
+ "event",
190
+ "discovery"
191
+ ]);
192
+ var MemoryInputError = class extends Error {
193
+ name = "MemoryInputError";
194
+ };
195
+ function parseMemoryConfig(value) {
196
+ if (value === void 0) return { ...DEFAULT_CONFIG };
197
+ const input = record(value, "Memory plugin config");
198
+ rejectUnknown(input, [
199
+ "autoCapture",
200
+ "autoRecall",
201
+ "captureMaxChars",
202
+ "idleFlushSeconds",
203
+ "backfillSessions",
204
+ "backfillMaxSessions"
205
+ ]);
206
+ return {
207
+ autoCapture: optionalBoolean(input.autoCapture, DEFAULT_CONFIG.autoCapture, "autoCapture"),
208
+ autoRecall: optionalBoolean(input.autoRecall, DEFAULT_CONFIG.autoRecall, "autoRecall"),
209
+ captureMaxChars: optionalInteger(input.captureMaxChars, DEFAULT_CONFIG.captureMaxChars, "captureMaxChars", 100, 1e4),
210
+ idleFlushSeconds: optionalInteger(input.idleFlushSeconds, DEFAULT_CONFIG.idleFlushSeconds, "idleFlushSeconds", 10, 600),
211
+ backfillSessions: optionalBoolean(input.backfillSessions, DEFAULT_CONFIG.backfillSessions, "backfillSessions"),
212
+ backfillMaxSessions: optionalInteger(input.backfillMaxSessions, DEFAULT_CONFIG.backfillMaxSessions, "backfillMaxSessions", 1, 1e3)
213
+ };
214
+ }
215
+ function parseSearchInput(value) {
216
+ const input = record(value, "Memory search parameters");
217
+ rejectUnknown(input, [
218
+ "query",
219
+ "limit",
220
+ "topic",
221
+ "subtopic",
222
+ "tag"
223
+ ]);
224
+ return compact({
225
+ query: string(input.query, "query", 1, MAX_QUERY_CHARS, true),
226
+ limit: input.limit === void 0 ? 10 : integer(input.limit, "limit", 1, 50),
227
+ topic: optionalString(input.topic, "topic", 256),
228
+ subtopic: optionalString(input.subtopic, "subtopic", 256),
229
+ tag: optionalTag(input.tag)
230
+ });
231
+ }
232
+ function parseStoreInput(value) {
233
+ const input = record(value, "Memory store parameters");
234
+ rejectUnknown(input, [
235
+ "text",
236
+ "topic",
237
+ "subtopic",
238
+ "tag",
239
+ "importance"
240
+ ]);
241
+ return compact({
242
+ text: string(input.text, "text", 1, MAX_STORE_TEXT_CHARS, true),
243
+ topic: optionalString(input.topic, "topic", 128),
244
+ subtopic: optionalString(input.subtopic, "subtopic", 128),
245
+ tag: optionalTag(input.tag),
246
+ importance: input.importance === void 0 ? void 0 : finite(input.importance, "importance", 0, 1)
247
+ });
248
+ }
249
+ function parseLearnInput(value) {
250
+ const input = record(value, "Memory learn parameters");
251
+ rejectUnknown(input, [
252
+ "content",
253
+ "path",
254
+ "source"
255
+ ]);
256
+ const content = optionalString(input.content, "content", MAX_LEARN_TEXT_CHARS, true);
257
+ const path = optionalString(input.path, "path", 1024);
258
+ if (content === void 0 === (path === void 0)) throw new MemoryInputError("Provide exactly one of content or path");
259
+ if (path !== void 0) try {
260
+ validateRelativePath(path);
261
+ } catch {
262
+ throw new MemoryInputError("path must remain inside the configured workspace");
263
+ }
264
+ return compact({
265
+ content,
266
+ path,
267
+ source: optionalString(input.source, "source", 512)
268
+ });
269
+ }
270
+ function parseForgetInput(value) {
271
+ const input = record(value, "Memory forget parameters");
272
+ rejectUnknown(input, ["query", "confirmMemoryIds"]);
273
+ let confirmMemoryIds;
274
+ if (input.confirmMemoryIds !== void 0) {
275
+ if (!Array.isArray(input.confirmMemoryIds) || input.confirmMemoryIds.length < 1 || input.confirmMemoryIds.length > 5) throw new MemoryInputError("confirmMemoryIds must contain between 1 and 5 memory IDs");
276
+ confirmMemoryIds = input.confirmMemoryIds.map((id) => string(id, "memory ID", 1, 512));
277
+ if (new Set(confirmMemoryIds).size !== confirmMemoryIds.length) throw new MemoryInputError("confirmMemoryIds must not contain duplicates");
278
+ }
279
+ return compact({
280
+ query: string(input.query, "query", 1, MAX_QUERY_CHARS, true),
281
+ confirmMemoryIds
282
+ });
283
+ }
284
+ function parseEntityInput(value) {
285
+ const input = record(value, "Memory graph parameters");
286
+ rejectUnknown(input, ["entity"]);
287
+ return { entity: string(input.entity, "entity", 1, 512, true) };
288
+ }
289
+ function parseEmptyInput(value) {
290
+ rejectUnknown(record(value, "Tool parameters"), []);
291
+ return {};
292
+ }
293
+ function safeCount(value) {
294
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
295
+ }
296
+ function safeText(value, maxChars) {
297
+ return typeof value === "string" ? value.slice(0, maxChars) : "";
298
+ }
299
+ function classifyToolFailure(error, action) {
300
+ if (error instanceof MemoryInputError) return error.message;
301
+ if (error instanceof Error && error.name === "WorkspaceFileError") return error.message;
302
+ return `${action} failed. Retry, and inspect agent diagnostics if the failure persists.`;
303
+ }
304
+ function record(value, label) {
305
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new MemoryInputError(`${label} must be an object`);
306
+ return value;
307
+ }
308
+ function rejectUnknown(input, allowed) {
309
+ const unknown = Object.keys(input).find((key) => !allowed.includes(key));
310
+ if (unknown) throw new MemoryInputError(`Unknown parameter: ${unknown}`);
311
+ }
312
+ function string(value, label, min, max, trim = false) {
313
+ if (typeof value !== "string") throw new MemoryInputError(`${label} must be a string`);
314
+ const result = trim ? value.trim() : value;
315
+ if (result.length < min || result.length > max || hasControlCharacters(result, true)) throw new MemoryInputError(`${label} must contain ${String(min)}-${String(max)} safe characters`);
316
+ return result;
317
+ }
318
+ function hasControlCharacters(value, allowTextWhitespace = false) {
319
+ for (const character of value) {
320
+ const code = character.charCodeAt(0);
321
+ if (code === 127) return true;
322
+ if (code < 32 && !(allowTextWhitespace && (code === 9 || code === 10 || code === 13))) return true;
323
+ }
324
+ return false;
325
+ }
326
+ function optionalString(value, label, max, trim = false) {
327
+ return value === void 0 ? void 0 : string(value, label, 1, max, trim);
328
+ }
329
+ function integer(value, label, min, max) {
330
+ if (!Number.isInteger(value) || value < min || value > max) throw new MemoryInputError(`${label} must be an integer from ${String(min)} to ${String(max)}`);
331
+ return value;
332
+ }
333
+ function optionalInteger(value, fallback, label, min, max) {
334
+ return value === void 0 ? fallback : integer(value, label, min, max);
335
+ }
336
+ function finite(value, label, min, max) {
337
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new MemoryInputError(`${label} must be a finite number from ${String(min)} to ${String(max)}`);
338
+ return value;
339
+ }
340
+ function optionalBoolean(value, fallback, label) {
341
+ if (value === void 0) return fallback;
342
+ if (typeof value !== "boolean") throw new MemoryInputError(`${label} must be a boolean`);
343
+ return value;
344
+ }
345
+ function optionalTag(value) {
346
+ if (value === void 0) return void 0;
347
+ if (typeof value !== "string" || !TAGS.has(value)) throw new MemoryInputError("tag must be fact, decision, preference, event, or discovery");
348
+ return value;
349
+ }
350
+ function compact(value) {
351
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
352
+ }
353
+ //#endregion
354
+ //#region src/formatter.ts
355
+ const MAX_FACTS = 50;
356
+ const MAX_MEMORIES = 50;
357
+ const MAX_FIELD_CHARS = 512;
358
+ const MAX_MEMORY_TEXT_CHARS = 2e3;
359
+ function normalizeSearchResults(value) {
360
+ const input = isRecord$1(value) ? value : {};
361
+ const rawFacts = Array.isArray(input.facts) ? input.facts : [];
362
+ const rawMemories = Array.isArray(input.memories) ? input.memories : [];
363
+ return {
364
+ facts: rawFacts.slice(0, MAX_FACTS).filter(isRecord$1).map((fact) => ({
365
+ subject: safeText(fact.subject, MAX_FIELD_CHARS),
366
+ predicate: safeText(fact.predicate, MAX_FIELD_CHARS),
367
+ object: safeText(fact.object, MAX_MEMORY_TEXT_CHARS),
368
+ since: safeText(fact.since, 64),
369
+ confidence: safeFinite(fact.confidence)
370
+ })),
371
+ memories: rawMemories.slice(0, MAX_MEMORIES).filter(isRecord$1).map((memory) => ({
372
+ id: safeText(memory.id, MAX_FIELD_CHARS),
373
+ text: safeText(memory.text, MAX_MEMORY_TEXT_CHARS),
374
+ topic: safeText(memory.topic, MAX_FIELD_CHARS),
375
+ subtopic: safeText(memory.subtopic, MAX_FIELD_CHARS),
376
+ tag: safeText(memory.tag, 64),
377
+ importance: safeFinite(memory.importance),
378
+ timestamp: safeFinite(memory.timestamp),
379
+ score: safeFinite(memory.score)
380
+ })),
381
+ truncated: rawFacts.length > MAX_FACTS || rawMemories.length > MAX_MEMORIES
382
+ };
383
+ }
384
+ /** Format trusted, locally-normalized search results for tool display. */
385
+ function formatSearchResults(results) {
386
+ return normalizeSearchResults(results);
387
+ }
388
+ /** Build prompt XML locally; the service-provided formatted field is ignored. */
389
+ function formatMemoryContext(value) {
390
+ const input = isRecord$1(value) ? value : {};
391
+ const facts = Array.isArray(input.facts) ? input.facts.slice(0, 20).filter(isRecord$1) : [];
392
+ const memories = Array.isArray(input.memories) ? input.memories.slice(0, 5).filter(isRecord$1) : [];
393
+ if (facts.length === 0 && memories.length === 0) return void 0;
394
+ const lines = ["<relevant-memories>"];
395
+ if (facts.length > 0) {
396
+ lines.push("Known facts:");
397
+ for (const fact of facts) lines.push(`- ${escapeXml(safeText(fact.subject, 256))} ${escapeXml(safeText(fact.predicate, 256))} ${escapeXml(safeText(fact.object, 1e3))} (since ${escapeXml(safeText(fact.since, 64))})`);
398
+ }
399
+ if (memories.length > 0) {
400
+ lines.push("Related conversations:");
401
+ for (const memory of memories) lines.push(`- [${escapeXml(safeText(memory.topic, 256))}/${escapeXml(safeText(memory.subtopic, 256))}] ${escapeXml(safeText(memory.text, 2e3))}`);
402
+ }
403
+ lines.push("</relevant-memories>");
404
+ const xml = lines.join("\n");
405
+ return xml.length <= 16384 ? xml : void 0;
406
+ }
407
+ function escapeXml(value) {
408
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&apos;");
409
+ }
410
+ function isRecord$1(value) {
411
+ return typeof value === "object" && value !== null && !Array.isArray(value);
412
+ }
413
+ function safeFinite(value) {
414
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
415
+ }
416
+ //#endregion
417
+ //#region src/auto-recall.ts
418
+ const RECALL_TIMEOUT_MS = 5e3;
419
+ /**
420
+ * Auto-recall loads tiered memory context at session start
421
+ * and formats it for injection into the agent's prompt.
422
+ *
423
+ * Called from the before_agent_start hook.
424
+ */
425
+ var AutoRecall = class {
426
+ constructor(client, config, logger) {
427
+ this.client = client;
428
+ this.config = config;
429
+ this.logger = logger;
430
+ }
431
+ /**
432
+ * Load memory context and return formatted XML for prompt injection.
433
+ * Returns prependContext string for the before_agent_start hook result.
434
+ */
435
+ async loadForPrompt(userMessage) {
436
+ if (!this.config.autoRecall) return void 0;
437
+ try {
438
+ const topicHint = extractTopicHint(userMessage);
439
+ const tier = topicHint ? 2 : 1;
440
+ const context = await withTimeout(this.client.memoryLoadContext(tier, topicHint), RECALL_TIMEOUT_MS);
441
+ const formatted = formatMemoryContext(context);
442
+ if (!formatted) {
443
+ this.logger.debug("No relevant memories found for prompt");
444
+ return;
445
+ }
446
+ this.logger.debug("Loaded memory context for prompt", {
447
+ tier,
448
+ topicHint,
449
+ factCount: Array.isArray(context.facts) ? context.facts.length : 0,
450
+ memoryCount: Array.isArray(context.memories) ? context.memories.length : 0,
451
+ tokenEstimate: typeof context.tokenEstimate === "number" ? context.tokenEstimate : 0
452
+ });
453
+ return formatted;
454
+ } catch {
455
+ this.logger.warn("Failed to load bounded memory context");
456
+ return;
457
+ }
458
+ }
459
+ };
460
+ /**
461
+ * Simple topic hint extraction from user message.
462
+ * Returns the first capitalized word or proper noun as a potential topic.
463
+ */
464
+ function extractTopicHint(message) {
465
+ const words = message.split(/\s+/);
466
+ for (let i = 1; i < words.length; i++) {
467
+ const word = words[i];
468
+ if (word && word.length > 2 && /^[A-Z]/.test(word) && !/^(The|And|But|For|With|This|That|What|How|Why|When|Where)$/.test(word)) return word.toLowerCase();
469
+ }
470
+ }
471
+ async function withTimeout(operation, timeoutMs) {
472
+ let timer;
473
+ const timeout = new Promise((_resolve, reject) => {
474
+ timer = setTimeout(() => {
475
+ reject(/* @__PURE__ */ new Error("Memory recall timed out"));
476
+ }, timeoutMs);
477
+ timer.unref();
478
+ });
479
+ try {
480
+ return await Promise.race([operation, timeout]);
481
+ } finally {
482
+ if (timer) clearTimeout(timer);
483
+ }
484
+ }
485
+ //#endregion
486
+ //#region src/ingest-epoch.ts
487
+ /** Strictly monotonic, crash-safe per-boot ingest epoch persistence. */
488
+ const DEFAULT_EPOCH_FILE = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "memory", "ingest-epoch.json");
489
+ const MAX_STATE_BYTES = 128;
490
+ function resolveIngestEpoch(opts = {}) {
491
+ const epochFile = opts.epochFile ?? DEFAULT_EPOCH_FILE;
492
+ const now = opts.now ?? Date.now();
493
+ if (!Number.isSafeInteger(now) || now < 0) throw new Error("Memory ingest epoch clock value is invalid");
494
+ const stateDir = (0, node_path.dirname)(epochFile);
495
+ (0, node_fs.mkdirSync)(stateDir, {
496
+ recursive: true,
497
+ mode: 448
498
+ });
499
+ const directoryStat = (0, node_fs.lstatSync)(stateDir);
500
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new Error("Memory ingest epoch directory is not a secure directory");
501
+ const last = readLastEpoch(epochFile);
502
+ if (last === Number.MAX_SAFE_INTEGER) throw new Error("Memory ingest epoch has reached its safe integer limit");
503
+ const epoch = Math.max(now, last + 1);
504
+ persistEpoch(epochFile, epoch);
505
+ return epoch;
506
+ }
507
+ function readLastEpoch(epochFile) {
508
+ try {
509
+ const target = (0, node_fs.lstatSync)(epochFile);
510
+ if (target.isSymbolicLink() || !target.isFile() || target.size > MAX_STATE_BYTES) throw new Error("Memory ingest epoch state file is unsafe");
511
+ } catch (error) {
512
+ if (error.code === "ENOENT") return 0;
513
+ throw error;
514
+ }
515
+ const fd = (0, node_fs.openSync)(epochFile, node_fs.constants.O_RDONLY | noFollowFlag$1());
516
+ try {
517
+ const opened = (0, node_fs.fstatSync)(fd);
518
+ if (!opened.isFile() || opened.size > MAX_STATE_BYTES) throw new Error("Memory ingest epoch state file is invalid");
519
+ const parsed = JSON.parse((0, node_fs.readFileSync)(fd, "utf8"));
520
+ const epoch = typeof parsed === "object" && parsed !== null ? parsed.epoch : void 0;
521
+ if (!Number.isSafeInteger(epoch) || epoch < 0) throw new Error("Memory ingest epoch state is corrupt");
522
+ return epoch;
523
+ } catch (error) {
524
+ if (error instanceof SyntaxError) throw new Error("Memory ingest epoch state is corrupt");
525
+ throw error;
526
+ } finally {
527
+ (0, node_fs.closeSync)(fd);
528
+ }
529
+ }
530
+ function persistEpoch(epochFile, epoch) {
531
+ const temporary = (0, node_path.join)((0, node_path.dirname)(epochFile), `.${(0, node_path.basename)(epochFile)}.${String(process.pid)}.${(0, node_crypto.randomUUID)()}.tmp`);
532
+ let fd;
533
+ try {
534
+ fd = (0, node_fs.openSync)(temporary, node_fs.constants.O_CREAT | node_fs.constants.O_EXCL | node_fs.constants.O_WRONLY | noFollowFlag$1(), 384);
535
+ (0, node_fs.writeFileSync)(fd, `${JSON.stringify({ epoch })}\n`, "utf8");
536
+ (0, node_fs.fsyncSync)(fd);
537
+ (0, node_fs.closeSync)(fd);
538
+ fd = void 0;
539
+ (0, node_fs.renameSync)(temporary, epochFile);
540
+ (0, node_fs.chmodSync)(epochFile, 384);
541
+ } catch (error) {
542
+ if (fd !== void 0) (0, node_fs.closeSync)(fd);
543
+ try {
544
+ (0, node_fs.unlinkSync)(temporary);
545
+ } catch {}
546
+ throw error;
547
+ }
548
+ }
549
+ function noFollowFlag$1() {
550
+ return "O_NOFOLLOW" in node_fs.constants ? node_fs.constants.O_NOFOLLOW : 0;
551
+ }
552
+ //#endregion
553
+ //#region src/session-backfill.ts
554
+ /**
555
+ * Session backfill — one-time ingestion of pre-existing chat history into
556
+ * cloud memory, run fire-and-forget at plugin startup (see index.ts).
557
+ *
558
+ * Reads the openclaw-chat plugin's on-disk session files directly:
559
+ * ~/.alfe/sessions/chat/{sessionId}.json
560
+ * The write side is packages/openclaw-chat/src/session-store.ts — that path
561
+ * and file shape are a read contract; keep the two in sync. The chat plugin
562
+ * is not a package dependency because its session-store functions aren't
563
+ * part of its public entry and the plugin isn't guaranteed to be installed
564
+ * alongside memory. Like the session store itself, this assumes one agent
565
+ * per machine (`alfe remove` deletes ~/.alfe, so files never outlive the
566
+ * agent they belong to).
567
+ *
568
+ * No-re-sync guarantees (in concert with services/memory):
569
+ * 1. Runs only while the server-side `sessionsBackfillSyncedAt` flag is
570
+ * unset; the flag survives integration uninstall/reinstall.
571
+ * 2. Only messages with timestamp < cutoffMs are ingested — for agents that
572
+ * already had memory installed, everything after `bootstrapSyncedAt` was
573
+ * already captured live by AutoCapture.
574
+ * 3. sessionKey = file sessionId and index = file array position, so the
575
+ * ingest consumer's per-session `lastProcessedIndex` high-water mark makes
576
+ * a crash-and-retry replay free (filtered server-side before any cost).
577
+ *
578
+ * Each session is sent as exactly ONE memoryIngest call (one SQS message,
579
+ * windowed to the most recent messages that fit the caps below). Splitting a
580
+ * session across multiple SQS messages would race the shared per-session
581
+ * high-water mark on the standard (non-FIFO) ingest queue: a transiently
582
+ * failed early chunk redelivered after a later chunk succeeds gets filtered
583
+ * out and silently lost.
584
+ */
585
+ const DEFAULT_SESSIONS_DIR = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "sessions", "chat");
586
+ const MAX_MESSAGES_PER_SESSION = 500;
587
+ const MAX_SESSION_KEY_CHARS = 256;
588
+ const MAX_MESSAGE_INDEX = 1e6;
589
+ const MAX_CONTENT_CHARS = 1e5;
590
+ /** One ingest call becomes one SQS SendMessage (256KB hard limit). */
591
+ const MAX_BATCH_BYTES = 18e4;
592
+ const MAX_SESSION_FILES = 1e4;
593
+ const MAX_SESSION_FILE_BYTES = 8 * 1024 * 1024;
594
+ const MAX_METADATA_CHARS = 256;
595
+ const MAX_DATE_MS = 864e13;
596
+ async function runSessionsBackfill(client, config, logger, opts) {
597
+ const sessionsDir = opts.sessionsDir ?? DEFAULT_SESSIONS_DIR;
598
+ let fileNames;
599
+ try {
600
+ fileNames = (await (0, node_fs_promises.readdir)(sessionsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort().slice(0, MAX_SESSION_FILES);
601
+ } catch (err) {
602
+ if (err.code === "ENOENT") {
603
+ logger.debug("sessions backfill: no sessions directory, marking complete");
604
+ try {
605
+ await client.memoryBootstrapStatusMark("sessions");
606
+ } catch {}
607
+ } else logger.warn("sessions backfill: sessions directory unreadable; will retry on next startup");
608
+ return;
609
+ }
610
+ const sessions = [];
611
+ for (const name of fileNames) {
612
+ const parsed = await parseSessionFile((0, node_path.join)(sessionsDir, name), config.captureMaxChars, opts.cutoffMs);
613
+ if (parsed) sessions.push(parsed);
614
+ }
615
+ const eligible = sessions.filter((s) => s.messages.length > 0).sort((a, b) => b.lastMessageMs - a.lastMessageMs).slice(0, config.backfillMaxSessions);
616
+ if (eligible.length === 0) {
617
+ logger.debug("sessions backfill: no eligible sessions, marking complete");
618
+ try {
619
+ await client.memoryBootstrapStatusMark("sessions");
620
+ } catch {}
621
+ return;
622
+ }
623
+ logger.info("sessions backfill: ingesting sessions", {
624
+ sessionCount: eligible.length,
625
+ messageCount: eligible.reduce((n, s) => n + s.messages.length, 0)
626
+ });
627
+ for (const session of eligible) {
628
+ const metadata = {
629
+ ...session.channel ? { channelId: session.channel } : {},
630
+ ...session.userId ? { userId: session.userId } : {}
631
+ };
632
+ try {
633
+ await client.memoryIngest(session.sessionId, session.messages, metadata);
634
+ } catch {
635
+ logger.warn("sessions backfill: ingest failed; will retry on next startup");
636
+ return;
637
+ }
638
+ }
639
+ try {
640
+ await client.memoryBootstrapStatusMark("sessions");
641
+ logger.info("sessions backfill: complete", { sessionCount: eligible.length });
642
+ } catch {
643
+ logger.warn("sessions backfill: mark failed; will retry on next startup");
644
+ }
645
+ }
646
+ /**
647
+ * Tolerant parse of a single session file. Returns null for corrupt or
648
+ * shape-mismatched files (skipped, never fatal). Message `index` is the
649
+ * original array position in the file — stable because the store is
650
+ * append-only — so retries resume idempotently server-side.
651
+ */
652
+ async function parseSessionFile(filePath, captureMaxChars, cutoffMs) {
653
+ let raw;
654
+ let handle;
655
+ try {
656
+ handle = await (0, node_fs_promises.open)(filePath, node_fs.constants.O_RDONLY | noFollowFlag());
657
+ const stat = await handle.stat();
658
+ if (!stat.isFile() || stat.size > MAX_SESSION_FILE_BYTES) return null;
659
+ raw = JSON.parse(await handle.readFile("utf8"));
660
+ } catch {
661
+ return null;
662
+ } finally {
663
+ await handle?.close();
664
+ }
665
+ if (typeof raw !== "object" || raw === null) return null;
666
+ const data = raw;
667
+ if (!isBoundedScalar(data.sessionId, MAX_SESSION_KEY_CHARS)) return null;
668
+ if (!Array.isArray(data.messages)) return null;
669
+ const maxContentChars = Math.min(captureMaxChars, MAX_CONTENT_CHARS);
670
+ let lastMessageMs = 0;
671
+ const filtered = [];
672
+ for (const [index, entry] of data.messages.entries()) {
673
+ if (index > MAX_MESSAGE_INDEX) break;
674
+ if (typeof entry !== "object" || entry === null) continue;
675
+ const msg = entry;
676
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
677
+ if (typeof msg.content !== "string" || msg.content.trim().length === 0) continue;
678
+ if (typeof msg.timestamp !== "number" || !Number.isFinite(msg.timestamp) || msg.timestamp < 0 || msg.timestamp > MAX_DATE_MS) continue;
679
+ lastMessageMs = Math.max(lastMessageMs, msg.timestamp);
680
+ if (msg.timestamp >= cutoffMs) continue;
681
+ filtered.push({
682
+ role: msg.role,
683
+ content: msg.content.length > maxContentChars ? msg.content.slice(0, maxContentChars) : msg.content,
684
+ index,
685
+ timestamp: new Date(msg.timestamp).toISOString()
686
+ });
687
+ }
688
+ return {
689
+ sessionId: data.sessionId,
690
+ channel: isBoundedScalar(data.channel, MAX_METADATA_CHARS) ? data.channel : void 0,
691
+ userId: isBoundedScalar(data.userId, MAX_METADATA_CHARS) ? data.userId : void 0,
692
+ messages: recentWindow(filtered),
693
+ lastMessageMs
694
+ };
695
+ }
696
+ function isBoundedScalar(value, maxChars) {
697
+ return typeof value === "string" && value.length > 0 && value.length <= maxChars && !hasControlCharacters(value);
698
+ }
699
+ function noFollowFlag() {
700
+ return "O_NOFOLLOW" in node_fs.constants ? node_fs.constants.O_NOFOLLOW : 0;
701
+ }
702
+ /**
703
+ * The most recent contiguous run of messages fitting both ingest caps
704
+ * (message count and serialized bytes). Walks backwards from the newest
705
+ * message and stops at the first one that would overflow the byte budget.
706
+ */
707
+ function recentWindow(messages) {
708
+ const window = [];
709
+ let bytes = 0;
710
+ for (let i = messages.length - 1; i >= 0; i--) {
711
+ if (window.length >= MAX_MESSAGES_PER_SESSION) break;
712
+ const msgBytes = Buffer.byteLength(JSON.stringify(messages[i])) + 1;
713
+ if (bytes + msgBytes > MAX_BATCH_BYTES) break;
714
+ window.unshift(messages[i]);
715
+ bytes += msgBytes;
716
+ }
717
+ return window;
718
+ }
719
+ //#endregion
720
+ //#region src/runtime.ts
721
+ const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
722
+ const PLUGIN_VERSION = typeof pkg.version === "string" ? pkg.version : "0.0.0";
723
+ const MAX_BOOTSTRAP_FILES = 200;
724
+ const TAG_SCHEMA = _sinclair_typebox.Type.Union([
725
+ _sinclair_typebox.Type.Literal("fact"),
726
+ _sinclair_typebox.Type.Literal("decision"),
727
+ _sinclair_typebox.Type.Literal("preference"),
728
+ _sinclair_typebox.Type.Literal("event"),
729
+ _sinclair_typebox.Type.Literal("discovery")
730
+ ]);
731
+ function createMemoryCloudPlugin(dependencies = {}) {
732
+ const resolveRuntimeConfig = dependencies.resolveConfig ?? _alfe_ai_config.resolveConfig;
733
+ const createClient = dependencies.createClient ?? ((config) => new _alfe_ai_agent_api_client.AgentApiClient(config));
734
+ const resolveEpoch = dependencies.resolveEpoch ?? resolveIngestEpoch;
735
+ const installErrorCapture = dependencies.installErrorCapture ?? _alfe_ai_agent_api_client.installToolErrorCapture;
736
+ return {
737
+ id: "@alfe.ai/openclaw-memory-cloud",
738
+ name: "Cloud Memory",
739
+ description: "Persistent agent memory backed by Turbopuffer and DynamoDB",
740
+ version: PLUGIN_VERSION,
741
+ kind: "memory",
742
+ register(api) {
743
+ installErrorCapture(api, { plugin: "openclaw-memory-cloud" });
744
+ const config = parseMemoryConfig(api.pluginConfig);
745
+ const state = {
746
+ client: null,
747
+ workspacePath: null,
748
+ capture: null
749
+ };
750
+ const ensureRuntime = () => {
751
+ if (state.client === null || state.workspacePath === null) {
752
+ const runtimeConfig = resolveRuntimeConfig();
753
+ state.client = createClient({
754
+ apiKey: runtimeConfig.apiKey,
755
+ apiUrl: runtimeConfig.apiUrl
756
+ });
757
+ state.workspacePath = runtimeConfig.workspacePath;
758
+ }
759
+ return {
760
+ client: state.client,
761
+ workspacePath: state.workspacePath
762
+ };
763
+ };
764
+ const captureApi = { memoryIngest: (...args) => ensureRuntime().client.memoryIngest(...args) };
765
+ const autoRecall = new AutoRecall({ memoryLoadContext: (...args) => ensureRuntime().client.memoryLoadContext(...args) }, config, api.logger);
766
+ const ensureCapture = () => {
767
+ if (!config.autoCapture || state.capture === false) return null;
768
+ if (state.capture === null) try {
769
+ state.capture = new AutoCapture(captureApi, config, api.logger, resolveEpoch());
770
+ } catch {
771
+ state.capture = false;
772
+ api.logger.warn("Memory auto-capture disabled because its monotonic epoch could not be secured");
773
+ return null;
774
+ }
775
+ return state.capture;
776
+ };
777
+ const runTool = async (action, operation) => {
778
+ try {
779
+ return await operation();
780
+ } catch (error) {
781
+ return {
782
+ status: "error",
783
+ error: classifyToolFailure(error, action)
784
+ };
785
+ }
786
+ };
787
+ api.registerMemoryPromptSection(({ availableTools }) => buildMemoryPrompt(availableTools));
788
+ api.registerTool((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
789
+ name: "memory_recall",
790
+ description: "Search private conversation memory and the per-agent knowledge graph.",
791
+ parameters: _sinclair_typebox.Type.Object({
792
+ query: _sinclair_typebox.Type.String({
793
+ minLength: 1,
794
+ maxLength: MAX_QUERY_CHARS
795
+ }),
796
+ limit: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Integer({
797
+ minimum: 1,
798
+ maximum: 50,
799
+ default: 10
800
+ })),
801
+ topic: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ maxLength: 256 })),
802
+ subtopic: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ maxLength: 256 })),
803
+ tag: _sinclair_typebox.Type.Optional(TAG_SCHEMA)
804
+ }, { additionalProperties: false }),
805
+ handler: async (params) => runTool("Memory recall", async () => {
806
+ const input = parseSearchInput(params);
807
+ return formatSearchResults(await ensureRuntime().client.memorySearch(input.query, input));
808
+ })
809
+ }), { names: ["memory_recall", "memory_search"] });
810
+ api.registerTool((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
811
+ name: "memory_store",
812
+ description: "Explicitly save one bounded item to private long-term memory.",
813
+ parameters: _sinclair_typebox.Type.Object({
814
+ text: _sinclair_typebox.Type.String({
815
+ minLength: 1,
816
+ maxLength: MAX_STORE_TEXT_CHARS
817
+ }),
818
+ topic: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ maxLength: 128 })),
819
+ subtopic: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({ maxLength: 128 })),
820
+ tag: _sinclair_typebox.Type.Optional(TAG_SCHEMA),
821
+ importance: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Number({
822
+ minimum: 0,
823
+ maximum: 1
824
+ }))
825
+ }, { additionalProperties: false }),
826
+ handler: async (params) => runTool("Memory store", async () => {
827
+ const input = parseStoreInput(params);
828
+ const memoryId = safeText((await ensureRuntime().client.memoryStore(input.text, input)).memoryId, 512);
829
+ if (!memoryId) throw new Error("Memory store response omitted the memory ID");
830
+ return {
831
+ status: "stored",
832
+ memoryId
833
+ };
834
+ })
835
+ }), { names: ["memory_store"] });
836
+ api.registerTool((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
837
+ name: "memory_learn",
838
+ description: "Ingest bounded inline text or one regular file inside the configured agent workspace.",
839
+ parameters: _sinclair_typebox.Type.Object({
840
+ content: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({
841
+ minLength: 1,
842
+ maxLength: 1e5
843
+ })),
844
+ path: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({
845
+ minLength: 1,
846
+ maxLength: 1024
847
+ })),
848
+ source: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.String({
849
+ minLength: 1,
850
+ maxLength: 512
851
+ }))
852
+ }, { additionalProperties: false }),
853
+ handler: async (params) => runTool("Memory learn", async () => {
854
+ const input = parseLearnInput(params);
855
+ const runtime = ensureRuntime();
856
+ let text;
857
+ if (input.path === void 0) {
858
+ if (input.content === void 0) throw new Error("Memory learn content is unavailable");
859
+ text = input.content;
860
+ } else text = await readWorkspaceText(runtime.workspacePath, input.path);
861
+ const result = await runtime.client.memoryLearn({
862
+ text,
863
+ source: input.source ?? input.path,
864
+ sourceType: input.path === void 0 ? "inline" : "file"
865
+ });
866
+ return {
867
+ status: "stored",
868
+ memoriesStored: safeCount(result.memoriesStored),
869
+ triplesStored: safeCount(result.triplesStored),
870
+ chunks: safeCount(result.chunks)
871
+ };
872
+ })
873
+ }), { names: ["memory_learn"] });
874
+ api.registerTool((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
875
+ name: "memory_forget",
876
+ description: "Preview up to five matching private memories, then delete only after confirming the exact returned IDs.",
877
+ parameters: _sinclair_typebox.Type.Object({
878
+ query: _sinclair_typebox.Type.String({
879
+ minLength: 1,
880
+ maxLength: MAX_QUERY_CHARS
881
+ }),
882
+ confirmMemoryIds: _sinclair_typebox.Type.Optional(_sinclair_typebox.Type.Array(_sinclair_typebox.Type.String({
883
+ minLength: 1,
884
+ maxLength: 512
885
+ }), {
886
+ minItems: 1,
887
+ maxItems: 5,
888
+ uniqueItems: true
889
+ }))
890
+ }, { additionalProperties: false }),
891
+ handler: async (params) => runTool("Memory deletion", async () => {
892
+ const input = parseForgetInput(params);
893
+ const client = ensureRuntime().client;
894
+ const matches = normalizeSearchResults(await client.memorySearch(input.query, { limit: 5 })).memories.slice(0, 5).filter((memory) => memory.id.length > 0);
895
+ const currentIds = matches.map((memory) => memory.id);
896
+ if (input.confirmMemoryIds === void 0) return {
897
+ status: "confirmation_required",
898
+ matches: matches.map((memory) => ({
899
+ id: memory.id,
900
+ text: memory.text.slice(0, 240)
901
+ })),
902
+ instruction: "Call memory_forget again with confirmMemoryIds exactly as returned, in the same order."
903
+ };
904
+ if (JSON.stringify(input.confirmMemoryIds) !== JSON.stringify(currentIds)) return {
905
+ status: "error",
906
+ error: "Matching memories changed; preview again before deleting."
907
+ };
908
+ const deletedIds = [];
909
+ const failedIds = [];
910
+ for (const memoryId of currentIds) try {
911
+ if ((await client.memoryDelete(memoryId)).deleted) deletedIds.push(memoryId);
912
+ else failedIds.push(memoryId);
913
+ } catch {
914
+ failedIds.push(memoryId);
915
+ }
916
+ return failedIds.length === 0 ? {
917
+ status: "deleted",
918
+ deletedIds
919
+ } : {
920
+ status: "error",
921
+ error: "Some memories could not be deleted.",
922
+ deletedIds,
923
+ failedIds
924
+ };
925
+ })
926
+ }), { names: ["memory_forget"] });
927
+ api.registerTool((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
928
+ name: "memory_graph",
929
+ description: "Look up bounded facts about one entity in the private knowledge graph.",
930
+ parameters: _sinclair_typebox.Type.Object({ entity: _sinclair_typebox.Type.String({
931
+ minLength: 1,
932
+ maxLength: 512
933
+ }) }, { additionalProperties: false }),
934
+ handler: async (params) => runTool("Memory graph lookup", async () => {
935
+ const { entity } = parseEntityInput(params);
936
+ const result = await ensureRuntime().client.memoryLookupEntity(entity);
937
+ const triples = Array.isArray(result.triples) ? result.triples.slice(0, 100) : [];
938
+ return {
939
+ subject: safeText(result.subject, 512),
940
+ triples: triples.map((triple) => ({
941
+ predicate: safeText(triple.predicate, 512),
942
+ object: safeText(triple.object, 2e3),
943
+ validFrom: safeText(triple.validFrom, 64),
944
+ confidence: typeof triple.confidence === "number" && Number.isFinite(triple.confidence) ? triple.confidence : 0
945
+ })),
946
+ truncated: Array.isArray(result.triples) && result.triples.length > 100
947
+ };
948
+ })
949
+ }), { names: ["memory_graph"] });
950
+ api.registerTool((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
951
+ name: "memory_stats",
952
+ description: "Get bounded private-memory storage statistics.",
953
+ parameters: _sinclair_typebox.Type.Object({}, { additionalProperties: false }),
954
+ handler: async (params) => runTool("Memory stats", async () => {
955
+ parseEmptyInput(params);
956
+ const stats = await ensureRuntime().client.memoryStats();
957
+ return {
958
+ vectorCount: safeCount(stats.vectorCount),
959
+ tripleCount: safeCount(stats.tripleCount),
960
+ storageEstimateBytes: safeCount(stats.storageEstimateBytes)
961
+ };
962
+ })
963
+ }), { names: ["memory_stats"] });
964
+ api.registerTool((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
965
+ name: "memory_navigate",
966
+ description: "Browse bounded topic and subtopic counts in private memory.",
967
+ parameters: _sinclair_typebox.Type.Object({}, { additionalProperties: false }),
968
+ handler: async (params) => runTool("Memory navigation", async () => {
969
+ parseEmptyInput(params);
970
+ const result = await ensureRuntime().client.memoryNavigate();
971
+ return {
972
+ topics: (Array.isArray(result.topics) ? result.topics.slice(0, 200) : []).map((topic) => ({
973
+ name: safeText(topic.name, 256),
974
+ tripleCount: safeCount(topic.tripleCount),
975
+ subtopics: Array.isArray(topic.subtopics) ? topic.subtopics.slice(0, 100).map((entry) => safeText(entry, 256)) : []
976
+ })),
977
+ truncated: Array.isArray(result.topics) && result.topics.length > 200
978
+ };
979
+ })
980
+ }), { names: ["memory_navigate"] });
981
+ api.on("before_agent_start", async (event) => {
982
+ const prompt = isRecord(event) && typeof event.prompt === "string" ? event.prompt : "";
983
+ const contextXml = await autoRecall.loadForPrompt(prompt.slice(0, MAX_QUERY_CHARS));
984
+ return contextXml ? { prependContext: contextXml } : void 0;
985
+ }, { priority: 10 });
986
+ api.on("message_received", (event, context) => {
987
+ trackHookMessage(ensureCapture(), "user", event, context);
988
+ });
989
+ api.on("message_sending", (event, context) => {
990
+ trackHookMessage(ensureCapture(), "assistant", event, context);
991
+ });
992
+ api.on("agent_end", async (event, context) => {
993
+ if (isRecord(event) && event.success === false) return;
994
+ const sessionKey = hookSessionKey(context);
995
+ if (sessionKey) await ensureCapture()?.onAgentEnd(sessionKey);
996
+ });
997
+ Promise.resolve().then(async () => {
998
+ const runtime = ensureRuntime();
999
+ await runStartupSync(runtime.client, runtime.workspacePath, config, api.logger);
1000
+ }).catch(() => {
1001
+ api.logger.warn("Memory startup sync failed; it will retry on next startup");
1002
+ });
1003
+ api.logger.info("memory-cloud extension registered", {
1004
+ autoCapture: config.autoCapture,
1005
+ autoRecall: config.autoRecall
1006
+ });
1007
+ }
1008
+ };
1009
+ }
1010
+ function trackHookMessage(capture, role, event, context) {
1011
+ if (!capture || !isRecord(event)) return;
1012
+ const sessionKey = hookSessionKey(context);
1013
+ const content = typeof event.content === "string" ? event.content : "";
1014
+ if (!sessionKey || content.length === 0) return;
1015
+ capture.trackMessage(sessionKey, role, content, hookMetadata(context));
1016
+ }
1017
+ function hookSessionKey(context) {
1018
+ if (!isRecord(context) || typeof context.sessionKey !== "string") return void 0;
1019
+ const value = context.sessionKey;
1020
+ return value.length > 0 && value.length <= 256 && !hasControlCharacters(value) ? value : void 0;
1021
+ }
1022
+ function hookMetadata(context) {
1023
+ if (!isRecord(context)) return void 0;
1024
+ const channelId = safeOptionalScalar(context.channelId);
1025
+ const userId = safeOptionalScalar(context.userId);
1026
+ return channelId || userId ? {
1027
+ ...channelId ? { channelId } : {},
1028
+ ...userId ? { userId } : {}
1029
+ } : void 0;
1030
+ }
1031
+ function safeOptionalScalar(value) {
1032
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && !hasControlCharacters(value) ? value : void 0;
1033
+ }
1034
+ function buildMemoryPrompt(availableTools) {
1035
+ const lines = ["## Memory"];
1036
+ if (availableTools.has("memory_recall")) lines.push("Use memory_recall to search private conversation memory and per-agent facts.");
1037
+ if (availableTools.has("memory_store")) lines.push("Use memory_store for an explicit bounded item worth retaining.");
1038
+ if (availableTools.has("memory_learn")) lines.push("Use memory_learn for bounded source text or a regular file inside the configured workspace.");
1039
+ if (availableTools.has("memory_graph")) lines.push("Use memory_graph to look up facts about one entity.");
1040
+ if (availableTools.has("memory_forget")) lines.push("memory_forget always requires a preview followed by exact ID confirmation.");
1041
+ return lines;
1042
+ }
1043
+ async function runStartupSync(client, workspacePath, config, logger) {
1044
+ let status;
1045
+ try {
1046
+ status = await client.memoryBootstrapStatus();
1047
+ } catch {
1048
+ logger.debug("Memory bootstrap status check failed; startup sync skipped");
1049
+ return;
1050
+ }
1051
+ const startupMs = Date.now();
1052
+ if (!status.synced && !await runBootstrap(client, workspacePath, logger)) return;
1053
+ if (config.backfillSessions && status.sessionsBackfillSynced === false) {
1054
+ const installedAtMs = status.syncedAt ? Date.parse(status.syncedAt) : Number.POSITIVE_INFINITY;
1055
+ await runSessionsBackfill(client, config, logger, { cutoffMs: Math.min(startupMs, Number.isFinite(installedAtMs) ? installedAtMs : Number.POSITIVE_INFINITY) });
1056
+ }
1057
+ }
1058
+ async function runBootstrap(client, workspacePath, logger) {
1059
+ const relativePaths = [];
1060
+ try {
1061
+ const stat = await (0, node_fs_promises.lstat)((0, node_path.join)(workspacePath, "MEMORY.md"));
1062
+ if (stat.isFile() && !stat.isSymbolicLink()) relativePaths.push("MEMORY.md");
1063
+ } catch {}
1064
+ try {
1065
+ const memoryDirectory = (0, node_path.join)(workspacePath, "memory");
1066
+ const stat = await (0, node_fs_promises.lstat)(memoryDirectory);
1067
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
1068
+ const entries = await (0, node_fs_promises.readdir)(memoryDirectory, { withFileTypes: true });
1069
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1070
+ if (relativePaths.length >= MAX_BOOTSTRAP_FILES) break;
1071
+ if (entry.isFile() && entry.name.endsWith(".md")) relativePaths.push(`memory/${entry.name}`);
1072
+ }
1073
+ }
1074
+ } catch {}
1075
+ for (const relativePath of relativePaths) try {
1076
+ const text = await readWorkspaceText(workspacePath, relativePath);
1077
+ if (text.trim().length > 0) await client.memoryLearn({
1078
+ text,
1079
+ source: relativePath,
1080
+ sourceType: "file"
1081
+ });
1082
+ } catch {
1083
+ logger.warn("Memory bootstrap file ingestion failed; it will retry on next startup");
1084
+ return false;
1085
+ }
1086
+ try {
1087
+ await client.memoryBootstrapStatusMark();
1088
+ logger.info("Memory workspace bootstrap complete", { fileCount: relativePaths.length });
1089
+ return true;
1090
+ } catch {
1091
+ logger.warn("Memory bootstrap completion mark failed; it will retry on next startup");
1092
+ return false;
1093
+ }
1094
+ }
1095
+ function isRecord(value) {
1096
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1097
+ }
1098
+ //#endregion
1099
+ //#region src/plugin.ts
1100
+ const plugin = createMemoryCloudPlugin();
1101
+ //#endregion
1102
+ Object.defineProperty(exports, "plugin", {
1103
+ enumerable: true,
1104
+ get: function() {
1105
+ return plugin;
1106
+ }
1107
+ });