@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
@@ -1,5 +1,5 @@
1
1
  // src/daemon/index.ts
2
- import { appendFileSync as appendFileSync2, existsSync as existsSync2, unlinkSync } from "fs";
2
+ import { appendFileSync as appendFileSync2, existsSync as existsSync3, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
3
3
 
4
4
  // src/core/kv-memory.ts
5
5
  import { copyFileSync, existsSync } from "fs";
@@ -23,6 +23,7 @@ async function createKVMemory(dbPath) {
23
23
  const integrityCheck = db.pragma("quick_check", { simple: true });
24
24
  if (integrityCheck !== "ok") {
25
25
  console.error("\u26A0 Database corruption detected!");
26
+ db.close();
26
27
  if (existsSync(dbPath)) {
27
28
  const backupPath = createBackup(dbPath);
28
29
  if (backupPath) {
@@ -30,7 +31,6 @@ async function createKVMemory(dbPath) {
30
31
  }
31
32
  }
32
33
  console.log("Attempting database recovery...");
33
- db.close();
34
34
  db = new Database(dbPath);
35
35
  }
36
36
  } catch (error) {
@@ -42,6 +42,7 @@ async function createKVMemory(dbPath) {
42
42
  db = new Database(dbPath);
43
43
  }
44
44
  db.pragma("journal_mode = WAL");
45
+ db.pragma("foreign_keys = ON");
45
46
  db.exec(`
46
47
  CREATE TABLE IF NOT EXISTS entries_index (
47
48
  id TEXT PRIMARY KEY NOT NULL,
@@ -81,6 +82,36 @@ async function createKVMemory(dbPath) {
81
82
  CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
82
83
  CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
83
84
  `);
85
+ db.exec(`
86
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(id, content, tokenize='porter');
87
+ `);
88
+ db.exec(`
89
+ CREATE TRIGGER IF NOT EXISTS entries_fts_insert
90
+ AFTER INSERT ON entries_value
91
+ BEGIN
92
+ INSERT OR REPLACE INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
93
+ END;
94
+ `);
95
+ db.exec(`
96
+ CREATE TRIGGER IF NOT EXISTS entries_fts_delete
97
+ AFTER DELETE ON entries_value
98
+ BEGIN
99
+ DELETE FROM entries_fts WHERE id = OLD.id;
100
+ END;
101
+ `);
102
+ db.exec(`
103
+ CREATE TRIGGER IF NOT EXISTS entries_fts_update
104
+ AFTER UPDATE ON entries_value
105
+ BEGIN
106
+ DELETE FROM entries_fts WHERE id = OLD.id;
107
+ INSERT INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
108
+ END;
109
+ `);
110
+ db.exec(`
111
+ INSERT OR IGNORE INTO entries_fts(id, content)
112
+ SELECT id, content FROM entries_value
113
+ WHERE id NOT IN (SELECT id FROM entries_fts);
114
+ `);
84
115
  const putIndexStmt = db.prepare(`
85
116
  INSERT OR REPLACE INTO entries_index
86
117
  (id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
@@ -169,14 +200,20 @@ async function createKVMemory(dbPath) {
169
200
  sql += " AND i.isBTSP = ?";
170
201
  params.push(filters.isBTSP ? 1 : 0);
171
202
  }
203
+ if (filters.tags && filters.tags.length > 0) {
204
+ for (const tag of filters.tags) {
205
+ sql += " AND v.tags LIKE ?";
206
+ params.push(`%"${tag}"%`);
207
+ }
208
+ }
172
209
  sql += " ORDER BY i.score DESC";
173
210
  if (filters.limit) {
174
211
  sql += " LIMIT ?";
175
212
  params.push(filters.limit);
176
- }
177
- if (filters.offset) {
178
- sql += " OFFSET ?";
179
- params.push(filters.offset);
213
+ if (filters.offset) {
214
+ sql += " OFFSET ?";
215
+ params.push(filters.offset);
216
+ }
180
217
  }
181
218
  const stmt = db.prepare(sql);
182
219
  const rows = stmt.all(...params);
@@ -211,7 +248,22 @@ async function createKVMemory(dbPath) {
211
248
  },
212
249
  async compact() {
213
250
  const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
214
- db.exec("DELETE FROM entries_index WHERE ttl <= 0");
251
+ const now = Date.now();
252
+ db.prepare("DELETE FROM entries_index WHERE isBTSP = 0 AND (timestamp + ttl * 1000) < ?").run(
253
+ now
254
+ );
255
+ db.exec("DELETE FROM entries_index WHERE isBTSP = 0 AND ttl <= 0");
256
+ const candidates = db.prepare("SELECT id, timestamp, ttl FROM entries_index WHERE isBTSP = 0").all();
257
+ for (const row of candidates) {
258
+ const ageSeconds = Math.max(0, (now - row.timestamp) / 1e3);
259
+ const ttlSeconds = row.ttl;
260
+ if (ttlSeconds <= 0) continue;
261
+ const decay = 1 - Math.exp(-ageSeconds / ttlSeconds);
262
+ if (decay >= 0.95) {
263
+ db.prepare("DELETE FROM entries_index WHERE id = ?").run(row.id);
264
+ }
265
+ }
266
+ db.exec("DELETE FROM entries_value WHERE id NOT IN (SELECT id FROM entries_index)");
215
267
  db.exec("VACUUM");
216
268
  const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
217
269
  return before.count - after.count;
@@ -231,6 +283,9 @@ async function createKVMemory(dbPath) {
231
283
  stats.entries_pruned,
232
284
  stats.duration_ms
233
285
  );
286
+ db.prepare(
287
+ "DELETE FROM optimization_stats WHERE id NOT IN (SELECT id FROM optimization_stats ORDER BY timestamp DESC LIMIT 1000)"
288
+ ).run();
234
289
  },
235
290
  async getOptimizationStats() {
236
291
  const stmt = db.prepare(`
@@ -243,16 +298,114 @@ async function createKVMemory(dbPath) {
243
298
  },
244
299
  async clearOptimizationStats() {
245
300
  db.exec("DELETE FROM optimization_stats");
301
+ },
302
+ async searchFTS(query, limit = 10) {
303
+ if (!query || query.trim().length === 0) return [];
304
+ const sanitized = query.replace(/[{}()[\]"':*^~]/g, " ").trim();
305
+ if (sanitized.length === 0) return [];
306
+ const stmt = db.prepare(`
307
+ SELECT
308
+ f.id, f.content, rank,
309
+ i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
310
+ v.tags, v.metadata
311
+ FROM entries_fts f
312
+ JOIN entries_index i ON f.id = i.id
313
+ JOIN entries_value v ON f.id = v.id
314
+ WHERE entries_fts MATCH ?
315
+ ORDER BY rank
316
+ LIMIT ?
317
+ `);
318
+ try {
319
+ const rows = stmt.all(sanitized, limit);
320
+ return rows.map((r) => ({
321
+ entry: {
322
+ id: r.id,
323
+ content: r.content,
324
+ hash: r.hash,
325
+ timestamp: r.timestamp,
326
+ score: r.score,
327
+ ttl: r.ttl,
328
+ state: r.state,
329
+ accessCount: r.accessCount,
330
+ tags: r.tags ? JSON.parse(r.tags) : [],
331
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
332
+ isBTSP: r.isBTSP === 1
333
+ },
334
+ rank: r.rank
335
+ }));
336
+ } catch {
337
+ return [];
338
+ }
246
339
  }
247
340
  };
248
341
  }
249
342
 
343
+ // src/utils/tokenizer.ts
344
+ import { encode } from "gpt-tokenizer";
345
+ var usePrecise = false;
346
+ function setPreciseTokenCounting(enabled) {
347
+ usePrecise = enabled;
348
+ }
349
+ function estimateTokens(text) {
350
+ if (!text || text.length === 0) {
351
+ return 0;
352
+ }
353
+ if (usePrecise) {
354
+ return encode(text).length;
355
+ }
356
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
357
+ const wordCount = words.length;
358
+ const charCount = text.length;
359
+ const charEstimate = Math.ceil(charCount / 4);
360
+ const wordEstimate = Math.ceil(wordCount * 0.75);
361
+ return Math.max(wordEstimate, charEstimate);
362
+ }
363
+
250
364
  // src/daemon/consolidation-scheduler.ts
251
365
  import { appendFileSync } from "fs";
252
366
 
367
+ // src/utils/tfidf.ts
368
+ function tokenize(text) {
369
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
370
+ }
371
+ function calculateTF(term, tokens) {
372
+ const count = tokens.filter((t) => t === term).length;
373
+ return Math.sqrt(count);
374
+ }
375
+ function createTFIDFIndex(entries) {
376
+ const documentFrequency = /* @__PURE__ */ new Map();
377
+ for (const entry of entries) {
378
+ const tokens = tokenize(entry.content);
379
+ const uniqueTerms = new Set(tokens);
380
+ for (const term of uniqueTerms) {
381
+ documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
382
+ }
383
+ }
384
+ return {
385
+ documentFrequency,
386
+ totalDocuments: entries.length
387
+ };
388
+ }
389
+ function scoreTFIDF(entry, index) {
390
+ const tokens = tokenize(entry.content);
391
+ if (tokens.length === 0) return 0;
392
+ const uniqueTerms = new Set(tokens);
393
+ let totalScore = 0;
394
+ for (const term of uniqueTerms) {
395
+ const tf = calculateTF(term, tokens);
396
+ const docsWithTerm = index.documentFrequency.get(term) || 0;
397
+ if (docsWithTerm === 0) continue;
398
+ const idf = Math.log(index.totalDocuments / docsWithTerm);
399
+ totalScore += tf * idf;
400
+ }
401
+ return totalScore / tokens.length;
402
+ }
403
+
253
404
  // src/core/engram-scorer.ts
254
405
  function createEngramScorer(config2) {
255
406
  const { defaultTTL } = config2;
407
+ const recencyWindowMs = (config2.recencyBoostMinutes ?? 30) * 60 * 1e3;
408
+ const recencyMultiplier = config2.recencyBoostMultiplier ?? 1.3;
256
409
  function calculateDecay(ageInSeconds, ttlInSeconds) {
257
410
  if (ttlInSeconds === 0) return 1;
258
411
  if (ageInSeconds <= 0) return 0;
@@ -272,6 +425,13 @@ function createEngramScorer(config2) {
272
425
  if (entry.isBTSP) {
273
426
  score = Math.max(score, 0.9);
274
427
  }
428
+ if (!entry.isBTSP && recencyWindowMs > 0) {
429
+ const ageMs = currentTime - entry.timestamp;
430
+ if (ageMs >= 0 && ageMs < recencyWindowMs) {
431
+ const boostFactor = 1 + (recencyMultiplier - 1) * (1 - ageMs / recencyWindowMs);
432
+ score = score * boostFactor;
433
+ }
434
+ }
275
435
  return Math.max(0, Math.min(1, score));
276
436
  }
277
437
  function refreshTTL(entry) {
@@ -380,13 +540,15 @@ function createSleepCompressor() {
380
540
  function cosineSimilarity(text1, text2) {
381
541
  const words1 = tokenize(text1);
382
542
  const words2 = tokenize(text2);
383
- const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
384
543
  const vec1 = {};
385
544
  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;
545
+ for (const word of words1) {
546
+ vec1[word] = (vec1[word] ?? 0) + 1;
547
+ }
548
+ for (const word of words2) {
549
+ vec2[word] = (vec2[word] ?? 0) + 1;
389
550
  }
551
+ const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
390
552
  let dotProduct = 0;
391
553
  let mag1 = 0;
392
554
  let mag2 = 0;
@@ -402,9 +564,6 @@ function createSleepCompressor() {
402
564
  if (mag1 === 0 || mag2 === 0) return 0;
403
565
  return dotProduct / (mag1 * mag2);
404
566
  }
405
- function tokenize(text) {
406
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
407
- }
408
567
  return {
409
568
  consolidate,
410
569
  findDuplicates,
@@ -476,11 +635,10 @@ function createMetricsCollector() {
476
635
  ...metric
477
636
  };
478
637
  }
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;
638
+ function calculatePercentile(sortedValues, percentile) {
639
+ if (sortedValues.length === 0) return 0;
640
+ const index = Math.ceil(percentile / 100 * sortedValues.length) - 1;
641
+ return sortedValues[index] || 0;
484
642
  }
485
643
  function getSnapshot() {
486
644
  const totalRuns = optimizations.length;
@@ -491,7 +649,7 @@ function createMetricsCollector() {
491
649
  );
492
650
  const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
493
651
  const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
494
- const durations = optimizations.map((m) => m.duration);
652
+ const sortedDurations = optimizations.map((m) => m.duration).sort((a, b) => a - b);
495
653
  const totalCacheQueries = cacheHits + cacheMisses;
496
654
  const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
497
655
  return {
@@ -501,9 +659,9 @@ function createMetricsCollector() {
501
659
  totalDuration,
502
660
  totalTokensSaved,
503
661
  averageReduction,
504
- p50Latency: calculatePercentile(durations, 50),
505
- p95Latency: calculatePercentile(durations, 95),
506
- p99Latency: calculatePercentile(durations, 99)
662
+ p50Latency: calculatePercentile(sortedDurations, 50),
663
+ p95Latency: calculatePercentile(sortedDurations, 95),
664
+ p99Latency: calculatePercentile(sortedDurations, 99)
507
665
  },
508
666
  cache: {
509
667
  hitRate,
@@ -559,6 +717,7 @@ function createConsolidationScheduler(options) {
559
717
  let lastRun = null;
560
718
  let lastResult = null;
561
719
  let nextRun = null;
720
+ let isRunning = false;
562
721
  function log2(message) {
563
722
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
564
723
  const logMessage = `[${timestamp}] [Consolidation] ${message}
@@ -572,6 +731,11 @@ function createConsolidationScheduler(options) {
572
731
  }
573
732
  }
574
733
  async function runConsolidation() {
734
+ if (isRunning) {
735
+ log2("Consolidation already in progress, skipping");
736
+ return;
737
+ }
738
+ isRunning = true;
575
739
  const startTime = Date.now();
576
740
  log2("Starting scheduled consolidation");
577
741
  try {
@@ -624,6 +788,7 @@ function createConsolidationScheduler(options) {
624
788
  if (intervalHours !== null && intervalHours > 0) {
625
789
  nextRun = Date.now() + intervalHours * 60 * 60 * 1e3;
626
790
  }
791
+ isRunning = false;
627
792
  }
628
793
  function start() {
629
794
  if (intervalHours === null || intervalHours <= 0) {
@@ -669,7 +834,15 @@ function createConsolidationScheduler(options) {
669
834
  }
670
835
 
671
836
  // src/daemon/session-watcher.ts
672
- import { readdirSync, statSync as statSync2, watch } from "fs";
837
+ import {
838
+ existsSync as existsSync2,
839
+ mkdirSync,
840
+ readdirSync,
841
+ readFileSync,
842
+ statSync as statSync2,
843
+ watch,
844
+ writeFileSync
845
+ } from "fs";
673
846
  import { homedir } from "os";
674
847
  import { dirname, join } from "path";
675
848
 
@@ -684,6 +857,11 @@ function hashContent(content) {
684
857
 
685
858
  // src/utils/context-parser.ts
686
859
  function parseClaudeCodeContext(context) {
860
+ const firstNonEmpty = context.split("\n").find((line) => line.trim().length > 0);
861
+ if (firstNonEmpty?.trim().startsWith("{")) {
862
+ const jsonlEntries = parseJSONLContext(context);
863
+ if (jsonlEntries.length > 0) return jsonlEntries;
864
+ }
687
865
  const entries = [];
688
866
  const now = Date.now();
689
867
  const lines = context.split("\n");
@@ -736,7 +914,7 @@ function createEntry(content, type, baseTime) {
736
914
  hash: hashContent(content),
737
915
  timestamp: baseTime,
738
916
  score: initialScore,
739
- state: initialScore > 0.7 ? "active" : initialScore > 0.3 ? "ready" : "silent",
917
+ state: initialScore >= 0.7 ? "active" : initialScore >= 0.3 ? "ready" : "silent",
740
918
  ttl: 24 * 3600,
741
919
  // 24 hours default
742
920
  accessCount: 0,
@@ -745,52 +923,63 @@ function createEntry(content, type, baseTime) {
745
923
  isBTSP: false
746
924
  };
747
925
  }
748
-
749
- // src/utils/tokenizer.ts
750
- function estimateTokens(text) {
751
- if (!text || text.length === 0) {
752
- return 0;
926
+ function parseJSONLLine(line) {
927
+ const trimmed = line.trim();
928
+ if (trimmed.length === 0) return null;
929
+ try {
930
+ return JSON.parse(trimmed);
931
+ } catch {
932
+ return null;
753
933
  }
754
- const words = text.split(/\s+/).filter((w) => w.length > 0);
755
- const wordCount = words.length;
756
- const charCount = text.length;
757
- const charEstimate = Math.ceil(charCount / 4);
758
- const wordEstimate = Math.ceil(wordCount * 0.75);
759
- return Math.max(wordEstimate, charEstimate);
934
+ }
935
+ function extractContent(content) {
936
+ if (typeof content === "string") return content;
937
+ if (Array.isArray(content)) {
938
+ return content.map((block) => {
939
+ if (block.type === "text" && block.text) return block.text;
940
+ if (block.type === "tool_use" && block.name) return `[tool_use: ${block.name}]`;
941
+ if (block.type === "tool_result") {
942
+ if (typeof block.content === "string") return block.content;
943
+ if (Array.isArray(block.content)) {
944
+ return block.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
945
+ }
946
+ }
947
+ return "";
948
+ }).filter((s) => s.length > 0).join("\n");
949
+ }
950
+ return "";
951
+ }
952
+ function classifyJSONLMessage(msg) {
953
+ if (Array.isArray(msg.content)) {
954
+ const hasToolUse = msg.content.some((b) => b.type === "tool_use");
955
+ const hasToolResult = msg.content.some((b) => b.type === "tool_result");
956
+ if (hasToolUse) return "tool";
957
+ if (hasToolResult) return "result";
958
+ }
959
+ if (msg.type === "tool_use" || msg.tool_use) return "tool";
960
+ if (msg.type === "tool_result" || msg.tool_result) return "result";
961
+ if (msg.role === "user" || msg.role === "assistant") return "conversation";
962
+ return "other";
963
+ }
964
+ function parseJSONLContext(context) {
965
+ const entries = [];
966
+ const now = Date.now();
967
+ const lines = context.split("\n");
968
+ for (const line of lines) {
969
+ const msg = parseJSONLLine(line);
970
+ if (!msg) continue;
971
+ const content = extractContent(msg.content);
972
+ if (!content || content.trim().length === 0) continue;
973
+ const blockType = classifyJSONLMessage(msg);
974
+ entries.push(createEntry(content, blockType, now));
975
+ }
976
+ return entries;
760
977
  }
761
978
 
762
979
  // src/core/budget-pruner.ts
763
980
  function createBudgetPruner(config2) {
764
981
  const { tokenBudget, decay } = config2;
765
982
  const engramScorer = createEngramScorer(decay);
766
- function tokenize(text) {
767
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
768
- }
769
- function calculateTF(term, tokens) {
770
- const count = tokens.filter((t) => t === term).length;
771
- return Math.sqrt(count);
772
- }
773
- function calculateIDF(term, allEntries) {
774
- const totalDocs = allEntries.length;
775
- const docsWithTerm = allEntries.filter((entry) => {
776
- const tokens = tokenize(entry.content);
777
- return tokens.includes(term);
778
- }).length;
779
- if (docsWithTerm === 0) return 0;
780
- return Math.log(totalDocs / docsWithTerm);
781
- }
782
- function calculateTFIDF(entry, allEntries) {
783
- const tokens = tokenize(entry.content);
784
- if (tokens.length === 0) return 0;
785
- const uniqueTerms = [...new Set(tokens)];
786
- let totalScore = 0;
787
- for (const term of uniqueTerms) {
788
- const tf = calculateTF(term, tokens);
789
- const idf = calculateIDF(term, allEntries);
790
- totalScore += tf * idf;
791
- }
792
- return totalScore / tokens.length;
793
- }
794
983
  function getStateMultiplier(entry) {
795
984
  if (entry.isBTSP) return 2;
796
985
  switch (entry.state) {
@@ -804,8 +993,8 @@ function createBudgetPruner(config2) {
804
993
  return 1;
805
994
  }
806
995
  }
807
- function priorityScore(entry, allEntries) {
808
- const tfidf = calculateTFIDF(entry, allEntries);
996
+ function priorityScore(entry, allEntries, index) {
997
+ const tfidf = index ? scoreTFIDF(entry, index) : scoreTFIDF(entry, createTFIDFIndex(allEntries));
809
998
  const currentScore = engramScorer.calculateScore(entry);
810
999
  const engramDecay = 1 - currentScore;
811
1000
  const stateMultiplier = getStateMultiplier(entry);
@@ -824,15 +1013,33 @@ function createBudgetPruner(config2) {
824
1013
  const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
825
1014
  const btspEntries = entries.filter((e) => e.isBTSP);
826
1015
  const regularEntries = entries.filter((e) => !e.isBTSP);
827
- const btspTokens = btspEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
1016
+ let includedBtsp = [];
1017
+ let btspTokens = 0;
1018
+ const sortedBtsp = [...btspEntries].sort((a, b) => b.timestamp - a.timestamp);
1019
+ for (const entry of sortedBtsp) {
1020
+ const tokens = estimateTokens(entry.content);
1021
+ if (btspTokens + tokens <= budget * 0.8) {
1022
+ includedBtsp.push(entry);
1023
+ btspTokens += tokens;
1024
+ }
1025
+ }
1026
+ if (includedBtsp.length === 0 && sortedBtsp.length > 0) {
1027
+ const firstBtsp = sortedBtsp[0];
1028
+ if (firstBtsp) {
1029
+ includedBtsp = [firstBtsp];
1030
+ btspTokens = estimateTokens(firstBtsp.content);
1031
+ }
1032
+ }
1033
+ const excludedBtsp = btspEntries.filter((e) => !includedBtsp.includes(e));
1034
+ const tfidfIndex = createTFIDFIndex(entries);
828
1035
  const scored = regularEntries.map((entry) => ({
829
1036
  entry,
830
- score: priorityScore(entry, entries),
1037
+ score: priorityScore(entry, entries, tfidfIndex),
831
1038
  tokens: estimateTokens(entry.content)
832
1039
  }));
833
1040
  scored.sort((a, b) => b.score - a.score);
834
- const kept = [...btspEntries];
835
- const removed = [];
1041
+ const kept = [...includedBtsp];
1042
+ const removed = [...excludedBtsp];
836
1043
  let currentTokens = btspTokens;
837
1044
  for (const item of scored) {
838
1045
  if (currentTokens + item.tokens <= budget) {
@@ -868,9 +1075,6 @@ function createIncrementalOptimizer(config2) {
868
1075
  updateCount: 0,
869
1076
  lastFullOptimization: Date.now()
870
1077
  };
871
- function tokenize(text) {
872
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
873
- }
874
1078
  function updateDocumentFrequency(entries, remove = false) {
875
1079
  for (const entry of entries) {
876
1080
  const tokens = tokenize(entry.content);
@@ -893,7 +1097,19 @@ function createIncrementalOptimizer(config2) {
893
1097
  if (!cached) return null;
894
1098
  return cached.entry;
895
1099
  }
1100
+ const MAX_CACHE_SIZE = 1e4;
896
1101
  function cacheEntry(entry, score) {
1102
+ if (state.entryCache.size >= MAX_CACHE_SIZE) {
1103
+ const entries = Array.from(state.entryCache.entries());
1104
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
1105
+ const toRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
1106
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
1107
+ const entry2 = entries[i];
1108
+ if (entry2) {
1109
+ state.entryCache.delete(entry2[0]);
1110
+ }
1111
+ }
1112
+ }
897
1113
  state.entryCache.set(entry.hash, {
898
1114
  entry,
899
1115
  score,
@@ -1020,13 +1236,40 @@ function createIncrementalOptimizer(config2) {
1020
1236
  lastFullOptimization: state.lastFullOptimization
1021
1237
  };
1022
1238
  }
1239
+ function serializeState() {
1240
+ const s = getState();
1241
+ return JSON.stringify({
1242
+ entryCache: Array.from(s.entryCache.entries()),
1243
+ documentFrequency: Array.from(s.documentFrequency.entries()),
1244
+ totalDocuments: s.totalDocuments,
1245
+ updateCount: s.updateCount,
1246
+ lastFullOptimization: s.lastFullOptimization
1247
+ });
1248
+ }
1249
+ function deserializeState(json) {
1250
+ try {
1251
+ const parsed = JSON.parse(json);
1252
+ restoreState({
1253
+ entryCache: new Map(parsed.entryCache),
1254
+ documentFrequency: new Map(parsed.documentFrequency),
1255
+ totalDocuments: parsed.totalDocuments,
1256
+ updateCount: parsed.updateCount,
1257
+ lastFullOptimization: parsed.lastFullOptimization
1258
+ });
1259
+ return true;
1260
+ } catch {
1261
+ return false;
1262
+ }
1263
+ }
1023
1264
  return {
1024
1265
  optimizeIncremental,
1025
1266
  optimizeFull,
1026
1267
  getState,
1027
1268
  restoreState,
1028
1269
  reset,
1029
- getStats
1270
+ getStats,
1271
+ serializeState,
1272
+ deserializeState
1030
1273
  };
1031
1274
  }
1032
1275
 
@@ -1051,9 +1294,22 @@ function createContextPipeline(config2) {
1051
1294
  currentEntries = result.kept;
1052
1295
  budgetUtilization = result.budgetUtilization;
1053
1296
  if (currentEntries.length > windowSize) {
1054
- const sorted = [...currentEntries].sort((a, b) => b.timestamp - a.timestamp);
1055
- const toKeep = sorted.slice(0, windowSize);
1056
- const toRemove = sorted.slice(windowSize);
1297
+ const timestamps = currentEntries.map((e) => e.timestamp);
1298
+ const minTs = Math.min(...timestamps);
1299
+ const maxTs = Math.max(...timestamps);
1300
+ const tsRange = maxTs - minTs || 1;
1301
+ const scored = currentEntries.map((entry) => {
1302
+ if (entry.isBTSP) return { entry, hybridScore: 2 };
1303
+ const ageNormalized = (entry.timestamp - minTs) / tsRange;
1304
+ const hybridScore = ageNormalized * 0.4 + entry.score * 0.6;
1305
+ return { entry, hybridScore };
1306
+ });
1307
+ scored.sort((a, b) => {
1308
+ if (b.hybridScore !== a.hybridScore) return b.hybridScore - a.hybridScore;
1309
+ return b.entry.timestamp - a.entry.timestamp;
1310
+ });
1311
+ const toKeep = scored.slice(0, windowSize).map((s) => s.entry);
1312
+ const toRemove = scored.slice(windowSize);
1057
1313
  currentEntries = toKeep;
1058
1314
  evictedEntries += toRemove.length;
1059
1315
  }
@@ -1089,7 +1345,15 @@ function createContextPipeline(config2) {
1089
1345
  budgetUtilization = 0;
1090
1346
  optimizer.reset();
1091
1347
  }
1348
+ function serializeOptimizerState() {
1349
+ return optimizer.serializeState();
1350
+ }
1351
+ function deserializeOptimizerState(json) {
1352
+ return optimizer.deserializeState(json);
1353
+ }
1092
1354
  return {
1355
+ serializeOptimizerState,
1356
+ deserializeOptimizerState,
1093
1357
  ingest,
1094
1358
  getContext,
1095
1359
  getEntries,
@@ -1099,7 +1363,7 @@ function createContextPipeline(config2) {
1099
1363
  }
1100
1364
 
1101
1365
  // src/daemon/file-tracker.ts
1102
- import { readFileSync, statSync } from "fs";
1366
+ import { closeSync, openSync, readSync, statSync } from "fs";
1103
1367
  function createFileTracker() {
1104
1368
  const positions = /* @__PURE__ */ new Map();
1105
1369
  function readNewLines(filePath) {
@@ -1125,9 +1389,14 @@ function createFileTracker() {
1125
1389
  }
1126
1390
  return [];
1127
1391
  }
1128
- const buffer = Buffer.alloc(currentSize - pos.position);
1129
- const fd = readFileSync(filePath);
1130
- fd.copy(buffer, 0, pos.position, currentSize);
1392
+ const bytesToRead = currentSize - pos.position;
1393
+ const buffer = Buffer.alloc(bytesToRead);
1394
+ const fd = openSync(filePath, "r");
1395
+ try {
1396
+ readSync(fd, buffer, 0, bytesToRead, pos.position);
1397
+ } finally {
1398
+ closeSync(fd);
1399
+ }
1131
1400
  const newContent = (pos.partialLine + buffer.toString("utf-8")).split("\n");
1132
1401
  const partialLine = newContent.pop() || "";
1133
1402
  pos.position = currentSize;
@@ -1186,6 +1455,7 @@ function createSessionWatcher(config2) {
1186
1455
  fullOptimizationInterval: 50
1187
1456
  // Full re-optimization every 50 incremental updates
1188
1457
  });
1458
+ loadState(sessionId, pipeline);
1189
1459
  pipelines.set(sessionId, pipeline);
1190
1460
  }
1191
1461
  return pipeline;
@@ -1236,7 +1506,7 @@ function createSessionWatcher(config2) {
1236
1506
  } else if (entry.endsWith(".jsonl")) {
1237
1507
  const matches = realtime.watchPatterns.some((pattern) => {
1238
1508
  const regex = new RegExp(
1239
- pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*").replace(/\./g, "\\.")
1509
+ pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/(?<!\.)(\*)/g, "[^/\\\\]*")
1240
1510
  );
1241
1511
  return regex.test(fullPath);
1242
1512
  });
@@ -1265,34 +1535,69 @@ function createSessionWatcher(config2) {
1265
1535
  async function start() {
1266
1536
  const projectsDir = getProjectsDir();
1267
1537
  const jsonlFiles = findJsonlFiles(projectsDir);
1268
- const watchedDirs = /* @__PURE__ */ new Set();
1269
- for (const file of jsonlFiles) {
1270
- const dir = dirname(file);
1271
- if (!watchedDirs.has(dir)) {
1272
- const watcher2 = watch(dir, { recursive: false }, (_eventType, filename) => {
1273
- if (filename?.endsWith(".jsonl")) {
1274
- const fullPath = join(dir, filename);
1275
- handleFileChange(fullPath);
1276
- }
1277
- });
1278
- watchers.push(watcher2);
1279
- watchedDirs.add(dir);
1538
+ try {
1539
+ const projectsWatcher = watch(projectsDir, { recursive: true }, (_eventType, filename) => {
1540
+ if (filename?.endsWith(".jsonl")) {
1541
+ const fullPath = join(projectsDir, filename);
1542
+ handleFileChange(fullPath);
1543
+ }
1544
+ });
1545
+ watchers.push(projectsWatcher);
1546
+ } catch {
1547
+ const watchedDirs = /* @__PURE__ */ new Set();
1548
+ for (const file of jsonlFiles) {
1549
+ const dir = dirname(file);
1550
+ if (!watchedDirs.has(dir)) {
1551
+ const watcher2 = watch(dir, { recursive: false }, (_eventType, filename) => {
1552
+ if (filename?.endsWith(".jsonl")) {
1553
+ const fullPath = join(dir, filename);
1554
+ handleFileChange(fullPath);
1555
+ }
1556
+ });
1557
+ watchers.push(watcher2);
1558
+ watchedDirs.add(dir);
1559
+ }
1280
1560
  }
1281
1561
  }
1282
- const projectsWatcher = watch(projectsDir, { recursive: true }, (_eventType, filename) => {
1283
- if (filename?.endsWith(".jsonl")) {
1284
- const fullPath = join(projectsDir, filename);
1285
- handleFileChange(fullPath);
1286
- }
1287
- });
1288
- watchers.push(projectsWatcher);
1289
1562
  getMetrics().updateDaemon({
1290
1563
  startTime: Date.now(),
1291
1564
  sessionsWatched: jsonlFiles.length,
1292
1565
  memoryUsage: process.memoryUsage().heapUsed
1293
1566
  });
1294
1567
  }
1568
+ function getStatePath() {
1569
+ return join(homedir(), ".sparn", "optimizer-state.json");
1570
+ }
1571
+ function saveState() {
1572
+ try {
1573
+ const stateMap = {};
1574
+ for (const [sessionId, pipeline] of pipelines.entries()) {
1575
+ stateMap[sessionId] = pipeline.serializeOptimizerState();
1576
+ }
1577
+ const statePath = getStatePath();
1578
+ const dir = dirname(statePath);
1579
+ if (!existsSync2(dir)) {
1580
+ mkdirSync(dir, { recursive: true });
1581
+ }
1582
+ writeFileSync(statePath, JSON.stringify(stateMap), "utf-8");
1583
+ } catch {
1584
+ }
1585
+ }
1586
+ function loadState(sessionId, pipeline) {
1587
+ try {
1588
+ const statePath = getStatePath();
1589
+ if (!existsSync2(statePath)) return;
1590
+ const raw = readFileSync(statePath, "utf-8");
1591
+ const stateMap = JSON.parse(raw);
1592
+ const sessionState = stateMap[sessionId];
1593
+ if (sessionState) {
1594
+ pipeline.deserializeOptimizerState(sessionState);
1595
+ }
1596
+ } catch {
1597
+ }
1598
+ }
1295
1599
  function stop() {
1600
+ saveState();
1296
1601
  for (const watcher2 of watchers) {
1297
1602
  watcher2.close();
1298
1603
  }
@@ -1340,11 +1645,22 @@ function createSessionWatcher(config2) {
1340
1645
  var configJson = process.env["SPARN_CONFIG"];
1341
1646
  var pidFile = process.env["SPARN_PID_FILE"];
1342
1647
  var logFile = process.env["SPARN_LOG_FILE"];
1648
+ var configFilePath = process.env["SPARN_CONFIG_FILE"];
1649
+ if ((!configJson || !pidFile || !logFile) && configFilePath && existsSync3(configFilePath)) {
1650
+ const fileConfig = JSON.parse(readFileSync2(configFilePath, "utf-8"));
1651
+ configJson = configJson || JSON.stringify(fileConfig.config);
1652
+ pidFile = pidFile || fileConfig.pidFile;
1653
+ logFile = logFile || fileConfig.logFile;
1654
+ }
1343
1655
  if (!configJson || !pidFile || !logFile) {
1344
1656
  console.error("Daemon: Missing required environment variables");
1345
1657
  process.exit(1);
1346
1658
  }
1347
1659
  var config = JSON.parse(configJson);
1660
+ if (config.realtime?.preciseTokenCounting) {
1661
+ setPreciseTokenCounting(true);
1662
+ }
1663
+ writeFileSync2(pidFile, String(process.pid), "utf-8");
1348
1664
  function log(message) {
1349
1665
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1350
1666
  const logMessage = `[${timestamp}] ${message}
@@ -1358,7 +1674,7 @@ function log(message) {
1358
1674
  }
1359
1675
  function cleanup() {
1360
1676
  log("Daemon shutting down");
1361
- if (pidFile && existsSync2(pidFile)) {
1677
+ if (pidFile && existsSync3(pidFile)) {
1362
1678
  try {
1363
1679
  unlinkSync(pidFile);
1364
1680
  } catch {
@@ -1423,5 +1739,9 @@ watcher.start().then(async () => {
1423
1739
  cleanup();
1424
1740
  });
1425
1741
  setInterval(() => {
1426
- }, 6e4);
1742
+ }, 3e4);
1743
+ try {
1744
+ process.stdin.resume();
1745
+ } catch {
1746
+ }
1427
1747
  //# sourceMappingURL=index.js.map