@alfe.ai/openclaw-memory-cloud 0.0.37 → 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.
- package/README.md +37 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -60
- package/dist/index.js +2 -396
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.d.cts +59 -0
- package/dist/plugin.d.cts.map +1 -0
- package/dist/plugin.d.ts +59 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +2 -0
- package/dist/plugin2.cjs +1107 -0
- package/dist/plugin2.d.cts +2 -0
- package/dist/plugin2.d.ts +2 -0
- package/dist/plugin2.js +1104 -0
- package/dist/plugin2.js.map +1 -0
- package/openclaw.plugin.json +6 -5
- package/package.json +27 -7
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -309
- package/dist/auto-capture.d.ts +0 -45
- package/dist/auto-capture.d.ts.map +0 -1
- package/dist/auto-capture.js +0 -101
- package/dist/auto-capture.js.map +0 -1
- package/dist/auto-recall.d.ts +0 -22
- package/dist/auto-recall.d.ts.map +0 -1
- package/dist/auto-recall.js +0 -63
- package/dist/auto-recall.js.map +0 -1
- package/dist/formatter.d.ts +0 -7
- package/dist/formatter.d.ts.map +0 -1
- package/dist/formatter.js +0 -27
- package/dist/formatter.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/ingest-epoch.d.ts +0 -38
- package/dist/ingest-epoch.d.ts.map +0 -1
- package/dist/ingest-epoch.js +0 -66
- package/dist/ingest-epoch.js.map +0 -1
- package/dist/session-backfill.d.ts +0 -54
- package/dist/session-backfill.d.ts.map +0 -1
- package/dist/session-backfill.js +0 -192
- package/dist/session-backfill.js.map +0 -1
- package/dist/types.d.ts +0 -113
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/src/__tests__/auto-capture.test.ts +0 -115
- package/src/__tests__/ingest-epoch.test.ts +0 -67
- package/src/__tests__/session-backfill.test.ts +0 -289
- package/src/auto-capture.ts +0 -108
- package/src/auto-recall.ts +0 -66
- package/src/formatter.ts +0 -30
- package/src/index.ts +0 -464
- package/src/ingest-epoch.ts +0 -78
- package/src/session-backfill.ts +0 -220
- package/src/types.ts +0 -93
- package/sst-env.d.ts +0 -10
- package/tsconfig.json +0 -20
package/dist/plugin2.cjs
ADDED
|
@@ -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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
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
|
+
});
|