@remnic/plugin-openclaw 1.0.3 → 1.0.4

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,960 @@
1
+ import {
2
+ StorageManager
3
+ } from "./chunk-5VTGFKKU.js";
4
+ import {
5
+ countRecallTokenOverlap,
6
+ normalizeRecallTokens
7
+ } from "./chunk-YHH3SXKD.js";
8
+ import {
9
+ log
10
+ } from "./chunk-UFU5GGGA.js";
11
+
12
+ // ../remnic-core/src/connectors/codex-materialize-runner.ts
13
+ import path2 from "path";
14
+ import { existsSync as existsSync2 } from "fs";
15
+
16
+ // ../remnic-core/src/connectors/codex-materialize.ts
17
+ import {
18
+ createHash
19
+ } from "crypto";
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ readdirSync,
24
+ readFileSync,
25
+ renameSync,
26
+ rmSync,
27
+ statSync,
28
+ unlinkSync,
29
+ writeFileSync
30
+ } from "fs";
31
+ import path from "path";
32
+ var MATERIALIZE_VERSION = 1;
33
+ var SENTINEL_FILE = ".remnic-managed";
34
+ var TMP_DIR = ".remnic-tmp";
35
+ var ROLLOUT_SUBDIR = "rollout_summaries";
36
+ function materializeForNamespace(namespace, options) {
37
+ const logger = options.logger ?? {
38
+ info: (msg) => log.info(`[codex-materialize] ${msg}`),
39
+ warn: (msg) => log.warn(`[codex-materialize] ${msg}`),
40
+ debug: (msg) => log.debug(`[codex-materialize] ${msg}`)
41
+ };
42
+ const codexHome = resolveCodexHome(options.codexHome);
43
+ const memoriesDir = path.join(codexHome, "memories");
44
+ const now = options.now ?? /* @__PURE__ */ new Date();
45
+ const maxSummaryTokens = typeof options.maxSummaryTokens === "number" && options.maxSummaryTokens >= 0 ? options.maxSummaryTokens : 4500;
46
+ const rolloutRetentionDays = typeof options.rolloutRetentionDays === "number" && options.rolloutRetentionDays >= 0 ? options.rolloutRetentionDays : 30;
47
+ const sentinelPath = path.join(memoriesDir, SENTINEL_FILE);
48
+ const existingSentinel = readSentinel(sentinelPath);
49
+ if (!existingSentinel) {
50
+ if (existsSync(memoriesDir)) {
51
+ logger.warn(
52
+ `sentinel ${SENTINEL_FILE} missing in ${memoriesDir}; skipping materialization to preserve hand-edits`
53
+ );
54
+ } else {
55
+ logger.debug?.(
56
+ `skipping materialization \u2014 ${memoriesDir} does not exist (user not opted in)`
57
+ );
58
+ }
59
+ return {
60
+ namespace,
61
+ memoriesDir,
62
+ wrote: false,
63
+ skippedNoSentinel: true,
64
+ skippedIdempotent: false,
65
+ filesWritten: [],
66
+ contentHash: ""
67
+ };
68
+ }
69
+ mkdirSync(memoriesDir, { recursive: true });
70
+ const memories = [...options.memories];
71
+ const rolloutsSupplied = options.rolloutSummaries !== void 0;
72
+ const rolloutSummaries = options.rolloutSummaries ?? [];
73
+ const retainedRollouts = pruneRollouts(rolloutSummaries, rolloutRetentionDays, now);
74
+ const dedupedRollouts = [];
75
+ const seenNames = /* @__PURE__ */ new Map();
76
+ const parseTs = (value) => {
77
+ if (!value) return Number.NEGATIVE_INFINITY;
78
+ const parsed = Date.parse(value);
79
+ return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
80
+ };
81
+ for (const r of retainedRollouts) {
82
+ const name = `${sanitizeSlug(r.slug)}.md`;
83
+ const existingIdx = seenNames.get(name);
84
+ if (existingIdx === void 0) {
85
+ seenNames.set(name, dedupedRollouts.length);
86
+ dedupedRollouts.push(r);
87
+ continue;
88
+ }
89
+ const existing = dedupedRollouts[existingIdx];
90
+ if (parseTs(r.updatedAt) > parseTs(existing.updatedAt)) {
91
+ dedupedRollouts[existingIdx] = r;
92
+ }
93
+ }
94
+ const memorySummary = renderMemorySummary({
95
+ namespace,
96
+ memories,
97
+ rolloutSummaries: dedupedRollouts,
98
+ maxTokens: maxSummaryTokens
99
+ });
100
+ const memoryMd = renderMemoryMd({
101
+ namespace,
102
+ memories,
103
+ rolloutSummaries: dedupedRollouts
104
+ });
105
+ const validation = validateMemoryMd(memoryMd);
106
+ if (!validation.valid) {
107
+ const reason = validation.errors.join("; ");
108
+ logger.warn(`MEMORY.md failed schema validation: ${reason}`);
109
+ throw new Error(`codex-materialize: MEMORY.md schema validation failed: ${reason}`);
110
+ }
111
+ const rawMemories = renderRawMemories({ memories });
112
+ const rolloutFiles = dedupedRollouts.map((r) => ({
113
+ name: `${sanitizeSlug(r.slug)}.md`,
114
+ body: renderRolloutSummary(r)
115
+ }));
116
+ const hash = computeContentHash({
117
+ namespace,
118
+ memorySummary,
119
+ memoryMd,
120
+ rawMemories,
121
+ rolloutFiles
122
+ });
123
+ if (existingSentinel.content_hash === hash) {
124
+ const requiredFiles = [
125
+ path.join(memoriesDir, "memory_summary.md"),
126
+ path.join(memoriesDir, "MEMORY.md"),
127
+ path.join(memoriesDir, "raw_memories.md"),
128
+ ...rolloutFiles.map((r) => path.join(memoriesDir, ROLLOUT_SUBDIR, r.name))
129
+ ];
130
+ const allPresent = requiredFiles.every((f) => existsSync(f));
131
+ if (allPresent) {
132
+ logger.debug?.(`no-op materialization for namespace=${namespace} (hash unchanged)`);
133
+ return {
134
+ namespace,
135
+ memoriesDir,
136
+ wrote: false,
137
+ skippedNoSentinel: false,
138
+ skippedIdempotent: true,
139
+ filesWritten: [],
140
+ contentHash: hash
141
+ };
142
+ }
143
+ logger.debug?.(
144
+ `hash unchanged for namespace=${namespace} but managed file missing \u2014 forcing rewrite`
145
+ );
146
+ }
147
+ const runTag = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
148
+ const tmpDir = path.join(memoriesDir, `${TMP_DIR}-${runTag}`);
149
+ const TMP_STALE_MS = 60 * 60 * 1e3;
150
+ const wallClockMs = Date.now();
151
+ try {
152
+ for (const entry of readdirSync(memoriesDir, { withFileTypes: true })) {
153
+ if (!entry.isDirectory()) continue;
154
+ if (!entry.name.startsWith(TMP_DIR)) continue;
155
+ const stalePath = path.join(memoriesDir, entry.name);
156
+ try {
157
+ const stat = statSync(stalePath);
158
+ if (wallClockMs - stat.mtimeMs < TMP_STALE_MS) continue;
159
+ rmSync(stalePath, { recursive: true, force: true });
160
+ } catch {
161
+ }
162
+ }
163
+ } catch {
164
+ }
165
+ mkdirSync(tmpDir, { recursive: true });
166
+ mkdirSync(path.join(tmpDir, ROLLOUT_SUBDIR), { recursive: true });
167
+ const filesWritten = [];
168
+ writeFileSync(path.join(tmpDir, "memory_summary.md"), memorySummary);
169
+ filesWritten.push("memory_summary.md");
170
+ writeFileSync(path.join(tmpDir, "MEMORY.md"), memoryMd);
171
+ filesWritten.push("MEMORY.md");
172
+ writeFileSync(path.join(tmpDir, "raw_memories.md"), rawMemories);
173
+ filesWritten.push("raw_memories.md");
174
+ for (const rollout of rolloutFiles) {
175
+ writeFileSync(path.join(tmpDir, ROLLOUT_SUBDIR, rollout.name), rollout.body);
176
+ filesWritten.push(path.join(ROLLOUT_SUBDIR, rollout.name));
177
+ }
178
+ for (const rel of ["memory_summary.md", "MEMORY.md", "raw_memories.md"]) {
179
+ const src = path.join(tmpDir, rel);
180
+ const dest = path.join(memoriesDir, rel);
181
+ renameSync(src, dest);
182
+ }
183
+ const destRolloutsDir = path.join(memoriesDir, ROLLOUT_SUBDIR);
184
+ mkdirSync(destRolloutsDir, { recursive: true });
185
+ if (rolloutsSupplied) {
186
+ const retainedRolloutNames = new Set(rolloutFiles.map((r) => r.name));
187
+ try {
188
+ for (const entry of readdirSync(destRolloutsDir, { withFileTypes: true })) {
189
+ if (!entry.isFile()) continue;
190
+ if (!entry.name.endsWith(".md")) continue;
191
+ if (retainedRolloutNames.has(entry.name)) continue;
192
+ try {
193
+ unlinkSync(path.join(destRolloutsDir, entry.name));
194
+ } catch {
195
+ }
196
+ }
197
+ } catch {
198
+ }
199
+ }
200
+ for (const rollout of rolloutFiles) {
201
+ const src = path.join(tmpDir, ROLLOUT_SUBDIR, rollout.name);
202
+ const dest = path.join(destRolloutsDir, rollout.name);
203
+ renameSync(src, dest);
204
+ }
205
+ const sentinel = {
206
+ version: MATERIALIZE_VERSION,
207
+ namespace,
208
+ updated_at: now.toISOString(),
209
+ content_hash: hash
210
+ };
211
+ writeFileSync(sentinelPath, `${JSON.stringify(sentinel, null, 2)}
212
+ `);
213
+ try {
214
+ rmSync(tmpDir, { recursive: true, force: true });
215
+ } catch {
216
+ }
217
+ logger.info(
218
+ `materialized namespace=${namespace} files=${filesWritten.length} hash=${hash.slice(0, 12)}`
219
+ );
220
+ return {
221
+ namespace,
222
+ memoriesDir,
223
+ wrote: true,
224
+ skippedNoSentinel: false,
225
+ skippedIdempotent: false,
226
+ filesWritten,
227
+ contentHash: hash
228
+ };
229
+ }
230
+ function renderMemorySummary(ctx) {
231
+ const lines = [];
232
+ lines.push("# Memory Summary");
233
+ lines.push("");
234
+ lines.push(`_namespace: ${ctx.namespace}_`);
235
+ lines.push(`_source: remnic_`);
236
+ lines.push("");
237
+ const highValue = selectSummaryMemories(ctx.memories, 12);
238
+ if (highValue.length > 0) {
239
+ lines.push("## Top memories");
240
+ lines.push("");
241
+ for (const mem of highValue) {
242
+ lines.push(`- ${oneLineSummary(mem)}`);
243
+ }
244
+ lines.push("");
245
+ }
246
+ if (ctx.rolloutSummaries.length > 0) {
247
+ lines.push("## Recent rollouts");
248
+ lines.push("");
249
+ const sorted = [...ctx.rolloutSummaries].sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")).slice(0, 5);
250
+ for (const r of sorted) {
251
+ const when = r.updatedAt ? ` (${r.updatedAt})` : "";
252
+ lines.push(`- ${r.slug}${when}`);
253
+ }
254
+ lines.push("");
255
+ }
256
+ const full = lines.join("\n").replace(/\n+$/u, "\n");
257
+ return truncateToTokenBudget(full, ctx.maxTokens);
258
+ }
259
+ function renderMemoryMd(ctx) {
260
+ const lines = [];
261
+ lines.push(`# Task Group: ${ctx.namespace}`);
262
+ lines.push(`scope: ${ctx.namespace}`);
263
+ lines.push(`applies_to: cwd=*; reuse_rule=namespace-match`);
264
+ lines.push("");
265
+ const byCategory = groupMemoriesByCategory(ctx.memories);
266
+ let taskIndex = 1;
267
+ if (byCategory.size === 0) {
268
+ lines.push(`## Task ${taskIndex}: baseline \u2014 no memories yet`);
269
+ lines.push("");
270
+ lines.push("### rollout_summary_files");
271
+ for (const r of ctx.rolloutSummaries) {
272
+ lines.push(
273
+ `- rollout_summaries/${sanitizeSlug(r.slug)}.md (cwd=${r.cwd ?? "*"}, rollout_path=${r.rolloutPath ?? ""}, updated_at=${r.updatedAt ?? ""}, thread_id=${r.threadId ?? ""})`
274
+ );
275
+ }
276
+ if (ctx.rolloutSummaries.length === 0) {
277
+ lines.push("- (none)");
278
+ }
279
+ lines.push("");
280
+ lines.push("### keywords");
281
+ lines.push(`- ${ctx.namespace}`);
282
+ lines.push("");
283
+ taskIndex += 1;
284
+ } else {
285
+ for (const [category, mems] of byCategory) {
286
+ lines.push(`## Task ${taskIndex}: ${category} memories, outcome=surface-to-codex`);
287
+ lines.push("");
288
+ lines.push("### rollout_summary_files");
289
+ const relevantRollouts = ctx.rolloutSummaries.slice(0, 5);
290
+ if (relevantRollouts.length === 0) {
291
+ lines.push("- (none)");
292
+ } else {
293
+ for (const r of relevantRollouts) {
294
+ lines.push(
295
+ `- rollout_summaries/${sanitizeSlug(r.slug)}.md (cwd=${r.cwd ?? "*"}, rollout_path=${r.rolloutPath ?? ""}, updated_at=${r.updatedAt ?? ""}, thread_id=${r.threadId ?? ""})`
296
+ );
297
+ }
298
+ }
299
+ lines.push("");
300
+ lines.push("### keywords");
301
+ const keywords = collectKeywords(mems, category, ctx.namespace);
302
+ lines.push(`- ${keywords.join(", ")}`);
303
+ lines.push("");
304
+ taskIndex += 1;
305
+ }
306
+ }
307
+ lines.push("## User preferences");
308
+ const prefs = pickCategory(ctx.memories, ["preference"]);
309
+ if (prefs.length === 0) {
310
+ lines.push("- (none recorded)");
311
+ } else {
312
+ for (const pref of prefs.slice(0, 20)) {
313
+ lines.push(`- ${oneLineSummary(pref)}`);
314
+ }
315
+ }
316
+ lines.push("");
317
+ lines.push("## Reusable knowledge");
318
+ const knowledge = pickCategory(ctx.memories, ["fact", "decision", "principle", "rule", "skill"]);
319
+ if (knowledge.length === 0) {
320
+ lines.push("- (none recorded)");
321
+ } else {
322
+ for (const mem of knowledge.slice(0, 30)) {
323
+ lines.push(`- ${oneLineSummary(mem)}`);
324
+ }
325
+ }
326
+ lines.push("");
327
+ lines.push("## Failures and how to do differently");
328
+ const corrections = pickCategory(ctx.memories, ["correction"]);
329
+ if (corrections.length === 0) {
330
+ lines.push("- (none recorded)");
331
+ } else {
332
+ for (const mem of corrections.slice(0, 20)) {
333
+ lines.push(`- ${oneLineSummary(mem)}`);
334
+ }
335
+ }
336
+ lines.push("");
337
+ return lines.join("\n");
338
+ }
339
+ function renderRawMemories(ctx) {
340
+ const sorted = [...ctx.memories].sort((a, b) => {
341
+ const aUpdated = a.frontmatter.updated ?? a.frontmatter.created ?? "";
342
+ const bUpdated = b.frontmatter.updated ?? b.frontmatter.created ?? "";
343
+ return bUpdated.localeCompare(aUpdated);
344
+ });
345
+ const lines = ["# Raw Memories", "", "_source: remnic \u2014 latest first_", ""];
346
+ for (const mem of sorted) {
347
+ const fm = mem.frontmatter;
348
+ const id = fm.id ?? "unknown";
349
+ const category = fm.category ?? "unknown";
350
+ const updated = fm.updated ?? fm.created ?? "";
351
+ lines.push(`## ${id} (${category}, updated=${updated})`);
352
+ lines.push("");
353
+ lines.push(mem.content.trim());
354
+ lines.push("");
355
+ }
356
+ return lines.join("\n");
357
+ }
358
+ function renderRolloutSummary(input) {
359
+ const lines = [];
360
+ lines.push(`# Rollout Summary: ${input.slug}`);
361
+ lines.push("");
362
+ const meta = [];
363
+ if (input.cwd) meta.push(`cwd=${input.cwd}`);
364
+ if (input.rolloutPath) meta.push(`rollout_path=${input.rolloutPath}`);
365
+ if (input.updatedAt) meta.push(`updated_at=${input.updatedAt}`);
366
+ if (input.threadId) meta.push(`thread_id=${input.threadId}`);
367
+ if (meta.length > 0) {
368
+ lines.push(`_${meta.join("; ")}_`);
369
+ lines.push("");
370
+ }
371
+ if (input.keywords && input.keywords.length > 0) {
372
+ lines.push(`**keywords:** ${input.keywords.join(", ")}`);
373
+ lines.push("");
374
+ }
375
+ lines.push(input.body.trim());
376
+ lines.push("");
377
+ return lines.join("\n");
378
+ }
379
+ function validateMemoryMd(content) {
380
+ const errors = [];
381
+ const lines = content.split(/\r?\n/u);
382
+ const taskGroupIndex = lines.findIndex((l) => /^#\s+Task Group:\s+\S+/u.test(l));
383
+ if (taskGroupIndex === -1) {
384
+ errors.push("missing `# Task Group:` header");
385
+ } else {
386
+ const tail = lines.slice(taskGroupIndex + 1, taskGroupIndex + 5);
387
+ if (!tail.some((l) => /^scope:\s*\S+/u.test(l))) {
388
+ errors.push("missing `scope:` line under Task Group header");
389
+ }
390
+ if (!tail.some((l) => /^applies_to:\s*\S+/u.test(l))) {
391
+ errors.push("missing `applies_to:` line under Task Group header");
392
+ }
393
+ }
394
+ const taskHeaders = lines.filter((l) => /^##\s+Task\s+\d+:/u.test(l));
395
+ if (taskHeaders.length === 0) {
396
+ errors.push("at least one `## Task N:` section is required");
397
+ }
398
+ const sectionRegex = /^##\s+/u;
399
+ for (let i = 0; i < lines.length; i++) {
400
+ if (!/^##\s+Task\s+\d+:/u.test(lines[i])) continue;
401
+ let hasRollout = false;
402
+ let hasKeywords = false;
403
+ for (let j = i + 1; j < lines.length; j++) {
404
+ if (sectionRegex.test(lines[j])) break;
405
+ if (/^###\s+rollout_summary_files\s*$/u.test(lines[j])) hasRollout = true;
406
+ if (/^###\s+keywords\s*$/u.test(lines[j])) hasKeywords = true;
407
+ }
408
+ if (!hasRollout) errors.push(`task block at line ${i + 1} missing \`### rollout_summary_files\``);
409
+ if (!hasKeywords) errors.push(`task block at line ${i + 1} missing \`### keywords\``);
410
+ }
411
+ const requiredSections = [
412
+ /^##\s+User preferences\s*$/u,
413
+ /^##\s+Reusable knowledge\s*$/u,
414
+ /^##\s+Failures and how to do differently\s*$/u
415
+ ];
416
+ for (const re of requiredSections) {
417
+ if (!lines.some((l) => re.test(l))) {
418
+ errors.push(`missing required section: ${re.source}`);
419
+ }
420
+ }
421
+ return { valid: errors.length === 0, errors };
422
+ }
423
+ function resolveCodexHome(override) {
424
+ if (override && override.trim().length > 0) return override;
425
+ const fromEnv = process.env.CODEX_HOME;
426
+ if (fromEnv && fromEnv.trim().length > 0) return fromEnv;
427
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
428
+ return path.join(home, ".codex");
429
+ }
430
+ function readSentinel(sentinelPath) {
431
+ if (!existsSync(sentinelPath)) return null;
432
+ try {
433
+ const raw = readFileSync(sentinelPath, "utf-8");
434
+ const parsed = JSON.parse(raw);
435
+ if (typeof parsed !== "object" || parsed === null) return null;
436
+ return {
437
+ version: typeof parsed.version === "number" ? parsed.version : MATERIALIZE_VERSION,
438
+ namespace: typeof parsed.namespace === "string" ? parsed.namespace : "",
439
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : "",
440
+ content_hash: typeof parsed.content_hash === "string" ? parsed.content_hash : ""
441
+ };
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+ function selectSummaryMemories(memories, limit) {
447
+ const scored = memories.filter((m) => !m.frontmatter.status || m.frontmatter.status === "active").map((m) => {
448
+ const confidence = typeof m.frontmatter.confidence === "number" ? m.frontmatter.confidence : 0;
449
+ const importance = typeof m.frontmatter.importance === "object" && m.frontmatter.importance !== null && typeof m.frontmatter.importance.score === "number" ? m.frontmatter.importance.score ?? 0 : 0;
450
+ const updated = m.frontmatter.updated ?? m.frontmatter.created ?? "";
451
+ return { memory: m, score: importance * 2 + confidence, updated };
452
+ });
453
+ scored.sort((a, b) => {
454
+ if (b.score !== a.score) return b.score - a.score;
455
+ return b.updated.localeCompare(a.updated);
456
+ });
457
+ return scored.slice(0, limit).map((s) => s.memory);
458
+ }
459
+ function oneLineSummary(memory) {
460
+ const raw = memory.content.replace(/\s+/gu, " ").trim();
461
+ if (raw.length <= 160) return raw;
462
+ return `${raw.slice(0, 157)}...`;
463
+ }
464
+ function groupMemoriesByCategory(memories) {
465
+ const map = /* @__PURE__ */ new Map();
466
+ for (const memory of memories) {
467
+ if (memory.frontmatter.status && memory.frontmatter.status !== "active") continue;
468
+ const category = memory.frontmatter.category ?? "unknown";
469
+ const list = map.get(category) ?? [];
470
+ list.push(memory);
471
+ map.set(category, list);
472
+ }
473
+ return map;
474
+ }
475
+ function pickCategory(memories, categories) {
476
+ const allowed = new Set(categories);
477
+ return memories.filter(
478
+ (m) => (!m.frontmatter.status || m.frontmatter.status === "active") && allowed.has(m.frontmatter.category ?? "")
479
+ );
480
+ }
481
+ function collectKeywords(memories, category, namespace) {
482
+ const keywords = /* @__PURE__ */ new Set();
483
+ keywords.add(category);
484
+ keywords.add(namespace);
485
+ for (const mem of memories.slice(0, 10)) {
486
+ for (const tag of mem.frontmatter.tags ?? []) {
487
+ if (typeof tag === "string" && tag.trim().length > 0) keywords.add(tag.trim());
488
+ }
489
+ }
490
+ return [...keywords].slice(0, 16);
491
+ }
492
+ function pruneRollouts(rollouts, retentionDays, now) {
493
+ if (retentionDays < 0) return rollouts;
494
+ const cutoffMs = now.getTime() - retentionDays * 24 * 60 * 60 * 1e3;
495
+ return rollouts.filter((r) => {
496
+ if (!r.updatedAt) return true;
497
+ const t = Date.parse(r.updatedAt);
498
+ if (!Number.isFinite(t)) return true;
499
+ return t >= cutoffMs;
500
+ });
501
+ }
502
+ function sanitizeSlug(slug) {
503
+ return slug.toLowerCase().replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 96) || "rollout";
504
+ }
505
+ function approximateTokenCount(text) {
506
+ const trimmed = text.trim();
507
+ if (trimmed.length === 0) return 0;
508
+ return trimmed.split(/\s+/u).length;
509
+ }
510
+ function truncateToTokenBudget(text, maxTokens) {
511
+ if (maxTokens <= 0) return "";
512
+ if (approximateTokenCount(text) <= maxTokens) return text;
513
+ const lineMarker = "_[truncated for summary budget]_";
514
+ const tailMarker = "[truncated]";
515
+ const lineMarkerTokens = approximateTokenCount(lineMarker);
516
+ const tailMarkerTokens = approximateTokenCount(tailMarker);
517
+ const lines = text.split(/\r?\n/u);
518
+ const lineBudget = Math.max(0, maxTokens - lineMarkerTokens);
519
+ while (lines.length > 0 && approximateTokenCount(lines.join("\n")) > lineBudget) {
520
+ lines.pop();
521
+ }
522
+ lines.push(lineMarker);
523
+ let result = lines.join("\n");
524
+ if (approximateTokenCount(result) > maxTokens) {
525
+ const tokens = result.split(/\s+/u);
526
+ const keep = Math.max(0, maxTokens - tailMarkerTokens);
527
+ result = keep > 0 ? `${tokens.slice(0, keep).join(" ")} ${tailMarker}` : tailMarker;
528
+ }
529
+ return result;
530
+ }
531
+ function computeContentHash(input) {
532
+ const hash = createHash("sha256");
533
+ hash.update(`v${MATERIALIZE_VERSION}
534
+ `);
535
+ hash.update(`namespace=${input.namespace}
536
+ `);
537
+ hash.update("---memory_summary---\n");
538
+ hash.update(input.memorySummary);
539
+ hash.update("\n---memory_md---\n");
540
+ hash.update(input.memoryMd);
541
+ hash.update("\n---raw_memories---\n");
542
+ hash.update(input.rawMemories);
543
+ const sortedRollouts = [...input.rolloutFiles].sort((a, b) => a.name.localeCompare(b.name));
544
+ for (const r of sortedRollouts) {
545
+ hash.update(`
546
+ ---rollout:${r.name}---
547
+ `);
548
+ hash.update(r.body);
549
+ }
550
+ return hash.digest("hex");
551
+ }
552
+
553
+ // ../remnic-core/src/connectors/codex-materialize-runner.ts
554
+ async function runCodexMaterialize(options) {
555
+ const cfg = options.config;
556
+ if (!cfg.codexMaterializeMemories) {
557
+ log.debug(`[codex-materialize] skipped \u2014 codexMaterializeMemories=false`);
558
+ return null;
559
+ }
560
+ if (options.reason === "session_end" && cfg.codexMaterializeOnSessionEnd === false) {
561
+ log.debug(
562
+ `[codex-materialize] skipped \u2014 session-end disabled via codexMaterializeOnSessionEnd=false`
563
+ );
564
+ return null;
565
+ }
566
+ const namespace = resolveNamespace(options.namespace, cfg);
567
+ const memoryDir = options.memoryDir ?? cfg.memoryDir;
568
+ if (!memoryDir) {
569
+ log.warn(`[codex-materialize] skipped \u2014 no memoryDir available`);
570
+ return null;
571
+ }
572
+ let memories;
573
+ if (options.memories) {
574
+ memories = options.memories;
575
+ } else {
576
+ const nsDir = resolveNamespaceDir(memoryDir, namespace, cfg);
577
+ const storage = new StorageManager(nsDir);
578
+ try {
579
+ memories = await storage.readAllMemories();
580
+ } catch (error) {
581
+ log.warn(
582
+ `[codex-materialize] skipped \u2014 failed to read memories from ${nsDir}: ${error instanceof Error ? error.message : String(error)}`
583
+ );
584
+ return null;
585
+ }
586
+ }
587
+ const result = materializeForNamespace(namespace, {
588
+ memories,
589
+ codexHome: options.codexHome,
590
+ maxSummaryTokens: cfg.codexMaterializeMaxSummaryTokens,
591
+ rolloutRetentionDays: cfg.codexMaterializeRolloutRetentionDays,
592
+ rolloutSummaries: options.rolloutSummaries,
593
+ now: options.now
594
+ });
595
+ if (options.reason) {
596
+ log.debug(
597
+ `[codex-materialize] ran reason=${options.reason} wrote=${result.wrote} files=${result.filesWritten.length}`
598
+ );
599
+ }
600
+ return result;
601
+ }
602
+ async function runPostConsolidationMaterialize(logPrefix, options) {
603
+ if (!options.config.codexMaterializeMemories) return null;
604
+ if (!options.config.codexMaterializeOnConsolidation) return null;
605
+ try {
606
+ return await runCodexMaterialize({
607
+ config: options.config,
608
+ namespace: options.namespace,
609
+ memories: options.memories,
610
+ memoryDir: options.memoryDir,
611
+ codexHome: options.codexHome,
612
+ rolloutSummaries: options.rolloutSummaries,
613
+ now: options.now,
614
+ reason: "consolidation"
615
+ });
616
+ } catch (error) {
617
+ log.warn(
618
+ `${logPrefix} Codex materialize post-hook failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`
619
+ );
620
+ return null;
621
+ }
622
+ }
623
+ function resolveNamespace(override, cfg) {
624
+ const requested = (override ?? cfg.codexMaterializeNamespace ?? "auto").trim();
625
+ if (requested.length === 0 || requested === "auto") {
626
+ return cfg.defaultNamespace && cfg.defaultNamespace.length > 0 ? cfg.defaultNamespace : "default";
627
+ }
628
+ return requested;
629
+ }
630
+ function resolveNamespaceDir(memoryDir, namespace, cfg) {
631
+ if (!cfg.namespacesEnabled) return memoryDir;
632
+ const ns = namespace || cfg.defaultNamespace || "default";
633
+ const namespacedRoot = path2.join(memoryDir, "namespaces", ns);
634
+ if (ns === cfg.defaultNamespace) {
635
+ return existsSync2(namespacedRoot) ? namespacedRoot : memoryDir;
636
+ }
637
+ return namespacedRoot;
638
+ }
639
+
640
+ // ../remnic-core/src/memory-extension-host/host-discovery.ts
641
+ import { readdir, readFile, lstat, realpath } from "fs/promises";
642
+ import path3 from "path";
643
+ var REMNIC_EXTENSIONS_TOTAL_TOKEN_LIMIT = 5e3;
644
+ var MAX_EXAMPLES_PER_EXTENSION = 10;
645
+ var VALID_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
646
+ var VALID_MEMORY_TYPES = /* @__PURE__ */ new Set(["fact", "preference", "procedure", "reference"]);
647
+ async function discoverMemoryExtensions(root, log2) {
648
+ let rootStat;
649
+ try {
650
+ rootStat = await lstat(root);
651
+ } catch {
652
+ return [];
653
+ }
654
+ if (rootStat.isSymbolicLink()) {
655
+ let resolved;
656
+ try {
657
+ resolved = await realpath(root);
658
+ } catch {
659
+ return [];
660
+ }
661
+ let expectedParent;
662
+ try {
663
+ expectedParent = await realpath(path3.resolve(path3.dirname(root)));
664
+ } catch {
665
+ return [];
666
+ }
667
+ if (!resolved.startsWith(expectedParent + path3.sep) && resolved !== expectedParent) {
668
+ log2.warn?.(
669
+ `[memory-extensions] root "${root}" is a symlink resolving outside the expected parent directory, skipping`
670
+ );
671
+ return [];
672
+ }
673
+ try {
674
+ rootStat = await lstat(resolved);
675
+ } catch {
676
+ return [];
677
+ }
678
+ }
679
+ if (!rootStat.isDirectory()) {
680
+ return [];
681
+ }
682
+ let entries;
683
+ try {
684
+ entries = await readdir(root);
685
+ } catch {
686
+ return [];
687
+ }
688
+ const extensions = [];
689
+ for (const entry of entries) {
690
+ const entryPath = path3.join(root, entry);
691
+ let entryStat;
692
+ try {
693
+ entryStat = await lstat(entryPath);
694
+ } catch {
695
+ continue;
696
+ }
697
+ if (entryStat.isSymbolicLink()) {
698
+ log2.warn?.(
699
+ `[memory-extensions] skipping "${entry}": symlinks are not followed for security`
700
+ );
701
+ continue;
702
+ }
703
+ if (!entryStat.isDirectory()) continue;
704
+ if (!VALID_SLUG_RE.test(entry)) {
705
+ log2.warn?.(
706
+ `[memory-extensions] skipping "${entry}": invalid slug (must be lowercase alphanumeric + hyphens, 1-64 chars)`
707
+ );
708
+ continue;
709
+ }
710
+ const instructionsPath = path3.join(entryPath, "instructions.md");
711
+ if (await isSymlink(instructionsPath)) {
712
+ log2.warn?.(
713
+ `[memory-extensions] skipping "${entry}": instructions.md is a symlink`
714
+ );
715
+ continue;
716
+ }
717
+ let instructions;
718
+ try {
719
+ instructions = await readFile(instructionsPath, "utf-8");
720
+ } catch {
721
+ log2.warn?.(
722
+ `[memory-extensions] skipping "${entry}": missing instructions.md`
723
+ );
724
+ continue;
725
+ }
726
+ let schema;
727
+ const schemaPath = path3.join(entryPath, "schema.json");
728
+ if (await isSymlink(schemaPath)) {
729
+ log2.warn?.(
730
+ `[memory-extensions] "${entry}": schema.json is a symlink, ignoring schema`
731
+ );
732
+ } else {
733
+ try {
734
+ const schemaRaw = await readFile(schemaPath, "utf-8");
735
+ const parsed = JSON.parse(schemaRaw);
736
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
737
+ schema = validateSchema(parsed);
738
+ } else {
739
+ log2.warn?.(
740
+ `[memory-extensions] "${entry}": schema.json is not a valid object, ignoring schema`
741
+ );
742
+ }
743
+ } catch (err) {
744
+ if (isFileNotFoundError(err)) {
745
+ } else {
746
+ log2.warn?.(
747
+ `[memory-extensions] "${entry}": malformed schema.json, ignoring schema`
748
+ );
749
+ }
750
+ }
751
+ }
752
+ const examplesPaths = [];
753
+ const examplesDir = path3.join(entryPath, "examples");
754
+ try {
755
+ const exampleEntries = await readdir(examplesDir);
756
+ const mdFiles = exampleEntries.filter((f) => f.endsWith(".md")).sort().slice(0, MAX_EXAMPLES_PER_EXTENSION);
757
+ for (const f of mdFiles) {
758
+ examplesPaths.push(path3.join(examplesDir, f));
759
+ }
760
+ } catch {
761
+ }
762
+ extensions.push({
763
+ name: entry,
764
+ root: entryPath,
765
+ instructionsPath,
766
+ instructions,
767
+ schema,
768
+ examplesPaths
769
+ });
770
+ }
771
+ extensions.sort((a, b) => {
772
+ if (a.name < b.name) return -1;
773
+ if (a.name > b.name) return 1;
774
+ return 0;
775
+ });
776
+ return extensions;
777
+ }
778
+ function validateSchema(raw) {
779
+ const memoryTypes = (() => {
780
+ if (!Array.isArray(raw.memoryTypes)) return void 0;
781
+ const valid = raw.memoryTypes.filter(
782
+ (t) => typeof t === "string" && VALID_MEMORY_TYPES.has(t)
783
+ );
784
+ return valid.length > 0 ? valid : void 0;
785
+ })();
786
+ const groupingHints = (() => {
787
+ if (!Array.isArray(raw.groupingHints)) return void 0;
788
+ const valid = raw.groupingHints.filter(
789
+ (h) => typeof h === "string" && h.length > 0
790
+ );
791
+ return valid.length > 0 ? valid : void 0;
792
+ })();
793
+ const version = typeof raw.version === "string" && raw.version.length > 0 ? raw.version : void 0;
794
+ return {
795
+ ...memoryTypes ? { memoryTypes } : {},
796
+ ...groupingHints ? { groupingHints } : {},
797
+ ...version ? { version } : {}
798
+ };
799
+ }
800
+ function isFileNotFoundError(err) {
801
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
802
+ }
803
+ async function isSymlink(filePath) {
804
+ try {
805
+ const s = await lstat(filePath);
806
+ return s.isSymbolicLink();
807
+ } catch {
808
+ return false;
809
+ }
810
+ }
811
+ function resolveExtensionsRoot(config) {
812
+ if (config.memoryExtensionsRoot.length > 0) {
813
+ return config.memoryExtensionsRoot;
814
+ }
815
+ return path3.join(path3.dirname(config.memoryDir), "memory_extensions");
816
+ }
817
+
818
+ // ../remnic-core/src/memory-extension-host/render-extensions-block.ts
819
+ function estimateTokens(text) {
820
+ return Math.ceil(text.length / 4);
821
+ }
822
+ function renderExtensionsBlock(extensions) {
823
+ if (extensions.length === 0) return "";
824
+ const header = `## Active memory extensions
825
+
826
+ You are running with the following third-party memory extensions. Each
827
+ extension's \`instructions.md\` tells you how to interpret memories that
828
+ extension produces or curates.
829
+
830
+ `;
831
+ let budget = REMNIC_EXTENSIONS_TOTAL_TOKEN_LIMIT;
832
+ budget -= estimateTokens(header);
833
+ const inlined = [];
834
+ const omitted = [];
835
+ for (const ext of extensions) {
836
+ const block = `### remnic-extension/${ext.name}
837
+ \`\`\`
838
+ ${ext.instructions}
839
+ \`\`\`
840
+
841
+ `;
842
+ const cost = estimateTokens(block);
843
+ if (cost <= budget) {
844
+ inlined.push(block);
845
+ budget -= cost;
846
+ } else {
847
+ omitted.push(ext.name);
848
+ }
849
+ }
850
+ let result = header;
851
+ result += inlined.join("");
852
+ if (omitted.length > 0) {
853
+ result += `> **Note:** ${omitted.length} extension(s) omitted due to token budget: ${omitted.join(", ")}
854
+ `;
855
+ }
856
+ return result;
857
+ }
858
+ function renderExtensionsFooter(extensions) {
859
+ if (extensions.length === 0) return "";
860
+ const names = extensions.map((ext) => ext.name).join(", ");
861
+ return `Active extensions: ${names}`;
862
+ }
863
+
864
+ // ../remnic-core/src/semantic-consolidation.ts
865
+ function findSimilarClusters(memories, config) {
866
+ const excluded = new Set(config.excludeCategories);
867
+ const byCategory = /* @__PURE__ */ new Map();
868
+ for (const m of memories) {
869
+ const cat = m.frontmatter.category;
870
+ if (excluded.has(cat)) continue;
871
+ if (m.frontmatter.status && m.frontmatter.status !== "active") continue;
872
+ const list = byCategory.get(cat) ?? [];
873
+ list.push(m);
874
+ byCategory.set(cat, list);
875
+ }
876
+ const clusters = [];
877
+ let totalCandidates = 0;
878
+ for (const [category, mems] of byCategory) {
879
+ if (totalCandidates >= config.maxPerRun) break;
880
+ const tokenized = mems.map((m) => ({
881
+ memory: m,
882
+ tokens: new Set(normalizeRecallTokens(m.content, []))
883
+ }));
884
+ const clustered = /* @__PURE__ */ new Set();
885
+ for (let i = 0; i < tokenized.length && totalCandidates < config.maxPerRun; i++) {
886
+ if (clustered.has(tokenized[i].memory.frontmatter.id)) continue;
887
+ const cluster = [tokenized[i].memory];
888
+ let totalOverlap = 0;
889
+ let comparisons = 0;
890
+ for (let j = i + 1; j < tokenized.length; j++) {
891
+ if (clustered.has(tokenized[j].memory.frontmatter.id)) continue;
892
+ const aTokens = tokenized[i].tokens;
893
+ const bTokens = tokenized[j].tokens;
894
+ if (aTokens.size === 0 || bTokens.size === 0) continue;
895
+ const overlap = countRecallTokenOverlap(aTokens, [...bTokens].join(" "));
896
+ const maxTokens = Math.max(aTokens.size, bTokens.size);
897
+ const score = maxTokens > 0 ? overlap / maxTokens : 0;
898
+ if (score >= config.threshold) {
899
+ cluster.push(tokenized[j].memory);
900
+ totalOverlap += score;
901
+ comparisons++;
902
+ if (totalCandidates + cluster.length >= config.maxPerRun) break;
903
+ }
904
+ }
905
+ if (cluster.length >= config.minClusterSize) {
906
+ for (const m of cluster) clustered.add(m.frontmatter.id);
907
+ clusters.push({
908
+ category,
909
+ memories: cluster,
910
+ overlapScore: comparisons > 0 ? totalOverlap / comparisons : 0
911
+ });
912
+ totalCandidates += cluster.length;
913
+ }
914
+ }
915
+ }
916
+ return clusters;
917
+ }
918
+ function buildConsolidationPrompt(cluster) {
919
+ const memoryTexts = cluster.memories.map(
920
+ (m, i) => `Memory ${i + 1} (${m.frontmatter.id}, created ${m.frontmatter.created}):
921
+ ${m.content}`
922
+ ).join("\n\n");
923
+ return `You are a memory consolidation system. The following ${cluster.memories.length} memories in the "${cluster.category}" category contain overlapping information.
924
+
925
+ Synthesize them into ONE canonical memory that:
926
+ 1. Preserves ALL unique information from every source memory
927
+ 2. Removes redundancy and repetition
928
+ 3. Uses clear, concise language
929
+ 4. Maintains the same category and tone
930
+ 5. Does NOT add information that isn't in the sources
931
+
932
+ ${memoryTexts}
933
+
934
+ Write ONLY the consolidated memory content (no metadata, no explanation, no preamble):`;
935
+ }
936
+ function parseConsolidationResponse(response) {
937
+ return response.trim();
938
+ }
939
+ async function buildExtensionsBlockForConsolidation(config) {
940
+ if (!config.memoryExtensionsEnabled) return "";
941
+ const root = resolveExtensionsRoot(config);
942
+ const extensions = await discoverMemoryExtensions(root, log);
943
+ if (extensions.length === 0) return "";
944
+ return renderExtensionsBlock(extensions);
945
+ }
946
+ async function materializeAfterSemanticConsolidation(options) {
947
+ return runPostConsolidationMaterialize("[semantic-consolidation]", options);
948
+ }
949
+
950
+ export {
951
+ discoverMemoryExtensions,
952
+ resolveExtensionsRoot,
953
+ renderExtensionsFooter,
954
+ runPostConsolidationMaterialize,
955
+ findSimilarClusters,
956
+ buildConsolidationPrompt,
957
+ parseConsolidationResponse,
958
+ buildExtensionsBlockForConsolidation,
959
+ materializeAfterSemanticConsolidation
960
+ };