@remnic/plugin-openclaw 1.0.0

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.
@@ -0,0 +1,434 @@
1
+ import {
2
+ log
3
+ } from "./chunk-DMGIUDBO.js";
4
+
5
+ // ../remnic-core/src/boxes.ts
6
+ import { mkdir, writeFile, readFile, readdir } from "fs/promises";
7
+ import path from "path";
8
+ import { createHash } from "crypto";
9
+ var BOX_DIR = "boxes";
10
+ var STATE_DIR = "state";
11
+ var TRACES_FILE = "traces.json";
12
+ var OPEN_BOX_STATE_FILE = "open-box.json";
13
+ function topicOverlapScore(a, b) {
14
+ if (a.length === 0 || b.length === 0) return 0;
15
+ const setA = new Set(a.map((t) => t.toLowerCase()));
16
+ const setB = new Set(b.map((t) => t.toLowerCase()));
17
+ const intersection = [...setA].filter((t) => setB.has(t)).length;
18
+ const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
19
+ return union === 0 ? 0 : intersection / union;
20
+ }
21
+ function makeBoxId() {
22
+ return `box-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
23
+ }
24
+ function makeTraceId(topics) {
25
+ const key = topics.slice().sort().join(",");
26
+ return `trace-${createHash("sha256").update(key).digest("hex").slice(0, 8)}`;
27
+ }
28
+ function serializeBoxFrontmatter(fm) {
29
+ const lines = [
30
+ "---",
31
+ `id: ${fm.id}`,
32
+ `memoryKind: ${fm.memoryKind}`,
33
+ `createdAt: ${fm.createdAt}`,
34
+ `sealedAt: ${fm.sealedAt}`,
35
+ `sealReason: ${fm.sealReason}`,
36
+ `topics: [${fm.topics.map((t) => `"${t}"`).join(", ")}]`,
37
+ `memoryIds: [${fm.memoryIds.map((m) => `"${m}"`).join(", ")}]`
38
+ ];
39
+ if (fm.sessionKey) lines.push(`sessionKey: ${fm.sessionKey}`);
40
+ if (fm.traceId) lines.push(`traceId: ${fm.traceId}`);
41
+ if (fm.goal) lines.push(`goal: ${fm.goal.replace(/[\r\n]+/g, " ")}`);
42
+ if (fm.toolsUsed?.length) lines.push(`toolsUsed: [${fm.toolsUsed.map((t) => `"${t}"`).join(", ")}]`);
43
+ if (fm.outcome) lines.push(`outcome: ${fm.outcome}`);
44
+ lines.push("---");
45
+ return lines.join("\n");
46
+ }
47
+ function parseBoxFrontmatter(raw) {
48
+ const match = raw.match(/^---\n([\s\S]*?)\n---/);
49
+ if (!match) return null;
50
+ const fmBlock = match[1];
51
+ const fm = {};
52
+ for (const line of fmBlock.split("\n")) {
53
+ const colonIdx = line.indexOf(":");
54
+ if (colonIdx === -1) continue;
55
+ fm[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
56
+ }
57
+ const parseArray = (val) => {
58
+ if (!val) return [];
59
+ const m = val.match(/\[(.*)]/);
60
+ if (!m) return [];
61
+ return m[1].split(",").map((s) => s.trim().replace(/^"|"$/g, "")).filter(Boolean);
62
+ };
63
+ const outcome = fm.outcome;
64
+ return {
65
+ id: fm.id ?? "",
66
+ memoryKind: "box",
67
+ createdAt: fm.createdAt ?? "",
68
+ sealedAt: fm.sealedAt ?? "",
69
+ sealReason: fm.sealReason ?? "forced",
70
+ sessionKey: fm.sessionKey,
71
+ topics: parseArray(fm.topics),
72
+ memoryIds: parseArray(fm.memoryIds),
73
+ traceId: fm.traceId,
74
+ goal: fm.goal || void 0,
75
+ toolsUsed: fm.toolsUsed ? parseArray(fm.toolsUsed) : void 0,
76
+ outcome: outcome && ["success", "failure", "partial", "unknown"].includes(outcome) ? outcome : void 0
77
+ };
78
+ }
79
+ var BoxBuilder = class {
80
+ baseDir;
81
+ cfg;
82
+ openBox = null;
83
+ stateLoaded = false;
84
+ constructor(baseDir, cfg) {
85
+ this.baseDir = baseDir;
86
+ this.cfg = cfg;
87
+ }
88
+ get boxBaseDir() {
89
+ return path.join(this.baseDir, BOX_DIR);
90
+ }
91
+ get stateDir() {
92
+ return path.join(this.baseDir, STATE_DIR);
93
+ }
94
+ get openBoxStatePath() {
95
+ return path.join(this.stateDir, OPEN_BOX_STATE_FILE);
96
+ }
97
+ get tracesPath() {
98
+ return path.join(this.stateDir, TRACES_FILE);
99
+ }
100
+ // ── State persistence ────────────────────────────────────────────────────
101
+ async loadOpenBox() {
102
+ if (this.stateLoaded) return;
103
+ this.stateLoaded = true;
104
+ try {
105
+ const raw = await readFile(this.openBoxStatePath, "utf-8");
106
+ this.openBox = JSON.parse(raw);
107
+ } catch {
108
+ this.openBox = null;
109
+ }
110
+ }
111
+ async saveOpenBox() {
112
+ await mkdir(this.stateDir, { recursive: true });
113
+ if (this.openBox) {
114
+ await writeFile(this.openBoxStatePath, JSON.stringify(this.openBox, null, 2), "utf-8");
115
+ } else {
116
+ try {
117
+ await writeFile(this.openBoxStatePath, "null", "utf-8");
118
+ } catch {
119
+ }
120
+ }
121
+ }
122
+ async loadTraceIndex() {
123
+ try {
124
+ const raw = await readFile(this.tracesPath, "utf-8");
125
+ const parsed = JSON.parse(raw);
126
+ parsed.traceLastSeen ??= {};
127
+ return parsed;
128
+ } catch {
129
+ return { traces: {}, boxToTrace: {}, traceTopics: {}, traceLastSeen: {} };
130
+ }
131
+ }
132
+ async saveTraceIndex(idx) {
133
+ try {
134
+ await mkdir(this.stateDir, { recursive: true });
135
+ await writeFile(this.tracesPath, JSON.stringify(idx, null, 2), "utf-8");
136
+ } catch (err) {
137
+ log.warn(`[engram/boxes] Failed to save trace index: ${err.message}`);
138
+ }
139
+ }
140
+ // ── Core logic ────────────────────────────────────────────────────────────
141
+ /**
142
+ * Called after each extraction run.
143
+ * Decides whether to seal the current open box and/or start a new one.
144
+ */
145
+ async onExtraction(event) {
146
+ if (!this.cfg.memoryBoxesEnabled) return;
147
+ await this.loadOpenBox();
148
+ const newTopics = event.topics.filter(Boolean);
149
+ const now = new Date(event.timestamp);
150
+ const nowMs = now.getTime();
151
+ if (this.openBox) {
152
+ const lastActivity = new Date(this.openBox.lastActivityAt).getTime();
153
+ const timeGapMs = nowMs - lastActivity;
154
+ const overlap = topicOverlapScore(this.openBox.topics, newTopics);
155
+ const topicShifted = newTopics.length > 0 && overlap < this.cfg.boxTopicShiftThreshold;
156
+ const timeExpired = timeGapMs >= this.cfg.boxTimeGapMs;
157
+ const tooManyMemories = this.openBox.memoryIds.length + event.memoryIds.length > this.cfg.boxMaxMemories;
158
+ if (tooManyMemories) {
159
+ const topicSet = /* @__PURE__ */ new Set([...this.openBox.topics, ...newTopics]);
160
+ this.openBox.topics = [...topicSet];
161
+ this.openBox.memoryIds.push(...event.memoryIds);
162
+ if (event.toolsUsed?.length) {
163
+ const toolSet = /* @__PURE__ */ new Set([...this.openBox.toolsUsed ?? [], ...event.toolsUsed]);
164
+ this.openBox.toolsUsed = [...toolSet];
165
+ }
166
+ await this.sealCurrent("max_memories");
167
+ } else if (topicShifted) {
168
+ await this.sealCurrent("topic_shift");
169
+ this.openBox = this.newBox(event, now.toISOString());
170
+ await this.saveOpenBox();
171
+ } else if (timeExpired) {
172
+ await this.sealCurrent("time_gap");
173
+ this.openBox = this.newBox(event, now.toISOString());
174
+ await this.saveOpenBox();
175
+ } else {
176
+ this.openBox.memoryIds.push(...event.memoryIds);
177
+ const topicSet = /* @__PURE__ */ new Set([...this.openBox.topics, ...newTopics]);
178
+ this.openBox.topics = [...topicSet];
179
+ this.openBox.lastActivityAt = now.toISOString();
180
+ if (event.toolsUsed?.length) {
181
+ const toolSet = /* @__PURE__ */ new Set([...this.openBox.toolsUsed ?? [], ...event.toolsUsed]);
182
+ this.openBox.toolsUsed = [...toolSet];
183
+ }
184
+ await this.saveOpenBox();
185
+ }
186
+ } else {
187
+ this.openBox = this.newBox(event, now.toISOString());
188
+ if (this.openBox.memoryIds.length > this.cfg.boxMaxMemories) {
189
+ await this.sealCurrent("max_memories");
190
+ } else {
191
+ await this.saveOpenBox();
192
+ }
193
+ }
194
+ }
195
+ newBox(event, ts) {
196
+ return {
197
+ id: makeBoxId(),
198
+ createdAt: ts,
199
+ lastActivityAt: ts,
200
+ topics: event.topics.filter(Boolean),
201
+ memoryIds: [...event.memoryIds],
202
+ goal: event.goal,
203
+ toolsUsed: event.toolsUsed?.length ? [...event.toolsUsed] : void 0
204
+ };
205
+ }
206
+ /**
207
+ * Seal the current open box and write it to disk.
208
+ * Also runs trace weaving if enabled.
209
+ */
210
+ async sealCurrent(reason) {
211
+ await this.loadOpenBox();
212
+ if (!this.openBox) return null;
213
+ const box = this.openBox;
214
+ this.openBox = null;
215
+ if (box.memoryIds.length === 0 && box.topics.length === 0) {
216
+ await this.saveOpenBox();
217
+ return null;
218
+ }
219
+ const sealedAt = (/* @__PURE__ */ new Date()).toISOString();
220
+ const day = sealedAt.slice(0, 10);
221
+ const dir = path.join(this.boxBaseDir, day);
222
+ await mkdir(dir, { recursive: true });
223
+ let traceId;
224
+ if (this.cfg.traceWeaverEnabled && box.topics.length > 0) {
225
+ traceId = await this.resolveTrace(box.id, box.topics);
226
+ }
227
+ const fm = {
228
+ id: box.id,
229
+ memoryKind: "box",
230
+ createdAt: box.createdAt,
231
+ sealedAt,
232
+ sealReason: reason,
233
+ topics: box.topics,
234
+ memoryIds: box.memoryIds,
235
+ traceId,
236
+ goal: box.goal,
237
+ toolsUsed: box.toolsUsed?.length ? box.toolsUsed : void 0,
238
+ outcome: "unknown"
239
+ };
240
+ const content = `${serializeBoxFrontmatter(fm)}
241
+
242
+ <!-- Topics: ${box.topics.join(", ")} | Memories: ${box.memoryIds.length} -->
243
+ `;
244
+ const filePath = path.join(dir, `${box.id}.md`);
245
+ await writeFile(filePath, content, "utf-8");
246
+ log.debug(`[boxes] sealed box ${box.id} (${reason}): ${box.memoryIds.length} memories, topics=[${box.topics.join(",")}]`);
247
+ await this.saveOpenBox();
248
+ return box.id;
249
+ }
250
+ // ── Trace Weaving ─────────────────────────────────────────────────────────
251
+ /**
252
+ * Find an existing trace that matches box topics, or create a new trace.
253
+ * Returns the traceId to assign to this box.
254
+ */
255
+ async resolveTrace(boxId, topics) {
256
+ const idx = await this.loadTraceIndex();
257
+ const lookbackMs = this.cfg.traceWeaverLookbackDays * 24 * 60 * 60 * 1e3;
258
+ const cutoff = new Date(Date.now() - lookbackMs);
259
+ let bestTraceId;
260
+ let bestScore = 0;
261
+ for (const [tid, traceTopics] of Object.entries(idx.traceTopics)) {
262
+ const lastSeen = idx.traceLastSeen[tid];
263
+ if (lastSeen && new Date(lastSeen) < cutoff) continue;
264
+ const score = topicOverlapScore(topics, traceTopics);
265
+ if (score >= this.cfg.traceWeaverOverlapThreshold && score > bestScore) {
266
+ bestScore = score;
267
+ bestTraceId = tid;
268
+ }
269
+ }
270
+ const traceId = bestTraceId ?? makeTraceId(topics);
271
+ const now = (/* @__PURE__ */ new Date()).toISOString();
272
+ if (!idx.traces[traceId]) idx.traces[traceId] = [];
273
+ idx.traces[traceId].push(boxId);
274
+ idx.boxToTrace[boxId] = traceId;
275
+ idx.traceLastSeen[traceId] = now;
276
+ if (idx.traceTopics[traceId]) {
277
+ const merged = /* @__PURE__ */ new Set([...idx.traceTopics[traceId], ...topics]);
278
+ idx.traceTopics[traceId] = [...merged];
279
+ } else {
280
+ idx.traceTopics[traceId] = [...topics];
281
+ }
282
+ await this.saveTraceIndex(idx);
283
+ return traceId;
284
+ }
285
+ // ── Recall ────────────────────────────────────────────────────────────────
286
+ /**
287
+ * Read all sealed boxes from the last N days for recall injection.
288
+ */
289
+ async readRecentBoxes(days) {
290
+ const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
291
+ const cutoffDateStr = cutoff.toISOString().slice(0, 10);
292
+ let topEntries;
293
+ try {
294
+ topEntries = await readdir(this.boxBaseDir, { withFileTypes: true, encoding: "utf-8" });
295
+ } catch {
296
+ return [];
297
+ }
298
+ const recentDirs = topEntries.filter((e) => e.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(e.name) && e.name >= cutoffDateStr).map((e) => path.join(this.boxBaseDir, e.name));
299
+ const legacyEntries = topEntries.filter(
300
+ (e) => !e.isDirectory() || !/^\d{4}-\d{2}-\d{2}$/.test(e.name)
301
+ );
302
+ const boxes = [];
303
+ const readDir = async (dir) => {
304
+ let files;
305
+ try {
306
+ files = (await readdir(dir)).filter((f) => f.endsWith(".md"));
307
+ } catch {
308
+ return;
309
+ }
310
+ const results = await Promise.all(
311
+ files.map(async (f) => {
312
+ try {
313
+ const raw = await readFile(path.join(dir, f), "utf-8");
314
+ const parsed = parseBoxFrontmatter(raw);
315
+ return parsed && new Date(parsed.sealedAt) >= cutoff ? parsed : null;
316
+ } catch {
317
+ return null;
318
+ }
319
+ })
320
+ );
321
+ for (const r of results) {
322
+ if (r !== null) boxes.push(r);
323
+ }
324
+ };
325
+ await Promise.all(recentDirs.map(readDir));
326
+ for (const e of legacyEntries) {
327
+ const full = path.join(this.boxBaseDir, e.name);
328
+ if (e.isDirectory()) {
329
+ await readDir(full);
330
+ } else if (e.name.endsWith(".md")) {
331
+ try {
332
+ const raw = await readFile(full, "utf-8");
333
+ const parsed = parseBoxFrontmatter(raw);
334
+ if (parsed && new Date(parsed.sealedAt) >= cutoff) boxes.push(parsed);
335
+ } catch {
336
+ }
337
+ }
338
+ }
339
+ boxes.sort((a, b) => new Date(b.sealedAt).getTime() - new Date(a.sealedAt).getTime());
340
+ return boxes;
341
+ }
342
+ };
343
+
344
+ // ../remnic-core/src/recall-tokenization.ts
345
+ function normalizeRecallTokens(value, extraStopWords = []) {
346
+ const stopWords = /* @__PURE__ */ new Set([
347
+ "the",
348
+ "and",
349
+ "for",
350
+ "with",
351
+ "from",
352
+ "into",
353
+ "that",
354
+ "this",
355
+ "why",
356
+ "did",
357
+ ...extraStopWords
358
+ ]);
359
+ return value.toLowerCase().split(/[^a-z0-9]+/).map((token) => token.trim()).filter((token) => token.length >= 3 && !stopWords.has(token));
360
+ }
361
+ function countRecallTokenOverlap(queryTokens, value, extraStopWords = []) {
362
+ if (!value) return 0;
363
+ const tokens = new Set(normalizeRecallTokens(value, extraStopWords));
364
+ let matches = 0;
365
+ for (const token of queryTokens) {
366
+ if (tokens.has(token)) matches += 1;
367
+ }
368
+ return matches;
369
+ }
370
+
371
+ // ../remnic-core/src/store-contract.ts
372
+ function isRecord(value) {
373
+ return typeof value === "object" && value !== null && !Array.isArray(value);
374
+ }
375
+ function assertString(value, field) {
376
+ if (typeof value !== "string" || value.trim().length === 0) {
377
+ throw new Error(`${field} must be a non-empty string`);
378
+ }
379
+ return value.trim();
380
+ }
381
+ function optionalString(value) {
382
+ if (typeof value !== "string" || value.trim().length === 0) return void 0;
383
+ return value.trim();
384
+ }
385
+ function assertSafePathSegment(value, field) {
386
+ if (value === "." || value === ".." || value.includes("/") || value.includes("\\")) {
387
+ throw new Error(`${field} must be a safe path segment`);
388
+ }
389
+ return value;
390
+ }
391
+ function assertIsoRecordedAt(value, field = "recordedAt") {
392
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) {
393
+ throw new Error(`${field} must be an ISO timestamp`);
394
+ }
395
+ return value;
396
+ }
397
+ function recordStoreDay(recordedAt) {
398
+ const day = recordedAt.slice(0, 10);
399
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) {
400
+ throw new Error("recordedAt must start with a valid YYYY-MM-DD date");
401
+ }
402
+ return day;
403
+ }
404
+ function optionalStringArray(value, field) {
405
+ if (value === void 0) return void 0;
406
+ if (!Array.isArray(value)) throw new Error(`${field} must be an array of strings`);
407
+ const items = value.map((item, index) => assertString(item, `${field}[${index}]`));
408
+ return items.length > 0 ? items : void 0;
409
+ }
410
+ function validateStringRecord(raw, field = "metadata") {
411
+ if (raw === void 0) return void 0;
412
+ if (!isRecord(raw)) throw new Error(`${field} must be an object of strings`);
413
+ const out = {};
414
+ for (const [key, value] of Object.entries(raw)) {
415
+ if (typeof value !== "string") throw new Error(`${field} must be an object of strings`);
416
+ out[key] = value;
417
+ }
418
+ return Object.keys(out).length > 0 ? out : void 0;
419
+ }
420
+
421
+ export {
422
+ topicOverlapScore,
423
+ BoxBuilder,
424
+ normalizeRecallTokens,
425
+ countRecallTokenOverlap,
426
+ isRecord,
427
+ assertString,
428
+ optionalString,
429
+ assertSafePathSegment,
430
+ assertIsoRecordedAt,
431
+ recordStoreDay,
432
+ optionalStringArray,
433
+ validateStringRecord
434
+ };
@@ -0,0 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };