@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.
@@ -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
- appendFileSync(logFile, logMessage, "utf-8");
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 && existsSync(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();