@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.
Files changed (42) hide show
  1. package/PRIVACY.md +1 -1
  2. package/README.md +136 -642
  3. package/SECURITY.md +1 -1
  4. package/dist/cli/dashboard.cjs +3977 -0
  5. package/dist/cli/dashboard.cjs.map +1 -0
  6. package/dist/cli/dashboard.d.cts +17 -0
  7. package/dist/cli/dashboard.d.ts +17 -0
  8. package/dist/cli/dashboard.js +3932 -0
  9. package/dist/cli/dashboard.js.map +1 -0
  10. package/dist/cli/index.cjs +3853 -484
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +3810 -457
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/daemon/index.cjs +411 -99
  15. package/dist/daemon/index.cjs.map +1 -1
  16. package/dist/daemon/index.js +423 -103
  17. package/dist/daemon/index.js.map +1 -1
  18. package/dist/hooks/post-tool-result.cjs +115 -266
  19. package/dist/hooks/post-tool-result.cjs.map +1 -1
  20. package/dist/hooks/post-tool-result.js +115 -266
  21. package/dist/hooks/post-tool-result.js.map +1 -1
  22. package/dist/hooks/pre-prompt.cjs +197 -268
  23. package/dist/hooks/pre-prompt.cjs.map +1 -1
  24. package/dist/hooks/pre-prompt.js +182 -268
  25. package/dist/hooks/pre-prompt.js.map +1 -1
  26. package/dist/hooks/stop-docs-refresh.cjs +123 -0
  27. package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
  28. package/dist/hooks/stop-docs-refresh.d.cts +1 -0
  29. package/dist/hooks/stop-docs-refresh.d.ts +1 -0
  30. package/dist/hooks/stop-docs-refresh.js +126 -0
  31. package/dist/hooks/stop-docs-refresh.js.map +1 -0
  32. package/dist/index.cjs +1754 -337
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +539 -40
  35. package/dist/index.d.ts +539 -40
  36. package/dist/index.js +1737 -329
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/index.cjs +304 -71
  39. package/dist/mcp/index.cjs.map +1 -1
  40. package/dist/mcp/index.js +308 -71
  41. package/dist/mcp/index.js.map +1 -1
  42. package/package.json +10 -3
@@ -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
- if (filters.offset) {
202
- sql += " OFFSET ?";
203
- params.push(filters.offset);
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
- db.exec("DELETE FROM entries_index WHERE ttl <= 0");
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 vocab) {
411
- vec1[word] = words1.filter((w) => w === word).length;
412
- vec2[word] = words2.filter((w) => w === word).length;
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(values, percentile) {
504
- if (values.length === 0) return 0;
505
- const sorted = [...values].sort((a, b) => a - b);
506
- const index = Math.ceil(percentile / 100 * sorted.length) - 1;
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 durations = optimizations.map((m) => m.duration);
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(durations, 50),
529
- p95Latency: calculatePercentile(durations, 95),
530
- p99Latency: calculatePercentile(durations, 99)
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 > 0.7 ? "active" : initialScore > 0.3 ? "ready" : "silent",
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
- // src/utils/tokenizer.ts
774
- function estimateTokens(text) {
775
- if (!text || text.length === 0) {
776
- return 0;
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
- const words = text.split(/\s+/).filter((w) => w.length > 0);
779
- const wordCount = words.length;
780
- const charCount = text.length;
781
- const charEstimate = Math.ceil(charCount / 4);
782
- const wordEstimate = Math.ceil(wordCount * 0.75);
783
- return Math.max(wordEstimate, charEstimate);
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 = calculateTFIDF(entry, allEntries);
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
- const btspTokens = btspEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
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 = [...btspEntries];
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 sorted = [...currentEntries].sort((a, b) => b.timestamp - a.timestamp);
1079
- const toKeep = sorted.slice(0, windowSize);
1080
- const toRemove = sorted.slice(windowSize);
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 buffer = Buffer.alloc(currentSize - pos.position);
1153
- const fd = (0, import_node_fs3.readFileSync)(filePath);
1154
- fd.copy(buffer, 0, pos.position, currentSize);
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(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*").replace(/\./g, "\\.")
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
- const watchedDirs = /* @__PURE__ */ new Set();
1293
- for (const file of jsonlFiles) {
1294
- const dir = (0, import_node_path.dirname)(file);
1295
- if (!watchedDirs.has(dir)) {
1296
- const watcher2 = (0, import_node_fs4.watch)(dir, { recursive: false }, (_eventType, filename) => {
1297
- if (filename?.endsWith(".jsonl")) {
1298
- const fullPath = (0, import_node_path.join)(dir, filename);
1299
- handleFileChange(fullPath);
1300
- }
1301
- });
1302
- watchers.push(watcher2);
1303
- watchedDirs.add(dir);
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
- }, 6e4);
1758
+ }, 3e4);
1759
+ try {
1760
+ process.stdin.resume();
1761
+ } catch {
1762
+ }
1451
1763
  //# sourceMappingURL=index.cjs.map