@ulrichc1/sparn 1.2.2 → 1.4.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/PRIVACY.md +1 -1
- package/README.md +136 -642
- package/SECURITY.md +1 -1
- package/dist/cli/dashboard.cjs +3977 -0
- package/dist/cli/dashboard.cjs.map +1 -0
- package/dist/cli/dashboard.d.cts +17 -0
- package/dist/cli/dashboard.d.ts +17 -0
- package/dist/cli/dashboard.js +3932 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/index.cjs +3853 -484
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3810 -457
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/index.cjs +411 -99
- package/dist/daemon/index.cjs.map +1 -1
- package/dist/daemon/index.js +423 -103
- package/dist/daemon/index.js.map +1 -1
- package/dist/hooks/post-tool-result.cjs +115 -266
- package/dist/hooks/post-tool-result.cjs.map +1 -1
- package/dist/hooks/post-tool-result.js +115 -266
- package/dist/hooks/post-tool-result.js.map +1 -1
- package/dist/hooks/pre-prompt.cjs +197 -268
- package/dist/hooks/pre-prompt.cjs.map +1 -1
- package/dist/hooks/pre-prompt.js +182 -268
- package/dist/hooks/pre-prompt.js.map +1 -1
- package/dist/hooks/stop-docs-refresh.cjs +123 -0
- package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
- package/dist/hooks/stop-docs-refresh.d.cts +1 -0
- package/dist/hooks/stop-docs-refresh.d.ts +1 -0
- package/dist/hooks/stop-docs-refresh.js +126 -0
- package/dist/hooks/stop-docs-refresh.js.map +1 -0
- package/dist/index.cjs +1754 -337
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +539 -40
- package/dist/index.d.ts +539 -40
- package/dist/index.js +1737 -329
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +304 -71
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +308 -71
- package/dist/mcp/index.js.map +1 -1
- package/package.json +10 -3
package/dist/daemon/index.cjs
CHANGED
|
@@ -47,6 +47,7 @@ async function createKVMemory(dbPath) {
|
|
|
47
47
|
const integrityCheck = db.pragma("quick_check", { simple: true });
|
|
48
48
|
if (integrityCheck !== "ok") {
|
|
49
49
|
console.error("\u26A0 Database corruption detected!");
|
|
50
|
+
db.close();
|
|
50
51
|
if ((0, import_node_fs.existsSync)(dbPath)) {
|
|
51
52
|
const backupPath = createBackup(dbPath);
|
|
52
53
|
if (backupPath) {
|
|
@@ -54,7 +55,6 @@ async function createKVMemory(dbPath) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
console.log("Attempting database recovery...");
|
|
57
|
-
db.close();
|
|
58
58
|
db = new import_better_sqlite3.default(dbPath);
|
|
59
59
|
}
|
|
60
60
|
} catch (error) {
|
|
@@ -66,6 +66,7 @@ async function createKVMemory(dbPath) {
|
|
|
66
66
|
db = new import_better_sqlite3.default(dbPath);
|
|
67
67
|
}
|
|
68
68
|
db.pragma("journal_mode = WAL");
|
|
69
|
+
db.pragma("foreign_keys = ON");
|
|
69
70
|
db.exec(`
|
|
70
71
|
CREATE TABLE IF NOT EXISTS entries_index (
|
|
71
72
|
id TEXT PRIMARY KEY NOT NULL,
|
|
@@ -105,6 +106,36 @@ async function createKVMemory(dbPath) {
|
|
|
105
106
|
CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
|
|
106
107
|
CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
|
|
107
108
|
`);
|
|
109
|
+
db.exec(`
|
|
110
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(id, content, tokenize='porter');
|
|
111
|
+
`);
|
|
112
|
+
db.exec(`
|
|
113
|
+
CREATE TRIGGER IF NOT EXISTS entries_fts_insert
|
|
114
|
+
AFTER INSERT ON entries_value
|
|
115
|
+
BEGIN
|
|
116
|
+
INSERT OR REPLACE INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
|
|
117
|
+
END;
|
|
118
|
+
`);
|
|
119
|
+
db.exec(`
|
|
120
|
+
CREATE TRIGGER IF NOT EXISTS entries_fts_delete
|
|
121
|
+
AFTER DELETE ON entries_value
|
|
122
|
+
BEGIN
|
|
123
|
+
DELETE FROM entries_fts WHERE id = OLD.id;
|
|
124
|
+
END;
|
|
125
|
+
`);
|
|
126
|
+
db.exec(`
|
|
127
|
+
CREATE TRIGGER IF NOT EXISTS entries_fts_update
|
|
128
|
+
AFTER UPDATE ON entries_value
|
|
129
|
+
BEGIN
|
|
130
|
+
DELETE FROM entries_fts WHERE id = OLD.id;
|
|
131
|
+
INSERT INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
|
|
132
|
+
END;
|
|
133
|
+
`);
|
|
134
|
+
db.exec(`
|
|
135
|
+
INSERT OR IGNORE INTO entries_fts(id, content)
|
|
136
|
+
SELECT id, content FROM entries_value
|
|
137
|
+
WHERE id NOT IN (SELECT id FROM entries_fts);
|
|
138
|
+
`);
|
|
108
139
|
const putIndexStmt = db.prepare(`
|
|
109
140
|
INSERT OR REPLACE INTO entries_index
|
|
110
141
|
(id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
|
|
@@ -193,14 +224,20 @@ async function createKVMemory(dbPath) {
|
|
|
193
224
|
sql += " AND i.isBTSP = ?";
|
|
194
225
|
params.push(filters.isBTSP ? 1 : 0);
|
|
195
226
|
}
|
|
227
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
228
|
+
for (const tag of filters.tags) {
|
|
229
|
+
sql += " AND v.tags LIKE ?";
|
|
230
|
+
params.push(`%"${tag}"%`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
196
233
|
sql += " ORDER BY i.score DESC";
|
|
197
234
|
if (filters.limit) {
|
|
198
235
|
sql += " LIMIT ?";
|
|
199
236
|
params.push(filters.limit);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
237
|
+
if (filters.offset) {
|
|
238
|
+
sql += " OFFSET ?";
|
|
239
|
+
params.push(filters.offset);
|
|
240
|
+
}
|
|
204
241
|
}
|
|
205
242
|
const stmt = db.prepare(sql);
|
|
206
243
|
const rows = stmt.all(...params);
|
|
@@ -235,7 +272,22 @@ async function createKVMemory(dbPath) {
|
|
|
235
272
|
},
|
|
236
273
|
async compact() {
|
|
237
274
|
const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
|
|
238
|
-
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
db.prepare("DELETE FROM entries_index WHERE isBTSP = 0 AND (timestamp + ttl * 1000) < ?").run(
|
|
277
|
+
now
|
|
278
|
+
);
|
|
279
|
+
db.exec("DELETE FROM entries_index WHERE isBTSP = 0 AND ttl <= 0");
|
|
280
|
+
const candidates = db.prepare("SELECT id, timestamp, ttl FROM entries_index WHERE isBTSP = 0").all();
|
|
281
|
+
for (const row of candidates) {
|
|
282
|
+
const ageSeconds = Math.max(0, (now - row.timestamp) / 1e3);
|
|
283
|
+
const ttlSeconds = row.ttl;
|
|
284
|
+
if (ttlSeconds <= 0) continue;
|
|
285
|
+
const decay = 1 - Math.exp(-ageSeconds / ttlSeconds);
|
|
286
|
+
if (decay >= 0.95) {
|
|
287
|
+
db.prepare("DELETE FROM entries_index WHERE id = ?").run(row.id);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
db.exec("DELETE FROM entries_value WHERE id NOT IN (SELECT id FROM entries_index)");
|
|
239
291
|
db.exec("VACUUM");
|
|
240
292
|
const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
|
|
241
293
|
return before.count - after.count;
|
|
@@ -255,6 +307,9 @@ async function createKVMemory(dbPath) {
|
|
|
255
307
|
stats.entries_pruned,
|
|
256
308
|
stats.duration_ms
|
|
257
309
|
);
|
|
310
|
+
db.prepare(
|
|
311
|
+
"DELETE FROM optimization_stats WHERE id NOT IN (SELECT id FROM optimization_stats ORDER BY timestamp DESC LIMIT 1000)"
|
|
312
|
+
).run();
|
|
258
313
|
},
|
|
259
314
|
async getOptimizationStats() {
|
|
260
315
|
const stmt = db.prepare(`
|
|
@@ -267,16 +322,114 @@ async function createKVMemory(dbPath) {
|
|
|
267
322
|
},
|
|
268
323
|
async clearOptimizationStats() {
|
|
269
324
|
db.exec("DELETE FROM optimization_stats");
|
|
325
|
+
},
|
|
326
|
+
async searchFTS(query, limit = 10) {
|
|
327
|
+
if (!query || query.trim().length === 0) return [];
|
|
328
|
+
const sanitized = query.replace(/[{}()[\]"':*^~]/g, " ").trim();
|
|
329
|
+
if (sanitized.length === 0) return [];
|
|
330
|
+
const stmt = db.prepare(`
|
|
331
|
+
SELECT
|
|
332
|
+
f.id, f.content, rank,
|
|
333
|
+
i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
|
|
334
|
+
v.tags, v.metadata
|
|
335
|
+
FROM entries_fts f
|
|
336
|
+
JOIN entries_index i ON f.id = i.id
|
|
337
|
+
JOIN entries_value v ON f.id = v.id
|
|
338
|
+
WHERE entries_fts MATCH ?
|
|
339
|
+
ORDER BY rank
|
|
340
|
+
LIMIT ?
|
|
341
|
+
`);
|
|
342
|
+
try {
|
|
343
|
+
const rows = stmt.all(sanitized, limit);
|
|
344
|
+
return rows.map((r) => ({
|
|
345
|
+
entry: {
|
|
346
|
+
id: r.id,
|
|
347
|
+
content: r.content,
|
|
348
|
+
hash: r.hash,
|
|
349
|
+
timestamp: r.timestamp,
|
|
350
|
+
score: r.score,
|
|
351
|
+
ttl: r.ttl,
|
|
352
|
+
state: r.state,
|
|
353
|
+
accessCount: r.accessCount,
|
|
354
|
+
tags: r.tags ? JSON.parse(r.tags) : [],
|
|
355
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : {},
|
|
356
|
+
isBTSP: r.isBTSP === 1
|
|
357
|
+
},
|
|
358
|
+
rank: r.rank
|
|
359
|
+
}));
|
|
360
|
+
} catch {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
270
363
|
}
|
|
271
364
|
};
|
|
272
365
|
}
|
|
273
366
|
|
|
367
|
+
// src/utils/tokenizer.ts
|
|
368
|
+
var import_gpt_tokenizer = require("gpt-tokenizer");
|
|
369
|
+
var usePrecise = false;
|
|
370
|
+
function setPreciseTokenCounting(enabled) {
|
|
371
|
+
usePrecise = enabled;
|
|
372
|
+
}
|
|
373
|
+
function estimateTokens(text) {
|
|
374
|
+
if (!text || text.length === 0) {
|
|
375
|
+
return 0;
|
|
376
|
+
}
|
|
377
|
+
if (usePrecise) {
|
|
378
|
+
return (0, import_gpt_tokenizer.encode)(text).length;
|
|
379
|
+
}
|
|
380
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
381
|
+
const wordCount = words.length;
|
|
382
|
+
const charCount = text.length;
|
|
383
|
+
const charEstimate = Math.ceil(charCount / 4);
|
|
384
|
+
const wordEstimate = Math.ceil(wordCount * 0.75);
|
|
385
|
+
return Math.max(wordEstimate, charEstimate);
|
|
386
|
+
}
|
|
387
|
+
|
|
274
388
|
// src/daemon/consolidation-scheduler.ts
|
|
275
389
|
var import_node_fs2 = require("fs");
|
|
276
390
|
|
|
391
|
+
// src/utils/tfidf.ts
|
|
392
|
+
function tokenize(text) {
|
|
393
|
+
return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
|
|
394
|
+
}
|
|
395
|
+
function calculateTF(term, tokens) {
|
|
396
|
+
const count = tokens.filter((t) => t === term).length;
|
|
397
|
+
return Math.sqrt(count);
|
|
398
|
+
}
|
|
399
|
+
function createTFIDFIndex(entries) {
|
|
400
|
+
const documentFrequency = /* @__PURE__ */ new Map();
|
|
401
|
+
for (const entry of entries) {
|
|
402
|
+
const tokens = tokenize(entry.content);
|
|
403
|
+
const uniqueTerms = new Set(tokens);
|
|
404
|
+
for (const term of uniqueTerms) {
|
|
405
|
+
documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
documentFrequency,
|
|
410
|
+
totalDocuments: entries.length
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function scoreTFIDF(entry, index) {
|
|
414
|
+
const tokens = tokenize(entry.content);
|
|
415
|
+
if (tokens.length === 0) return 0;
|
|
416
|
+
const uniqueTerms = new Set(tokens);
|
|
417
|
+
let totalScore = 0;
|
|
418
|
+
for (const term of uniqueTerms) {
|
|
419
|
+
const tf = calculateTF(term, tokens);
|
|
420
|
+
const docsWithTerm = index.documentFrequency.get(term) || 0;
|
|
421
|
+
if (docsWithTerm === 0) continue;
|
|
422
|
+
const idf = Math.log(index.totalDocuments / docsWithTerm);
|
|
423
|
+
totalScore += tf * idf;
|
|
424
|
+
}
|
|
425
|
+
return totalScore / tokens.length;
|
|
426
|
+
}
|
|
427
|
+
|
|
277
428
|
// src/core/engram-scorer.ts
|
|
278
429
|
function createEngramScorer(config2) {
|
|
279
430
|
const { defaultTTL } = config2;
|
|
431
|
+
const recencyWindowMs = (config2.recencyBoostMinutes ?? 30) * 60 * 1e3;
|
|
432
|
+
const recencyMultiplier = config2.recencyBoostMultiplier ?? 1.3;
|
|
280
433
|
function calculateDecay(ageInSeconds, ttlInSeconds) {
|
|
281
434
|
if (ttlInSeconds === 0) return 1;
|
|
282
435
|
if (ageInSeconds <= 0) return 0;
|
|
@@ -296,6 +449,13 @@ function createEngramScorer(config2) {
|
|
|
296
449
|
if (entry.isBTSP) {
|
|
297
450
|
score = Math.max(score, 0.9);
|
|
298
451
|
}
|
|
452
|
+
if (!entry.isBTSP && recencyWindowMs > 0) {
|
|
453
|
+
const ageMs = currentTime - entry.timestamp;
|
|
454
|
+
if (ageMs >= 0 && ageMs < recencyWindowMs) {
|
|
455
|
+
const boostFactor = 1 + (recencyMultiplier - 1) * (1 - ageMs / recencyWindowMs);
|
|
456
|
+
score = score * boostFactor;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
299
459
|
return Math.max(0, Math.min(1, score));
|
|
300
460
|
}
|
|
301
461
|
function refreshTTL(entry) {
|
|
@@ -404,13 +564,15 @@ function createSleepCompressor() {
|
|
|
404
564
|
function cosineSimilarity(text1, text2) {
|
|
405
565
|
const words1 = tokenize(text1);
|
|
406
566
|
const words2 = tokenize(text2);
|
|
407
|
-
const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
|
|
408
567
|
const vec1 = {};
|
|
409
568
|
const vec2 = {};
|
|
410
|
-
for (const word of
|
|
411
|
-
vec1[word] =
|
|
412
|
-
|
|
569
|
+
for (const word of words1) {
|
|
570
|
+
vec1[word] = (vec1[word] ?? 0) + 1;
|
|
571
|
+
}
|
|
572
|
+
for (const word of words2) {
|
|
573
|
+
vec2[word] = (vec2[word] ?? 0) + 1;
|
|
413
574
|
}
|
|
575
|
+
const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
|
|
414
576
|
let dotProduct = 0;
|
|
415
577
|
let mag1 = 0;
|
|
416
578
|
let mag2 = 0;
|
|
@@ -426,9 +588,6 @@ function createSleepCompressor() {
|
|
|
426
588
|
if (mag1 === 0 || mag2 === 0) return 0;
|
|
427
589
|
return dotProduct / (mag1 * mag2);
|
|
428
590
|
}
|
|
429
|
-
function tokenize(text) {
|
|
430
|
-
return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
|
|
431
|
-
}
|
|
432
591
|
return {
|
|
433
592
|
consolidate,
|
|
434
593
|
findDuplicates,
|
|
@@ -500,11 +659,10 @@ function createMetricsCollector() {
|
|
|
500
659
|
...metric
|
|
501
660
|
};
|
|
502
661
|
}
|
|
503
|
-
function calculatePercentile(
|
|
504
|
-
if (
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
return sorted[index] || 0;
|
|
662
|
+
function calculatePercentile(sortedValues, percentile) {
|
|
663
|
+
if (sortedValues.length === 0) return 0;
|
|
664
|
+
const index = Math.ceil(percentile / 100 * sortedValues.length) - 1;
|
|
665
|
+
return sortedValues[index] || 0;
|
|
508
666
|
}
|
|
509
667
|
function getSnapshot() {
|
|
510
668
|
const totalRuns = optimizations.length;
|
|
@@ -515,7 +673,7 @@ function createMetricsCollector() {
|
|
|
515
673
|
);
|
|
516
674
|
const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
|
|
517
675
|
const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
|
|
518
|
-
const
|
|
676
|
+
const sortedDurations = optimizations.map((m) => m.duration).sort((a, b) => a - b);
|
|
519
677
|
const totalCacheQueries = cacheHits + cacheMisses;
|
|
520
678
|
const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
|
|
521
679
|
return {
|
|
@@ -525,9 +683,9 @@ function createMetricsCollector() {
|
|
|
525
683
|
totalDuration,
|
|
526
684
|
totalTokensSaved,
|
|
527
685
|
averageReduction,
|
|
528
|
-
p50Latency: calculatePercentile(
|
|
529
|
-
p95Latency: calculatePercentile(
|
|
530
|
-
p99Latency: calculatePercentile(
|
|
686
|
+
p50Latency: calculatePercentile(sortedDurations, 50),
|
|
687
|
+
p95Latency: calculatePercentile(sortedDurations, 95),
|
|
688
|
+
p99Latency: calculatePercentile(sortedDurations, 99)
|
|
531
689
|
},
|
|
532
690
|
cache: {
|
|
533
691
|
hitRate,
|
|
@@ -583,6 +741,7 @@ function createConsolidationScheduler(options) {
|
|
|
583
741
|
let lastRun = null;
|
|
584
742
|
let lastResult = null;
|
|
585
743
|
let nextRun = null;
|
|
744
|
+
let isRunning = false;
|
|
586
745
|
function log2(message) {
|
|
587
746
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
588
747
|
const logMessage = `[${timestamp}] [Consolidation] ${message}
|
|
@@ -596,6 +755,11 @@ function createConsolidationScheduler(options) {
|
|
|
596
755
|
}
|
|
597
756
|
}
|
|
598
757
|
async function runConsolidation() {
|
|
758
|
+
if (isRunning) {
|
|
759
|
+
log2("Consolidation already in progress, skipping");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
isRunning = true;
|
|
599
763
|
const startTime = Date.now();
|
|
600
764
|
log2("Starting scheduled consolidation");
|
|
601
765
|
try {
|
|
@@ -648,6 +812,7 @@ function createConsolidationScheduler(options) {
|
|
|
648
812
|
if (intervalHours !== null && intervalHours > 0) {
|
|
649
813
|
nextRun = Date.now() + intervalHours * 60 * 60 * 1e3;
|
|
650
814
|
}
|
|
815
|
+
isRunning = false;
|
|
651
816
|
}
|
|
652
817
|
function start() {
|
|
653
818
|
if (intervalHours === null || intervalHours <= 0) {
|
|
@@ -708,6 +873,11 @@ function hashContent(content) {
|
|
|
708
873
|
|
|
709
874
|
// src/utils/context-parser.ts
|
|
710
875
|
function parseClaudeCodeContext(context) {
|
|
876
|
+
const firstNonEmpty = context.split("\n").find((line) => line.trim().length > 0);
|
|
877
|
+
if (firstNonEmpty?.trim().startsWith("{")) {
|
|
878
|
+
const jsonlEntries = parseJSONLContext(context);
|
|
879
|
+
if (jsonlEntries.length > 0) return jsonlEntries;
|
|
880
|
+
}
|
|
711
881
|
const entries = [];
|
|
712
882
|
const now = Date.now();
|
|
713
883
|
const lines = context.split("\n");
|
|
@@ -760,7 +930,7 @@ function createEntry(content, type, baseTime) {
|
|
|
760
930
|
hash: hashContent(content),
|
|
761
931
|
timestamp: baseTime,
|
|
762
932
|
score: initialScore,
|
|
763
|
-
state: initialScore
|
|
933
|
+
state: initialScore >= 0.7 ? "active" : initialScore >= 0.3 ? "ready" : "silent",
|
|
764
934
|
ttl: 24 * 3600,
|
|
765
935
|
// 24 hours default
|
|
766
936
|
accessCount: 0,
|
|
@@ -769,52 +939,63 @@ function createEntry(content, type, baseTime) {
|
|
|
769
939
|
isBTSP: false
|
|
770
940
|
};
|
|
771
941
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return
|
|
942
|
+
function parseJSONLLine(line) {
|
|
943
|
+
const trimmed = line.trim();
|
|
944
|
+
if (trimmed.length === 0) return null;
|
|
945
|
+
try {
|
|
946
|
+
return JSON.parse(trimmed);
|
|
947
|
+
} catch {
|
|
948
|
+
return null;
|
|
777
949
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
950
|
+
}
|
|
951
|
+
function extractContent(content) {
|
|
952
|
+
if (typeof content === "string") return content;
|
|
953
|
+
if (Array.isArray(content)) {
|
|
954
|
+
return content.map((block) => {
|
|
955
|
+
if (block.type === "text" && block.text) return block.text;
|
|
956
|
+
if (block.type === "tool_use" && block.name) return `[tool_use: ${block.name}]`;
|
|
957
|
+
if (block.type === "tool_result") {
|
|
958
|
+
if (typeof block.content === "string") return block.content;
|
|
959
|
+
if (Array.isArray(block.content)) {
|
|
960
|
+
return block.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return "";
|
|
964
|
+
}).filter((s) => s.length > 0).join("\n");
|
|
965
|
+
}
|
|
966
|
+
return "";
|
|
967
|
+
}
|
|
968
|
+
function classifyJSONLMessage(msg) {
|
|
969
|
+
if (Array.isArray(msg.content)) {
|
|
970
|
+
const hasToolUse = msg.content.some((b) => b.type === "tool_use");
|
|
971
|
+
const hasToolResult = msg.content.some((b) => b.type === "tool_result");
|
|
972
|
+
if (hasToolUse) return "tool";
|
|
973
|
+
if (hasToolResult) return "result";
|
|
974
|
+
}
|
|
975
|
+
if (msg.type === "tool_use" || msg.tool_use) return "tool";
|
|
976
|
+
if (msg.type === "tool_result" || msg.tool_result) return "result";
|
|
977
|
+
if (msg.role === "user" || msg.role === "assistant") return "conversation";
|
|
978
|
+
return "other";
|
|
979
|
+
}
|
|
980
|
+
function parseJSONLContext(context) {
|
|
981
|
+
const entries = [];
|
|
982
|
+
const now = Date.now();
|
|
983
|
+
const lines = context.split("\n");
|
|
984
|
+
for (const line of lines) {
|
|
985
|
+
const msg = parseJSONLLine(line);
|
|
986
|
+
if (!msg) continue;
|
|
987
|
+
const content = extractContent(msg.content);
|
|
988
|
+
if (!content || content.trim().length === 0) continue;
|
|
989
|
+
const blockType = classifyJSONLMessage(msg);
|
|
990
|
+
entries.push(createEntry(content, blockType, now));
|
|
991
|
+
}
|
|
992
|
+
return entries;
|
|
784
993
|
}
|
|
785
994
|
|
|
786
995
|
// src/core/budget-pruner.ts
|
|
787
996
|
function createBudgetPruner(config2) {
|
|
788
997
|
const { tokenBudget, decay } = config2;
|
|
789
998
|
const engramScorer = createEngramScorer(decay);
|
|
790
|
-
function tokenize(text) {
|
|
791
|
-
return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
|
|
792
|
-
}
|
|
793
|
-
function calculateTF(term, tokens) {
|
|
794
|
-
const count = tokens.filter((t) => t === term).length;
|
|
795
|
-
return Math.sqrt(count);
|
|
796
|
-
}
|
|
797
|
-
function calculateIDF(term, allEntries) {
|
|
798
|
-
const totalDocs = allEntries.length;
|
|
799
|
-
const docsWithTerm = allEntries.filter((entry) => {
|
|
800
|
-
const tokens = tokenize(entry.content);
|
|
801
|
-
return tokens.includes(term);
|
|
802
|
-
}).length;
|
|
803
|
-
if (docsWithTerm === 0) return 0;
|
|
804
|
-
return Math.log(totalDocs / docsWithTerm);
|
|
805
|
-
}
|
|
806
|
-
function calculateTFIDF(entry, allEntries) {
|
|
807
|
-
const tokens = tokenize(entry.content);
|
|
808
|
-
if (tokens.length === 0) return 0;
|
|
809
|
-
const uniqueTerms = [...new Set(tokens)];
|
|
810
|
-
let totalScore = 0;
|
|
811
|
-
for (const term of uniqueTerms) {
|
|
812
|
-
const tf = calculateTF(term, tokens);
|
|
813
|
-
const idf = calculateIDF(term, allEntries);
|
|
814
|
-
totalScore += tf * idf;
|
|
815
|
-
}
|
|
816
|
-
return totalScore / tokens.length;
|
|
817
|
-
}
|
|
818
999
|
function getStateMultiplier(entry) {
|
|
819
1000
|
if (entry.isBTSP) return 2;
|
|
820
1001
|
switch (entry.state) {
|
|
@@ -828,8 +1009,8 @@ function createBudgetPruner(config2) {
|
|
|
828
1009
|
return 1;
|
|
829
1010
|
}
|
|
830
1011
|
}
|
|
831
|
-
function priorityScore(entry, allEntries) {
|
|
832
|
-
const tfidf =
|
|
1012
|
+
function priorityScore(entry, allEntries, index) {
|
|
1013
|
+
const tfidf = index ? scoreTFIDF(entry, index) : scoreTFIDF(entry, createTFIDFIndex(allEntries));
|
|
833
1014
|
const currentScore = engramScorer.calculateScore(entry);
|
|
834
1015
|
const engramDecay = 1 - currentScore;
|
|
835
1016
|
const stateMultiplier = getStateMultiplier(entry);
|
|
@@ -848,15 +1029,33 @@ function createBudgetPruner(config2) {
|
|
|
848
1029
|
const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
|
|
849
1030
|
const btspEntries = entries.filter((e) => e.isBTSP);
|
|
850
1031
|
const regularEntries = entries.filter((e) => !e.isBTSP);
|
|
851
|
-
|
|
1032
|
+
let includedBtsp = [];
|
|
1033
|
+
let btspTokens = 0;
|
|
1034
|
+
const sortedBtsp = [...btspEntries].sort((a, b) => b.timestamp - a.timestamp);
|
|
1035
|
+
for (const entry of sortedBtsp) {
|
|
1036
|
+
const tokens = estimateTokens(entry.content);
|
|
1037
|
+
if (btspTokens + tokens <= budget * 0.8) {
|
|
1038
|
+
includedBtsp.push(entry);
|
|
1039
|
+
btspTokens += tokens;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (includedBtsp.length === 0 && sortedBtsp.length > 0) {
|
|
1043
|
+
const firstBtsp = sortedBtsp[0];
|
|
1044
|
+
if (firstBtsp) {
|
|
1045
|
+
includedBtsp = [firstBtsp];
|
|
1046
|
+
btspTokens = estimateTokens(firstBtsp.content);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const excludedBtsp = btspEntries.filter((e) => !includedBtsp.includes(e));
|
|
1050
|
+
const tfidfIndex = createTFIDFIndex(entries);
|
|
852
1051
|
const scored = regularEntries.map((entry) => ({
|
|
853
1052
|
entry,
|
|
854
|
-
score: priorityScore(entry, entries),
|
|
1053
|
+
score: priorityScore(entry, entries, tfidfIndex),
|
|
855
1054
|
tokens: estimateTokens(entry.content)
|
|
856
1055
|
}));
|
|
857
1056
|
scored.sort((a, b) => b.score - a.score);
|
|
858
|
-
const kept = [...
|
|
859
|
-
const removed = [];
|
|
1057
|
+
const kept = [...includedBtsp];
|
|
1058
|
+
const removed = [...excludedBtsp];
|
|
860
1059
|
let currentTokens = btspTokens;
|
|
861
1060
|
for (const item of scored) {
|
|
862
1061
|
if (currentTokens + item.tokens <= budget) {
|
|
@@ -892,9 +1091,6 @@ function createIncrementalOptimizer(config2) {
|
|
|
892
1091
|
updateCount: 0,
|
|
893
1092
|
lastFullOptimization: Date.now()
|
|
894
1093
|
};
|
|
895
|
-
function tokenize(text) {
|
|
896
|
-
return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
|
|
897
|
-
}
|
|
898
1094
|
function updateDocumentFrequency(entries, remove = false) {
|
|
899
1095
|
for (const entry of entries) {
|
|
900
1096
|
const tokens = tokenize(entry.content);
|
|
@@ -917,7 +1113,19 @@ function createIncrementalOptimizer(config2) {
|
|
|
917
1113
|
if (!cached) return null;
|
|
918
1114
|
return cached.entry;
|
|
919
1115
|
}
|
|
1116
|
+
const MAX_CACHE_SIZE = 1e4;
|
|
920
1117
|
function cacheEntry(entry, score) {
|
|
1118
|
+
if (state.entryCache.size >= MAX_CACHE_SIZE) {
|
|
1119
|
+
const entries = Array.from(state.entryCache.entries());
|
|
1120
|
+
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
1121
|
+
const toRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
|
|
1122
|
+
for (let i = 0; i < toRemove && i < entries.length; i++) {
|
|
1123
|
+
const entry2 = entries[i];
|
|
1124
|
+
if (entry2) {
|
|
1125
|
+
state.entryCache.delete(entry2[0]);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
921
1129
|
state.entryCache.set(entry.hash, {
|
|
922
1130
|
entry,
|
|
923
1131
|
score,
|
|
@@ -1044,13 +1252,40 @@ function createIncrementalOptimizer(config2) {
|
|
|
1044
1252
|
lastFullOptimization: state.lastFullOptimization
|
|
1045
1253
|
};
|
|
1046
1254
|
}
|
|
1255
|
+
function serializeState() {
|
|
1256
|
+
const s = getState();
|
|
1257
|
+
return JSON.stringify({
|
|
1258
|
+
entryCache: Array.from(s.entryCache.entries()),
|
|
1259
|
+
documentFrequency: Array.from(s.documentFrequency.entries()),
|
|
1260
|
+
totalDocuments: s.totalDocuments,
|
|
1261
|
+
updateCount: s.updateCount,
|
|
1262
|
+
lastFullOptimization: s.lastFullOptimization
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
function deserializeState(json) {
|
|
1266
|
+
try {
|
|
1267
|
+
const parsed = JSON.parse(json);
|
|
1268
|
+
restoreState({
|
|
1269
|
+
entryCache: new Map(parsed.entryCache),
|
|
1270
|
+
documentFrequency: new Map(parsed.documentFrequency),
|
|
1271
|
+
totalDocuments: parsed.totalDocuments,
|
|
1272
|
+
updateCount: parsed.updateCount,
|
|
1273
|
+
lastFullOptimization: parsed.lastFullOptimization
|
|
1274
|
+
});
|
|
1275
|
+
return true;
|
|
1276
|
+
} catch {
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1047
1280
|
return {
|
|
1048
1281
|
optimizeIncremental,
|
|
1049
1282
|
optimizeFull,
|
|
1050
1283
|
getState,
|
|
1051
1284
|
restoreState,
|
|
1052
1285
|
reset,
|
|
1053
|
-
getStats
|
|
1286
|
+
getStats,
|
|
1287
|
+
serializeState,
|
|
1288
|
+
deserializeState
|
|
1054
1289
|
};
|
|
1055
1290
|
}
|
|
1056
1291
|
|
|
@@ -1075,9 +1310,22 @@ function createContextPipeline(config2) {
|
|
|
1075
1310
|
currentEntries = result.kept;
|
|
1076
1311
|
budgetUtilization = result.budgetUtilization;
|
|
1077
1312
|
if (currentEntries.length > windowSize) {
|
|
1078
|
-
const
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
1313
|
+
const timestamps = currentEntries.map((e) => e.timestamp);
|
|
1314
|
+
const minTs = Math.min(...timestamps);
|
|
1315
|
+
const maxTs = Math.max(...timestamps);
|
|
1316
|
+
const tsRange = maxTs - minTs || 1;
|
|
1317
|
+
const scored = currentEntries.map((entry) => {
|
|
1318
|
+
if (entry.isBTSP) return { entry, hybridScore: 2 };
|
|
1319
|
+
const ageNormalized = (entry.timestamp - minTs) / tsRange;
|
|
1320
|
+
const hybridScore = ageNormalized * 0.4 + entry.score * 0.6;
|
|
1321
|
+
return { entry, hybridScore };
|
|
1322
|
+
});
|
|
1323
|
+
scored.sort((a, b) => {
|
|
1324
|
+
if (b.hybridScore !== a.hybridScore) return b.hybridScore - a.hybridScore;
|
|
1325
|
+
return b.entry.timestamp - a.entry.timestamp;
|
|
1326
|
+
});
|
|
1327
|
+
const toKeep = scored.slice(0, windowSize).map((s) => s.entry);
|
|
1328
|
+
const toRemove = scored.slice(windowSize);
|
|
1081
1329
|
currentEntries = toKeep;
|
|
1082
1330
|
evictedEntries += toRemove.length;
|
|
1083
1331
|
}
|
|
@@ -1113,7 +1361,15 @@ function createContextPipeline(config2) {
|
|
|
1113
1361
|
budgetUtilization = 0;
|
|
1114
1362
|
optimizer.reset();
|
|
1115
1363
|
}
|
|
1364
|
+
function serializeOptimizerState() {
|
|
1365
|
+
return optimizer.serializeState();
|
|
1366
|
+
}
|
|
1367
|
+
function deserializeOptimizerState(json) {
|
|
1368
|
+
return optimizer.deserializeState(json);
|
|
1369
|
+
}
|
|
1116
1370
|
return {
|
|
1371
|
+
serializeOptimizerState,
|
|
1372
|
+
deserializeOptimizerState,
|
|
1117
1373
|
ingest,
|
|
1118
1374
|
getContext,
|
|
1119
1375
|
getEntries,
|
|
@@ -1149,9 +1405,14 @@ function createFileTracker() {
|
|
|
1149
1405
|
}
|
|
1150
1406
|
return [];
|
|
1151
1407
|
}
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
1154
|
-
fd
|
|
1408
|
+
const bytesToRead = currentSize - pos.position;
|
|
1409
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
1410
|
+
const fd = (0, import_node_fs3.openSync)(filePath, "r");
|
|
1411
|
+
try {
|
|
1412
|
+
(0, import_node_fs3.readSync)(fd, buffer, 0, bytesToRead, pos.position);
|
|
1413
|
+
} finally {
|
|
1414
|
+
(0, import_node_fs3.closeSync)(fd);
|
|
1415
|
+
}
|
|
1155
1416
|
const newContent = (pos.partialLine + buffer.toString("utf-8")).split("\n");
|
|
1156
1417
|
const partialLine = newContent.pop() || "";
|
|
1157
1418
|
pos.position = currentSize;
|
|
@@ -1210,6 +1471,7 @@ function createSessionWatcher(config2) {
|
|
|
1210
1471
|
fullOptimizationInterval: 50
|
|
1211
1472
|
// Full re-optimization every 50 incremental updates
|
|
1212
1473
|
});
|
|
1474
|
+
loadState(sessionId, pipeline);
|
|
1213
1475
|
pipelines.set(sessionId, pipeline);
|
|
1214
1476
|
}
|
|
1215
1477
|
return pipeline;
|
|
@@ -1260,7 +1522,7 @@ function createSessionWatcher(config2) {
|
|
|
1260
1522
|
} else if (entry.endsWith(".jsonl")) {
|
|
1261
1523
|
const matches = realtime.watchPatterns.some((pattern) => {
|
|
1262
1524
|
const regex = new RegExp(
|
|
1263
|
-
pattern.replace(
|
|
1525
|
+
pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/(?<!\.)(\*)/g, "[^/\\\\]*")
|
|
1264
1526
|
);
|
|
1265
1527
|
return regex.test(fullPath);
|
|
1266
1528
|
});
|
|
@@ -1289,34 +1551,69 @@ function createSessionWatcher(config2) {
|
|
|
1289
1551
|
async function start() {
|
|
1290
1552
|
const projectsDir = getProjectsDir();
|
|
1291
1553
|
const jsonlFiles = findJsonlFiles(projectsDir);
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1554
|
+
try {
|
|
1555
|
+
const projectsWatcher = (0, import_node_fs4.watch)(projectsDir, { recursive: true }, (_eventType, filename) => {
|
|
1556
|
+
if (filename?.endsWith(".jsonl")) {
|
|
1557
|
+
const fullPath = (0, import_node_path.join)(projectsDir, filename);
|
|
1558
|
+
handleFileChange(fullPath);
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
watchers.push(projectsWatcher);
|
|
1562
|
+
} catch {
|
|
1563
|
+
const watchedDirs = /* @__PURE__ */ new Set();
|
|
1564
|
+
for (const file of jsonlFiles) {
|
|
1565
|
+
const dir = (0, import_node_path.dirname)(file);
|
|
1566
|
+
if (!watchedDirs.has(dir)) {
|
|
1567
|
+
const watcher2 = (0, import_node_fs4.watch)(dir, { recursive: false }, (_eventType, filename) => {
|
|
1568
|
+
if (filename?.endsWith(".jsonl")) {
|
|
1569
|
+
const fullPath = (0, import_node_path.join)(dir, filename);
|
|
1570
|
+
handleFileChange(fullPath);
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
watchers.push(watcher2);
|
|
1574
|
+
watchedDirs.add(dir);
|
|
1575
|
+
}
|
|
1304
1576
|
}
|
|
1305
1577
|
}
|
|
1306
|
-
const projectsWatcher = (0, import_node_fs4.watch)(projectsDir, { recursive: true }, (_eventType, filename) => {
|
|
1307
|
-
if (filename?.endsWith(".jsonl")) {
|
|
1308
|
-
const fullPath = (0, import_node_path.join)(projectsDir, filename);
|
|
1309
|
-
handleFileChange(fullPath);
|
|
1310
|
-
}
|
|
1311
|
-
});
|
|
1312
|
-
watchers.push(projectsWatcher);
|
|
1313
1578
|
getMetrics().updateDaemon({
|
|
1314
1579
|
startTime: Date.now(),
|
|
1315
1580
|
sessionsWatched: jsonlFiles.length,
|
|
1316
1581
|
memoryUsage: process.memoryUsage().heapUsed
|
|
1317
1582
|
});
|
|
1318
1583
|
}
|
|
1584
|
+
function getStatePath() {
|
|
1585
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".sparn", "optimizer-state.json");
|
|
1586
|
+
}
|
|
1587
|
+
function saveState() {
|
|
1588
|
+
try {
|
|
1589
|
+
const stateMap = {};
|
|
1590
|
+
for (const [sessionId, pipeline] of pipelines.entries()) {
|
|
1591
|
+
stateMap[sessionId] = pipeline.serializeOptimizerState();
|
|
1592
|
+
}
|
|
1593
|
+
const statePath = getStatePath();
|
|
1594
|
+
const dir = (0, import_node_path.dirname)(statePath);
|
|
1595
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
1596
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
1597
|
+
}
|
|
1598
|
+
(0, import_node_fs4.writeFileSync)(statePath, JSON.stringify(stateMap), "utf-8");
|
|
1599
|
+
} catch {
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function loadState(sessionId, pipeline) {
|
|
1603
|
+
try {
|
|
1604
|
+
const statePath = getStatePath();
|
|
1605
|
+
if (!(0, import_node_fs4.existsSync)(statePath)) return;
|
|
1606
|
+
const raw = (0, import_node_fs4.readFileSync)(statePath, "utf-8");
|
|
1607
|
+
const stateMap = JSON.parse(raw);
|
|
1608
|
+
const sessionState = stateMap[sessionId];
|
|
1609
|
+
if (sessionState) {
|
|
1610
|
+
pipeline.deserializeOptimizerState(sessionState);
|
|
1611
|
+
}
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1319
1615
|
function stop() {
|
|
1616
|
+
saveState();
|
|
1320
1617
|
for (const watcher2 of watchers) {
|
|
1321
1618
|
watcher2.close();
|
|
1322
1619
|
}
|
|
@@ -1364,11 +1661,22 @@ function createSessionWatcher(config2) {
|
|
|
1364
1661
|
var configJson = process.env["SPARN_CONFIG"];
|
|
1365
1662
|
var pidFile = process.env["SPARN_PID_FILE"];
|
|
1366
1663
|
var logFile = process.env["SPARN_LOG_FILE"];
|
|
1664
|
+
var configFilePath = process.env["SPARN_CONFIG_FILE"];
|
|
1665
|
+
if ((!configJson || !pidFile || !logFile) && configFilePath && (0, import_node_fs5.existsSync)(configFilePath)) {
|
|
1666
|
+
const fileConfig = JSON.parse((0, import_node_fs5.readFileSync)(configFilePath, "utf-8"));
|
|
1667
|
+
configJson = configJson || JSON.stringify(fileConfig.config);
|
|
1668
|
+
pidFile = pidFile || fileConfig.pidFile;
|
|
1669
|
+
logFile = logFile || fileConfig.logFile;
|
|
1670
|
+
}
|
|
1367
1671
|
if (!configJson || !pidFile || !logFile) {
|
|
1368
1672
|
console.error("Daemon: Missing required environment variables");
|
|
1369
1673
|
process.exit(1);
|
|
1370
1674
|
}
|
|
1371
1675
|
var config = JSON.parse(configJson);
|
|
1676
|
+
if (config.realtime?.preciseTokenCounting) {
|
|
1677
|
+
setPreciseTokenCounting(true);
|
|
1678
|
+
}
|
|
1679
|
+
(0, import_node_fs5.writeFileSync)(pidFile, String(process.pid), "utf-8");
|
|
1372
1680
|
function log(message) {
|
|
1373
1681
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1374
1682
|
const logMessage = `[${timestamp}] ${message}
|
|
@@ -1447,5 +1755,9 @@ watcher.start().then(async () => {
|
|
|
1447
1755
|
cleanup();
|
|
1448
1756
|
});
|
|
1449
1757
|
setInterval(() => {
|
|
1450
|
-
},
|
|
1758
|
+
}, 3e4);
|
|
1759
|
+
try {
|
|
1760
|
+
process.stdin.resume();
|
|
1761
|
+
} catch {
|
|
1762
|
+
}
|
|
1451
1763
|
//# sourceMappingURL=index.cjs.map
|