@ulrichc1/sparn 1.1.1 → 1.2.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.
- package/README.md +52 -7
- package/dist/cli/index.cjs +567 -26
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +565 -25
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/index.cjs +728 -159
- package/dist/daemon/index.cjs.map +1 -1
- package/dist/daemon/index.js +697 -150
- package/dist/daemon/index.js.map +1 -1
- package/dist/index.cjs +214 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +213 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +957 -0
- package/dist/mcp/index.cjs.map +1 -0
- package/dist/mcp/index.d.cts +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +934 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +8 -3
package/dist/daemon/index.js
CHANGED
|
@@ -1,5 +1,672 @@
|
|
|
1
1
|
// src/daemon/index.ts
|
|
2
|
-
import { appendFileSync, existsSync, unlinkSync } from "fs";
|
|
2
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync2, unlinkSync } from "fs";
|
|
3
|
+
|
|
4
|
+
// src/core/kv-memory.ts
|
|
5
|
+
import { copyFileSync, existsSync } from "fs";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
function createBackup(dbPath) {
|
|
8
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
9
|
+
const backupPath = `${dbPath}.backup-${timestamp}`;
|
|
10
|
+
try {
|
|
11
|
+
copyFileSync(dbPath, backupPath);
|
|
12
|
+
console.log(`\u2713 Database backed up to: ${backupPath}`);
|
|
13
|
+
return backupPath;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(`Warning: Could not create backup: ${error}`);
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function createKVMemory(dbPath) {
|
|
20
|
+
let db;
|
|
21
|
+
try {
|
|
22
|
+
db = new Database(dbPath);
|
|
23
|
+
const integrityCheck = db.pragma("quick_check", { simple: true });
|
|
24
|
+
if (integrityCheck !== "ok") {
|
|
25
|
+
console.error("\u26A0 Database corruption detected!");
|
|
26
|
+
if (existsSync(dbPath)) {
|
|
27
|
+
const backupPath = createBackup(dbPath);
|
|
28
|
+
if (backupPath) {
|
|
29
|
+
console.log(`Backup created at: ${backupPath}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
console.log("Attempting database recovery...");
|
|
33
|
+
db.close();
|
|
34
|
+
db = new Database(dbPath);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("\u26A0 Database error detected:", error);
|
|
38
|
+
if (existsSync(dbPath)) {
|
|
39
|
+
createBackup(dbPath);
|
|
40
|
+
console.log("Creating new database...");
|
|
41
|
+
}
|
|
42
|
+
db = new Database(dbPath);
|
|
43
|
+
}
|
|
44
|
+
db.pragma("journal_mode = WAL");
|
|
45
|
+
db.exec(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS entries_index (
|
|
47
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
48
|
+
hash TEXT UNIQUE NOT NULL,
|
|
49
|
+
timestamp INTEGER NOT NULL,
|
|
50
|
+
score REAL NOT NULL DEFAULT 0.0 CHECK(score >= 0.0 AND score <= 1.0),
|
|
51
|
+
ttl INTEGER NOT NULL CHECK(ttl >= 0),
|
|
52
|
+
state TEXT NOT NULL CHECK(state IN ('silent', 'ready', 'active')),
|
|
53
|
+
accessCount INTEGER NOT NULL DEFAULT 0 CHECK(accessCount >= 0),
|
|
54
|
+
isBTSP INTEGER NOT NULL DEFAULT 0 CHECK(isBTSP IN (0, 1)),
|
|
55
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
|
56
|
+
);
|
|
57
|
+
`);
|
|
58
|
+
db.exec(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS entries_value (
|
|
60
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
61
|
+
content TEXT NOT NULL,
|
|
62
|
+
tags TEXT,
|
|
63
|
+
metadata TEXT,
|
|
64
|
+
FOREIGN KEY (id) REFERENCES entries_index(id) ON DELETE CASCADE
|
|
65
|
+
);
|
|
66
|
+
`);
|
|
67
|
+
db.exec(`
|
|
68
|
+
CREATE TABLE IF NOT EXISTS optimization_stats (
|
|
69
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
+
timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
|
71
|
+
tokens_before INTEGER NOT NULL,
|
|
72
|
+
tokens_after INTEGER NOT NULL,
|
|
73
|
+
entries_pruned INTEGER NOT NULL,
|
|
74
|
+
duration_ms INTEGER NOT NULL
|
|
75
|
+
);
|
|
76
|
+
`);
|
|
77
|
+
db.exec(`
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_entries_state ON entries_index(state);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_entries_score ON entries_index(score DESC);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_entries_hash ON entries_index(hash);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
|
|
83
|
+
`);
|
|
84
|
+
const putIndexStmt = db.prepare(`
|
|
85
|
+
INSERT OR REPLACE INTO entries_index
|
|
86
|
+
(id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
|
|
87
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
88
|
+
`);
|
|
89
|
+
const putValueStmt = db.prepare(`
|
|
90
|
+
INSERT OR REPLACE INTO entries_value
|
|
91
|
+
(id, content, tags, metadata)
|
|
92
|
+
VALUES (?, ?, ?, ?)
|
|
93
|
+
`);
|
|
94
|
+
const getStmt = db.prepare(`
|
|
95
|
+
SELECT
|
|
96
|
+
i.id, i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
|
|
97
|
+
v.content, v.tags, v.metadata
|
|
98
|
+
FROM entries_index i
|
|
99
|
+
JOIN entries_value v ON i.id = v.id
|
|
100
|
+
WHERE i.id = ?
|
|
101
|
+
`);
|
|
102
|
+
const deleteIndexStmt = db.prepare("DELETE FROM entries_index WHERE id = ?");
|
|
103
|
+
const deleteValueStmt = db.prepare("DELETE FROM entries_value WHERE id = ?");
|
|
104
|
+
return {
|
|
105
|
+
async put(entry) {
|
|
106
|
+
const transaction = db.transaction(() => {
|
|
107
|
+
putIndexStmt.run(
|
|
108
|
+
entry.id,
|
|
109
|
+
entry.hash,
|
|
110
|
+
entry.timestamp,
|
|
111
|
+
entry.score,
|
|
112
|
+
entry.ttl,
|
|
113
|
+
entry.state,
|
|
114
|
+
entry.accessCount,
|
|
115
|
+
entry.isBTSP ? 1 : 0
|
|
116
|
+
);
|
|
117
|
+
putValueStmt.run(
|
|
118
|
+
entry.id,
|
|
119
|
+
entry.content,
|
|
120
|
+
JSON.stringify(entry.tags),
|
|
121
|
+
JSON.stringify(entry.metadata)
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
transaction();
|
|
125
|
+
},
|
|
126
|
+
async get(id) {
|
|
127
|
+
const row = getStmt.get(id);
|
|
128
|
+
if (!row) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const r = row;
|
|
132
|
+
return {
|
|
133
|
+
id: r.id,
|
|
134
|
+
content: r.content,
|
|
135
|
+
hash: r.hash,
|
|
136
|
+
timestamp: r.timestamp,
|
|
137
|
+
score: r.score,
|
|
138
|
+
ttl: r.ttl,
|
|
139
|
+
state: r.state,
|
|
140
|
+
accessCount: r.accessCount,
|
|
141
|
+
tags: r.tags ? JSON.parse(r.tags) : [],
|
|
142
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : {},
|
|
143
|
+
isBTSP: r.isBTSP === 1
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
async query(filters) {
|
|
147
|
+
let sql = `
|
|
148
|
+
SELECT
|
|
149
|
+
i.id, i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
|
|
150
|
+
v.content, v.tags, v.metadata
|
|
151
|
+
FROM entries_index i
|
|
152
|
+
JOIN entries_value v ON i.id = v.id
|
|
153
|
+
WHERE 1=1
|
|
154
|
+
`;
|
|
155
|
+
const params = [];
|
|
156
|
+
if (filters.state) {
|
|
157
|
+
sql += " AND i.state = ?";
|
|
158
|
+
params.push(filters.state);
|
|
159
|
+
}
|
|
160
|
+
if (filters.minScore !== void 0) {
|
|
161
|
+
sql += " AND i.score >= ?";
|
|
162
|
+
params.push(filters.minScore);
|
|
163
|
+
}
|
|
164
|
+
if (filters.maxScore !== void 0) {
|
|
165
|
+
sql += " AND i.score <= ?";
|
|
166
|
+
params.push(filters.maxScore);
|
|
167
|
+
}
|
|
168
|
+
if (filters.isBTSP !== void 0) {
|
|
169
|
+
sql += " AND i.isBTSP = ?";
|
|
170
|
+
params.push(filters.isBTSP ? 1 : 0);
|
|
171
|
+
}
|
|
172
|
+
sql += " ORDER BY i.score DESC";
|
|
173
|
+
if (filters.limit) {
|
|
174
|
+
sql += " LIMIT ?";
|
|
175
|
+
params.push(filters.limit);
|
|
176
|
+
}
|
|
177
|
+
if (filters.offset) {
|
|
178
|
+
sql += " OFFSET ?";
|
|
179
|
+
params.push(filters.offset);
|
|
180
|
+
}
|
|
181
|
+
const stmt = db.prepare(sql);
|
|
182
|
+
const rows = stmt.all(...params);
|
|
183
|
+
return rows.map((row) => {
|
|
184
|
+
const r = row;
|
|
185
|
+
return {
|
|
186
|
+
id: r.id,
|
|
187
|
+
content: r.content,
|
|
188
|
+
hash: r.hash,
|
|
189
|
+
timestamp: r.timestamp,
|
|
190
|
+
score: r.score,
|
|
191
|
+
ttl: r.ttl,
|
|
192
|
+
state: r.state,
|
|
193
|
+
accessCount: r.accessCount,
|
|
194
|
+
tags: r.tags ? JSON.parse(r.tags) : [],
|
|
195
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : {},
|
|
196
|
+
isBTSP: r.isBTSP === 1
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
async delete(id) {
|
|
201
|
+
const transaction = db.transaction(() => {
|
|
202
|
+
deleteIndexStmt.run(id);
|
|
203
|
+
deleteValueStmt.run(id);
|
|
204
|
+
});
|
|
205
|
+
transaction();
|
|
206
|
+
},
|
|
207
|
+
async list() {
|
|
208
|
+
const stmt = db.prepare("SELECT id FROM entries_index");
|
|
209
|
+
const rows = stmt.all();
|
|
210
|
+
return rows.map((r) => r.id);
|
|
211
|
+
},
|
|
212
|
+
async compact() {
|
|
213
|
+
const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
|
|
214
|
+
db.exec("DELETE FROM entries_index WHERE ttl <= 0");
|
|
215
|
+
db.exec("VACUUM");
|
|
216
|
+
const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
|
|
217
|
+
return before.count - after.count;
|
|
218
|
+
},
|
|
219
|
+
async close() {
|
|
220
|
+
db.close();
|
|
221
|
+
},
|
|
222
|
+
async recordOptimization(stats) {
|
|
223
|
+
const stmt = db.prepare(`
|
|
224
|
+
INSERT INTO optimization_stats (timestamp, tokens_before, tokens_after, entries_pruned, duration_ms)
|
|
225
|
+
VALUES (?, ?, ?, ?, ?)
|
|
226
|
+
`);
|
|
227
|
+
stmt.run(
|
|
228
|
+
stats.timestamp,
|
|
229
|
+
stats.tokens_before,
|
|
230
|
+
stats.tokens_after,
|
|
231
|
+
stats.entries_pruned,
|
|
232
|
+
stats.duration_ms
|
|
233
|
+
);
|
|
234
|
+
},
|
|
235
|
+
async getOptimizationStats() {
|
|
236
|
+
const stmt = db.prepare(`
|
|
237
|
+
SELECT id, timestamp, tokens_before, tokens_after, entries_pruned, duration_ms
|
|
238
|
+
FROM optimization_stats
|
|
239
|
+
ORDER BY timestamp DESC
|
|
240
|
+
`);
|
|
241
|
+
const rows = stmt.all();
|
|
242
|
+
return rows;
|
|
243
|
+
},
|
|
244
|
+
async clearOptimizationStats() {
|
|
245
|
+
db.exec("DELETE FROM optimization_stats");
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/daemon/consolidation-scheduler.ts
|
|
251
|
+
import { appendFileSync } from "fs";
|
|
252
|
+
|
|
253
|
+
// src/core/engram-scorer.ts
|
|
254
|
+
function createEngramScorer(config2) {
|
|
255
|
+
const { defaultTTL } = config2;
|
|
256
|
+
function calculateDecay(ageInSeconds, ttlInSeconds) {
|
|
257
|
+
if (ttlInSeconds === 0) return 1;
|
|
258
|
+
if (ageInSeconds <= 0) return 0;
|
|
259
|
+
const ratio = ageInSeconds / ttlInSeconds;
|
|
260
|
+
const decay = 1 - Math.exp(-ratio);
|
|
261
|
+
return Math.max(0, Math.min(1, decay));
|
|
262
|
+
}
|
|
263
|
+
function calculateScore(entry, currentTime = Date.now()) {
|
|
264
|
+
const ageInMilliseconds = currentTime - entry.timestamp;
|
|
265
|
+
const ageInSeconds = Math.max(0, ageInMilliseconds / 1e3);
|
|
266
|
+
const decay = calculateDecay(ageInSeconds, entry.ttl);
|
|
267
|
+
let score = entry.score * (1 - decay);
|
|
268
|
+
if (entry.accessCount > 0) {
|
|
269
|
+
const accessBonus = Math.log(entry.accessCount + 1) * 0.1;
|
|
270
|
+
score = Math.min(1, score + accessBonus);
|
|
271
|
+
}
|
|
272
|
+
if (entry.isBTSP) {
|
|
273
|
+
score = Math.max(score, 0.9);
|
|
274
|
+
}
|
|
275
|
+
return Math.max(0, Math.min(1, score));
|
|
276
|
+
}
|
|
277
|
+
function refreshTTL(entry) {
|
|
278
|
+
return {
|
|
279
|
+
...entry,
|
|
280
|
+
ttl: defaultTTL * 3600,
|
|
281
|
+
// Convert hours to seconds
|
|
282
|
+
timestamp: Date.now()
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
calculateScore,
|
|
287
|
+
refreshTTL,
|
|
288
|
+
calculateDecay
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/core/sleep-compressor.ts
|
|
293
|
+
function createSleepCompressor() {
|
|
294
|
+
const scorer = createEngramScorer({ defaultTTL: 24, decayThreshold: 0.95 });
|
|
295
|
+
function consolidate(entries) {
|
|
296
|
+
const startTime = Date.now();
|
|
297
|
+
const originalCount = entries.length;
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
const nonDecayed = entries.filter((entry) => {
|
|
300
|
+
const ageInSeconds = (now - entry.timestamp) / 1e3;
|
|
301
|
+
const decay = scorer.calculateDecay(ageInSeconds, entry.ttl);
|
|
302
|
+
return decay < 0.95;
|
|
303
|
+
});
|
|
304
|
+
const decayedRemoved = originalCount - nonDecayed.length;
|
|
305
|
+
const duplicateGroups = findDuplicates(nonDecayed);
|
|
306
|
+
const merged = mergeDuplicates(duplicateGroups);
|
|
307
|
+
const duplicateIds = new Set(duplicateGroups.flatMap((g) => g.entries.map((e) => e.id)));
|
|
308
|
+
const nonDuplicates = nonDecayed.filter((e) => !duplicateIds.has(e.id));
|
|
309
|
+
const kept = [...merged, ...nonDuplicates];
|
|
310
|
+
const removed = entries.filter((e) => !kept.some((k) => k.id === e.id));
|
|
311
|
+
const duplicatesRemoved = duplicateGroups.reduce((sum, g) => sum + (g.entries.length - 1), 0);
|
|
312
|
+
return {
|
|
313
|
+
kept,
|
|
314
|
+
removed,
|
|
315
|
+
entriesBefore: originalCount,
|
|
316
|
+
entriesAfter: kept.length,
|
|
317
|
+
decayedRemoved,
|
|
318
|
+
duplicatesRemoved,
|
|
319
|
+
compressionRatio: originalCount > 0 ? kept.length / originalCount : 0,
|
|
320
|
+
durationMs: Date.now() - startTime
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function findDuplicates(entries) {
|
|
324
|
+
const groups = [];
|
|
325
|
+
const processed = /* @__PURE__ */ new Set();
|
|
326
|
+
for (let i = 0; i < entries.length; i++) {
|
|
327
|
+
const entry = entries[i];
|
|
328
|
+
if (!entry || processed.has(entry.id)) continue;
|
|
329
|
+
const duplicates = entries.filter((e, idx) => idx !== i && e.hash === entry.hash);
|
|
330
|
+
if (duplicates.length > 0) {
|
|
331
|
+
const group = {
|
|
332
|
+
entries: [entry, ...duplicates],
|
|
333
|
+
similarity: 1
|
|
334
|
+
// Exact match
|
|
335
|
+
};
|
|
336
|
+
groups.push(group);
|
|
337
|
+
processed.add(entry.id);
|
|
338
|
+
for (const dup of duplicates) {
|
|
339
|
+
processed.add(dup.id);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (let i = 0; i < entries.length; i++) {
|
|
344
|
+
const entryI = entries[i];
|
|
345
|
+
if (!entryI || processed.has(entryI.id)) continue;
|
|
346
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
347
|
+
const entryJ = entries[j];
|
|
348
|
+
if (!entryJ || processed.has(entryJ.id)) continue;
|
|
349
|
+
const similarity = cosineSimilarity(entryI.content, entryJ.content);
|
|
350
|
+
if (similarity >= 0.85) {
|
|
351
|
+
const group = {
|
|
352
|
+
entries: [entryI, entryJ],
|
|
353
|
+
similarity
|
|
354
|
+
};
|
|
355
|
+
groups.push(group);
|
|
356
|
+
processed.add(entryI.id);
|
|
357
|
+
processed.add(entryJ.id);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return groups;
|
|
363
|
+
}
|
|
364
|
+
function mergeDuplicates(groups) {
|
|
365
|
+
const merged = [];
|
|
366
|
+
for (const group of groups) {
|
|
367
|
+
const sorted = [...group.entries].sort((a, b) => b.score - a.score);
|
|
368
|
+
const best = sorted[0];
|
|
369
|
+
if (!best) continue;
|
|
370
|
+
const totalAccessCount = group.entries.reduce((sum, e) => sum + e.accessCount, 0);
|
|
371
|
+
const allTags = new Set(group.entries.flatMap((e) => e.tags));
|
|
372
|
+
merged.push({
|
|
373
|
+
...best,
|
|
374
|
+
accessCount: totalAccessCount,
|
|
375
|
+
tags: Array.from(allTags)
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return merged;
|
|
379
|
+
}
|
|
380
|
+
function cosineSimilarity(text1, text2) {
|
|
381
|
+
const words1 = tokenize(text1);
|
|
382
|
+
const words2 = tokenize(text2);
|
|
383
|
+
const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
|
|
384
|
+
const vec1 = {};
|
|
385
|
+
const vec2 = {};
|
|
386
|
+
for (const word of vocab) {
|
|
387
|
+
vec1[word] = words1.filter((w) => w === word).length;
|
|
388
|
+
vec2[word] = words2.filter((w) => w === word).length;
|
|
389
|
+
}
|
|
390
|
+
let dotProduct = 0;
|
|
391
|
+
let mag1 = 0;
|
|
392
|
+
let mag2 = 0;
|
|
393
|
+
for (const word of vocab) {
|
|
394
|
+
const count1 = vec1[word] ?? 0;
|
|
395
|
+
const count2 = vec2[word] ?? 0;
|
|
396
|
+
dotProduct += count1 * count2;
|
|
397
|
+
mag1 += count1 * count1;
|
|
398
|
+
mag2 += count2 * count2;
|
|
399
|
+
}
|
|
400
|
+
mag1 = Math.sqrt(mag1);
|
|
401
|
+
mag2 = Math.sqrt(mag2);
|
|
402
|
+
if (mag1 === 0 || mag2 === 0) return 0;
|
|
403
|
+
return dotProduct / (mag1 * mag2);
|
|
404
|
+
}
|
|
405
|
+
function tokenize(text) {
|
|
406
|
+
return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
consolidate,
|
|
410
|
+
findDuplicates,
|
|
411
|
+
mergeDuplicates
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/cli/commands/consolidate.ts
|
|
416
|
+
async function consolidateCommand(options) {
|
|
417
|
+
const { memory: memory2 } = options;
|
|
418
|
+
const allIds = await memory2.list();
|
|
419
|
+
const allEntries = await Promise.all(
|
|
420
|
+
allIds.map(async (id) => {
|
|
421
|
+
const entry = await memory2.get(id);
|
|
422
|
+
return entry;
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
const entries = allEntries.filter((e) => e !== null);
|
|
426
|
+
const compressor = createSleepCompressor();
|
|
427
|
+
const result = compressor.consolidate(entries);
|
|
428
|
+
for (const removed of result.removed) {
|
|
429
|
+
await memory2.delete(removed.id);
|
|
430
|
+
}
|
|
431
|
+
for (const kept of result.kept) {
|
|
432
|
+
await memory2.put(kept);
|
|
433
|
+
}
|
|
434
|
+
await memory2.compact();
|
|
435
|
+
return {
|
|
436
|
+
entriesBefore: result.entriesBefore,
|
|
437
|
+
entriesAfter: result.entriesAfter,
|
|
438
|
+
decayedRemoved: result.decayedRemoved,
|
|
439
|
+
duplicatesRemoved: result.duplicatesRemoved,
|
|
440
|
+
compressionRatio: result.compressionRatio,
|
|
441
|
+
durationMs: result.durationMs,
|
|
442
|
+
vacuumCompleted: true
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/core/metrics.ts
|
|
447
|
+
function createMetricsCollector() {
|
|
448
|
+
const optimizations = [];
|
|
449
|
+
let daemonMetrics = {
|
|
450
|
+
startTime: Date.now(),
|
|
451
|
+
sessionsWatched: 0,
|
|
452
|
+
totalOptimizations: 0,
|
|
453
|
+
totalTokensSaved: 0,
|
|
454
|
+
averageLatency: 0,
|
|
455
|
+
memoryUsage: 0
|
|
456
|
+
};
|
|
457
|
+
let cacheHits = 0;
|
|
458
|
+
let cacheMisses = 0;
|
|
459
|
+
function recordOptimization(metric) {
|
|
460
|
+
optimizations.push(metric);
|
|
461
|
+
daemonMetrics.totalOptimizations++;
|
|
462
|
+
daemonMetrics.totalTokensSaved += metric.tokensBefore - metric.tokensAfter;
|
|
463
|
+
if (metric.cacheHitRate > 0) {
|
|
464
|
+
const hits = Math.round(metric.entriesProcessed * metric.cacheHitRate);
|
|
465
|
+
cacheHits += hits;
|
|
466
|
+
cacheMisses += metric.entriesProcessed - hits;
|
|
467
|
+
}
|
|
468
|
+
daemonMetrics.averageLatency = (daemonMetrics.averageLatency * (daemonMetrics.totalOptimizations - 1) + metric.duration) / daemonMetrics.totalOptimizations;
|
|
469
|
+
if (optimizations.length > 1e3) {
|
|
470
|
+
optimizations.shift();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function updateDaemon(metric) {
|
|
474
|
+
daemonMetrics = {
|
|
475
|
+
...daemonMetrics,
|
|
476
|
+
...metric
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function calculatePercentile(values, percentile) {
|
|
480
|
+
if (values.length === 0) return 0;
|
|
481
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
482
|
+
const index = Math.ceil(percentile / 100 * sorted.length) - 1;
|
|
483
|
+
return sorted[index] || 0;
|
|
484
|
+
}
|
|
485
|
+
function getSnapshot() {
|
|
486
|
+
const totalRuns = optimizations.length;
|
|
487
|
+
const totalDuration = optimizations.reduce((sum, m) => sum + m.duration, 0);
|
|
488
|
+
const totalTokensSaved = optimizations.reduce(
|
|
489
|
+
(sum, m) => sum + (m.tokensBefore - m.tokensAfter),
|
|
490
|
+
0
|
|
491
|
+
);
|
|
492
|
+
const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
|
|
493
|
+
const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
|
|
494
|
+
const durations = optimizations.map((m) => m.duration);
|
|
495
|
+
const totalCacheQueries = cacheHits + cacheMisses;
|
|
496
|
+
const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
|
|
497
|
+
return {
|
|
498
|
+
timestamp: Date.now(),
|
|
499
|
+
optimization: {
|
|
500
|
+
totalRuns,
|
|
501
|
+
totalDuration,
|
|
502
|
+
totalTokensSaved,
|
|
503
|
+
averageReduction,
|
|
504
|
+
p50Latency: calculatePercentile(durations, 50),
|
|
505
|
+
p95Latency: calculatePercentile(durations, 95),
|
|
506
|
+
p99Latency: calculatePercentile(durations, 99)
|
|
507
|
+
},
|
|
508
|
+
cache: {
|
|
509
|
+
hitRate,
|
|
510
|
+
totalHits: cacheHits,
|
|
511
|
+
totalMisses: cacheMisses,
|
|
512
|
+
size: optimizations.reduce((sum, m) => sum + m.entriesKept, 0)
|
|
513
|
+
},
|
|
514
|
+
daemon: {
|
|
515
|
+
uptime: Date.now() - daemonMetrics.startTime,
|
|
516
|
+
sessionsWatched: daemonMetrics.sessionsWatched,
|
|
517
|
+
memoryUsage: daemonMetrics.memoryUsage
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function exportMetrics() {
|
|
522
|
+
return JSON.stringify(getSnapshot(), null, 2);
|
|
523
|
+
}
|
|
524
|
+
function reset() {
|
|
525
|
+
optimizations.length = 0;
|
|
526
|
+
cacheHits = 0;
|
|
527
|
+
cacheMisses = 0;
|
|
528
|
+
daemonMetrics = {
|
|
529
|
+
startTime: Date.now(),
|
|
530
|
+
sessionsWatched: 0,
|
|
531
|
+
totalOptimizations: 0,
|
|
532
|
+
totalTokensSaved: 0,
|
|
533
|
+
averageLatency: 0,
|
|
534
|
+
memoryUsage: 0
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
recordOptimization,
|
|
539
|
+
updateDaemon,
|
|
540
|
+
getSnapshot,
|
|
541
|
+
export: exportMetrics,
|
|
542
|
+
reset
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
var globalMetrics = null;
|
|
546
|
+
function getMetrics() {
|
|
547
|
+
if (!globalMetrics) {
|
|
548
|
+
globalMetrics = createMetricsCollector();
|
|
549
|
+
}
|
|
550
|
+
return globalMetrics;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/daemon/consolidation-scheduler.ts
|
|
554
|
+
function createConsolidationScheduler(options) {
|
|
555
|
+
const { memory: memory2, config: config2, logFile: logFile2 } = options;
|
|
556
|
+
const intervalHours = config2.realtime.consolidationInterval;
|
|
557
|
+
let timerId = null;
|
|
558
|
+
let totalRuns = 0;
|
|
559
|
+
let lastRun = null;
|
|
560
|
+
let lastResult = null;
|
|
561
|
+
let nextRun = null;
|
|
562
|
+
function log2(message) {
|
|
563
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
564
|
+
const logMessage = `[${timestamp}] [Consolidation] ${message}
|
|
565
|
+
`;
|
|
566
|
+
const logPath = logFile2 || config2.realtime.logFile;
|
|
567
|
+
if (logPath) {
|
|
568
|
+
try {
|
|
569
|
+
appendFileSync(logPath, logMessage, "utf-8");
|
|
570
|
+
} catch {
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function runConsolidation() {
|
|
575
|
+
const startTime = Date.now();
|
|
576
|
+
log2("Starting scheduled consolidation");
|
|
577
|
+
try {
|
|
578
|
+
const result = await consolidateCommand({ memory: memory2 });
|
|
579
|
+
lastRun = startTime;
|
|
580
|
+
totalRuns++;
|
|
581
|
+
lastResult = {
|
|
582
|
+
timestamp: startTime,
|
|
583
|
+
entriesBefore: result.entriesBefore,
|
|
584
|
+
entriesAfter: result.entriesAfter,
|
|
585
|
+
decayedRemoved: result.decayedRemoved,
|
|
586
|
+
duplicatesRemoved: result.duplicatesRemoved,
|
|
587
|
+
compressionRatio: result.compressionRatio,
|
|
588
|
+
durationMs: result.durationMs,
|
|
589
|
+
success: true
|
|
590
|
+
};
|
|
591
|
+
log2(
|
|
592
|
+
`Consolidation completed: ${result.entriesBefore} -> ${result.entriesAfter} entries (${result.decayedRemoved} decayed, ${result.duplicatesRemoved} duplicates, ${Math.round(result.compressionRatio * 100)}% compression) in ${result.durationMs}ms`
|
|
593
|
+
);
|
|
594
|
+
const metrics = getMetrics();
|
|
595
|
+
metrics.recordOptimization({
|
|
596
|
+
timestamp: startTime,
|
|
597
|
+
duration: result.durationMs,
|
|
598
|
+
tokensBefore: result.entriesBefore * 100,
|
|
599
|
+
// Rough estimate
|
|
600
|
+
tokensAfter: result.entriesAfter * 100,
|
|
601
|
+
// Rough estimate
|
|
602
|
+
entriesProcessed: result.entriesBefore,
|
|
603
|
+
entriesKept: result.entriesAfter,
|
|
604
|
+
cacheHitRate: 0,
|
|
605
|
+
// N/A for consolidation
|
|
606
|
+
memoryUsage: process.memoryUsage().heapUsed
|
|
607
|
+
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
lastRun = startTime;
|
|
610
|
+
totalRuns++;
|
|
611
|
+
lastResult = {
|
|
612
|
+
timestamp: startTime,
|
|
613
|
+
entriesBefore: 0,
|
|
614
|
+
entriesAfter: 0,
|
|
615
|
+
decayedRemoved: 0,
|
|
616
|
+
duplicatesRemoved: 0,
|
|
617
|
+
compressionRatio: 0,
|
|
618
|
+
durationMs: Date.now() - startTime,
|
|
619
|
+
success: false,
|
|
620
|
+
error: error instanceof Error ? error.message : String(error)
|
|
621
|
+
};
|
|
622
|
+
log2(`Consolidation failed: ${lastResult.error}`);
|
|
623
|
+
}
|
|
624
|
+
if (intervalHours !== null && intervalHours > 0) {
|
|
625
|
+
nextRun = Date.now() + intervalHours * 60 * 60 * 1e3;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function start() {
|
|
629
|
+
if (intervalHours === null || intervalHours <= 0) {
|
|
630
|
+
log2("Consolidation scheduler disabled (consolidationInterval not set)");
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (timerId !== null) {
|
|
634
|
+
log2("Consolidation scheduler already running");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
638
|
+
timerId = setInterval(() => {
|
|
639
|
+
void runConsolidation();
|
|
640
|
+
}, intervalMs);
|
|
641
|
+
nextRun = Date.now() + intervalMs;
|
|
642
|
+
log2(
|
|
643
|
+
`Consolidation scheduler started (interval: ${intervalHours}h, next run: ${new Date(nextRun).toISOString()})`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
function stop() {
|
|
647
|
+
if (timerId !== null) {
|
|
648
|
+
clearInterval(timerId);
|
|
649
|
+
timerId = null;
|
|
650
|
+
nextRun = null;
|
|
651
|
+
log2("Consolidation scheduler stopped");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function getStatus() {
|
|
655
|
+
return {
|
|
656
|
+
running: timerId !== null,
|
|
657
|
+
intervalHours,
|
|
658
|
+
nextRun,
|
|
659
|
+
totalRuns,
|
|
660
|
+
lastRun,
|
|
661
|
+
lastResult
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
start,
|
|
666
|
+
stop,
|
|
667
|
+
getStatus
|
|
668
|
+
};
|
|
669
|
+
}
|
|
3
670
|
|
|
4
671
|
// src/daemon/session-watcher.ts
|
|
5
672
|
import { readdirSync, statSync as statSync2, watch } from "fs";
|
|
@@ -92,45 +759,6 @@ function estimateTokens(text) {
|
|
|
92
759
|
return Math.max(wordEstimate, charEstimate);
|
|
93
760
|
}
|
|
94
761
|
|
|
95
|
-
// src/core/engram-scorer.ts
|
|
96
|
-
function createEngramScorer(config2) {
|
|
97
|
-
const { defaultTTL } = config2;
|
|
98
|
-
function calculateDecay(ageInSeconds, ttlInSeconds) {
|
|
99
|
-
if (ttlInSeconds === 0) return 1;
|
|
100
|
-
if (ageInSeconds <= 0) return 0;
|
|
101
|
-
const ratio = ageInSeconds / ttlInSeconds;
|
|
102
|
-
const decay = 1 - Math.exp(-ratio);
|
|
103
|
-
return Math.max(0, Math.min(1, decay));
|
|
104
|
-
}
|
|
105
|
-
function calculateScore(entry, currentTime = Date.now()) {
|
|
106
|
-
const ageInMilliseconds = currentTime - entry.timestamp;
|
|
107
|
-
const ageInSeconds = Math.max(0, ageInMilliseconds / 1e3);
|
|
108
|
-
const decay = calculateDecay(ageInSeconds, entry.ttl);
|
|
109
|
-
let score = entry.score * (1 - decay);
|
|
110
|
-
if (entry.accessCount > 0) {
|
|
111
|
-
const accessBonus = Math.log(entry.accessCount + 1) * 0.1;
|
|
112
|
-
score = Math.min(1, score + accessBonus);
|
|
113
|
-
}
|
|
114
|
-
if (entry.isBTSP) {
|
|
115
|
-
score = Math.max(score, 0.9);
|
|
116
|
-
}
|
|
117
|
-
return Math.max(0, Math.min(1, score));
|
|
118
|
-
}
|
|
119
|
-
function refreshTTL(entry) {
|
|
120
|
-
return {
|
|
121
|
-
...entry,
|
|
122
|
-
ttl: defaultTTL * 3600,
|
|
123
|
-
// Convert hours to seconds
|
|
124
|
-
timestamp: Date.now()
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
calculateScore,
|
|
129
|
-
refreshTTL,
|
|
130
|
-
calculateDecay
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
762
|
// src/core/budget-pruner.ts
|
|
135
763
|
function createBudgetPruner(config2) {
|
|
136
764
|
const { tokenBudget, decay } = config2;
|
|
@@ -229,113 +857,6 @@ function createBudgetPruner(config2) {
|
|
|
229
857
|
};
|
|
230
858
|
}
|
|
231
859
|
|
|
232
|
-
// src/core/metrics.ts
|
|
233
|
-
function createMetricsCollector() {
|
|
234
|
-
const optimizations = [];
|
|
235
|
-
let daemonMetrics = {
|
|
236
|
-
startTime: Date.now(),
|
|
237
|
-
sessionsWatched: 0,
|
|
238
|
-
totalOptimizations: 0,
|
|
239
|
-
totalTokensSaved: 0,
|
|
240
|
-
averageLatency: 0,
|
|
241
|
-
memoryUsage: 0
|
|
242
|
-
};
|
|
243
|
-
let cacheHits = 0;
|
|
244
|
-
let cacheMisses = 0;
|
|
245
|
-
function recordOptimization(metric) {
|
|
246
|
-
optimizations.push(metric);
|
|
247
|
-
daemonMetrics.totalOptimizations++;
|
|
248
|
-
daemonMetrics.totalTokensSaved += metric.tokensBefore - metric.tokensAfter;
|
|
249
|
-
if (metric.cacheHitRate > 0) {
|
|
250
|
-
const hits = Math.round(metric.entriesProcessed * metric.cacheHitRate);
|
|
251
|
-
cacheHits += hits;
|
|
252
|
-
cacheMisses += metric.entriesProcessed - hits;
|
|
253
|
-
}
|
|
254
|
-
daemonMetrics.averageLatency = (daemonMetrics.averageLatency * (daemonMetrics.totalOptimizations - 1) + metric.duration) / daemonMetrics.totalOptimizations;
|
|
255
|
-
if (optimizations.length > 1e3) {
|
|
256
|
-
optimizations.shift();
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
function updateDaemon(metric) {
|
|
260
|
-
daemonMetrics = {
|
|
261
|
-
...daemonMetrics,
|
|
262
|
-
...metric
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
function calculatePercentile(values, percentile) {
|
|
266
|
-
if (values.length === 0) return 0;
|
|
267
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
268
|
-
const index = Math.ceil(percentile / 100 * sorted.length) - 1;
|
|
269
|
-
return sorted[index] || 0;
|
|
270
|
-
}
|
|
271
|
-
function getSnapshot() {
|
|
272
|
-
const totalRuns = optimizations.length;
|
|
273
|
-
const totalDuration = optimizations.reduce((sum, m) => sum + m.duration, 0);
|
|
274
|
-
const totalTokensSaved = optimizations.reduce(
|
|
275
|
-
(sum, m) => sum + (m.tokensBefore - m.tokensAfter),
|
|
276
|
-
0
|
|
277
|
-
);
|
|
278
|
-
const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
|
|
279
|
-
const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
|
|
280
|
-
const durations = optimizations.map((m) => m.duration);
|
|
281
|
-
const totalCacheQueries = cacheHits + cacheMisses;
|
|
282
|
-
const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
|
|
283
|
-
return {
|
|
284
|
-
timestamp: Date.now(),
|
|
285
|
-
optimization: {
|
|
286
|
-
totalRuns,
|
|
287
|
-
totalDuration,
|
|
288
|
-
totalTokensSaved,
|
|
289
|
-
averageReduction,
|
|
290
|
-
p50Latency: calculatePercentile(durations, 50),
|
|
291
|
-
p95Latency: calculatePercentile(durations, 95),
|
|
292
|
-
p99Latency: calculatePercentile(durations, 99)
|
|
293
|
-
},
|
|
294
|
-
cache: {
|
|
295
|
-
hitRate,
|
|
296
|
-
totalHits: cacheHits,
|
|
297
|
-
totalMisses: cacheMisses,
|
|
298
|
-
size: optimizations.reduce((sum, m) => sum + m.entriesKept, 0)
|
|
299
|
-
},
|
|
300
|
-
daemon: {
|
|
301
|
-
uptime: Date.now() - daemonMetrics.startTime,
|
|
302
|
-
sessionsWatched: daemonMetrics.sessionsWatched,
|
|
303
|
-
memoryUsage: daemonMetrics.memoryUsage
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
function exportMetrics() {
|
|
308
|
-
return JSON.stringify(getSnapshot(), null, 2);
|
|
309
|
-
}
|
|
310
|
-
function reset() {
|
|
311
|
-
optimizations.length = 0;
|
|
312
|
-
cacheHits = 0;
|
|
313
|
-
cacheMisses = 0;
|
|
314
|
-
daemonMetrics = {
|
|
315
|
-
startTime: Date.now(),
|
|
316
|
-
sessionsWatched: 0,
|
|
317
|
-
totalOptimizations: 0,
|
|
318
|
-
totalTokensSaved: 0,
|
|
319
|
-
averageLatency: 0,
|
|
320
|
-
memoryUsage: 0
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
return {
|
|
324
|
-
recordOptimization,
|
|
325
|
-
updateDaemon,
|
|
326
|
-
getSnapshot,
|
|
327
|
-
export: exportMetrics,
|
|
328
|
-
reset
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
var globalMetrics = null;
|
|
332
|
-
function getMetrics() {
|
|
333
|
-
if (!globalMetrics) {
|
|
334
|
-
globalMetrics = createMetricsCollector();
|
|
335
|
-
}
|
|
336
|
-
return globalMetrics;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
860
|
// src/core/incremental-optimizer.ts
|
|
340
861
|
function createIncrementalOptimizer(config2) {
|
|
341
862
|
const pruner = createBudgetPruner(config2);
|
|
@@ -830,20 +1351,26 @@ function log(message) {
|
|
|
830
1351
|
`;
|
|
831
1352
|
if (logFile) {
|
|
832
1353
|
try {
|
|
833
|
-
|
|
1354
|
+
appendFileSync2(logFile, logMessage, "utf-8");
|
|
834
1355
|
} catch {
|
|
835
1356
|
}
|
|
836
1357
|
}
|
|
837
1358
|
}
|
|
838
1359
|
function cleanup() {
|
|
839
1360
|
log("Daemon shutting down");
|
|
840
|
-
if (pidFile &&
|
|
1361
|
+
if (pidFile && existsSync2(pidFile)) {
|
|
841
1362
|
try {
|
|
842
1363
|
unlinkSync(pidFile);
|
|
843
1364
|
} catch {
|
|
844
1365
|
}
|
|
845
1366
|
}
|
|
1367
|
+
if (scheduler) {
|
|
1368
|
+
scheduler.stop();
|
|
1369
|
+
}
|
|
846
1370
|
watcher.stop();
|
|
1371
|
+
if (memory) {
|
|
1372
|
+
void memory.close();
|
|
1373
|
+
}
|
|
847
1374
|
process.exit(0);
|
|
848
1375
|
}
|
|
849
1376
|
process.on("SIGTERM", cleanup);
|
|
@@ -857,6 +1384,8 @@ process.on("unhandledRejection", (reason) => {
|
|
|
857
1384
|
log(`Unhandled rejection: ${reason}`);
|
|
858
1385
|
cleanup();
|
|
859
1386
|
});
|
|
1387
|
+
var memory = null;
|
|
1388
|
+
var scheduler = null;
|
|
860
1389
|
log("Daemon starting");
|
|
861
1390
|
var watcher = createSessionWatcher({
|
|
862
1391
|
config,
|
|
@@ -869,8 +1398,26 @@ var watcher = createSessionWatcher({
|
|
|
869
1398
|
log(`Error: ${error.message}`);
|
|
870
1399
|
}
|
|
871
1400
|
});
|
|
872
|
-
watcher.start().then(() => {
|
|
1401
|
+
watcher.start().then(async () => {
|
|
873
1402
|
log("Daemon ready - watching Claude Code sessions");
|
|
1403
|
+
if (config.realtime.consolidationInterval !== null && config.realtime.consolidationInterval > 0) {
|
|
1404
|
+
try {
|
|
1405
|
+
memory = await createKVMemory(".sparn/memory.db");
|
|
1406
|
+
scheduler = createConsolidationScheduler({
|
|
1407
|
+
memory,
|
|
1408
|
+
config,
|
|
1409
|
+
logFile
|
|
1410
|
+
});
|
|
1411
|
+
scheduler.start();
|
|
1412
|
+
log(
|
|
1413
|
+
`Consolidation scheduler started (interval: ${config.realtime.consolidationInterval}h)`
|
|
1414
|
+
);
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
log(
|
|
1417
|
+
`Failed to start consolidation scheduler: ${error instanceof Error ? error.message : String(error)}`
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
874
1421
|
}).catch((error) => {
|
|
875
1422
|
log(`Failed to start: ${error.message}`);
|
|
876
1423
|
cleanup();
|