@ulrichc1/sparn 1.0.1 → 1.1.1

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/dist/index.cjs CHANGED
@@ -32,21 +32,32 @@ var src_exports = {};
32
32
  __export(src_exports, {
33
33
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
34
34
  createBTSPEmbedder: () => createBTSPEmbedder,
35
+ createBudgetPruner: () => createBudgetPruner,
36
+ createBudgetPrunerFromConfig: () => createBudgetPrunerFromConfig,
35
37
  createClaudeCodeAdapter: () => createClaudeCodeAdapter,
36
38
  createConfidenceStates: () => createConfidenceStates,
39
+ createContextPipeline: () => createContextPipeline,
40
+ createDaemonCommand: () => createDaemonCommand,
37
41
  createEngramScorer: () => createEngramScorer,
42
+ createEntry: () => createEntry,
43
+ createFileTracker: () => createFileTracker,
38
44
  createGenericAdapter: () => createGenericAdapter,
45
+ createIncrementalOptimizer: () => createIncrementalOptimizer,
39
46
  createKVMemory: () => createKVMemory,
40
47
  createLogger: () => createLogger,
48
+ createSessionWatcher: () => createSessionWatcher,
41
49
  createSleepCompressor: () => createSleepCompressor,
42
50
  createSparsePruner: () => createSparsePruner,
43
51
  estimateTokens: () => estimateTokens,
44
- hashContent: () => hashContent
52
+ hashContent: () => hashContent,
53
+ parseClaudeCodeContext: () => parseClaudeCodeContext,
54
+ parseGenericContext: () => parseGenericContext
45
55
  });
46
56
  module.exports = __toCommonJS(src_exports);
47
57
 
48
- // src/adapters/claude-code.ts
49
- var import_node_crypto3 = require("crypto");
58
+ // node_modules/tsup/assets/cjs_shims.js
59
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
60
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
50
61
 
51
62
  // src/core/btsp-embedder.ts
52
63
  var import_node_crypto2 = require("crypto");
@@ -261,6 +272,82 @@ function createSparsePruner(config) {
261
272
  };
262
273
  }
263
274
 
275
+ // src/utils/context-parser.ts
276
+ var import_node_crypto3 = require("crypto");
277
+ function parseClaudeCodeContext(context) {
278
+ const entries = [];
279
+ const now = Date.now();
280
+ const lines = context.split("\n");
281
+ let currentBlock = [];
282
+ let blockType = "other";
283
+ for (const line of lines) {
284
+ const trimmed = line.trim();
285
+ if (trimmed.startsWith("User:") || trimmed.startsWith("Assistant:")) {
286
+ if (currentBlock.length > 0) {
287
+ entries.push(createEntry(currentBlock.join("\n"), blockType, now));
288
+ currentBlock = [];
289
+ }
290
+ blockType = "conversation";
291
+ currentBlock.push(line);
292
+ } else if (trimmed.includes("<function_calls>") || trimmed.includes("<invoke>") || trimmed.includes("<tool_use>")) {
293
+ if (currentBlock.length > 0) {
294
+ entries.push(createEntry(currentBlock.join("\n"), blockType, now));
295
+ currentBlock = [];
296
+ }
297
+ blockType = "tool";
298
+ currentBlock.push(line);
299
+ } else if (trimmed.includes("<function_results>") || trimmed.includes("</function_results>")) {
300
+ if (currentBlock.length > 0 && blockType !== "result") {
301
+ entries.push(createEntry(currentBlock.join("\n"), blockType, now));
302
+ currentBlock = [];
303
+ }
304
+ blockType = "result";
305
+ currentBlock.push(line);
306
+ } else if (currentBlock.length > 0) {
307
+ currentBlock.push(line);
308
+ } else if (trimmed.length > 0) {
309
+ currentBlock.push(line);
310
+ blockType = "other";
311
+ }
312
+ }
313
+ if (currentBlock.length > 0) {
314
+ entries.push(createEntry(currentBlock.join("\n"), blockType, now));
315
+ }
316
+ return entries.filter((e) => e.content.trim().length > 0);
317
+ }
318
+ function createEntry(content, type, baseTime) {
319
+ const tags = [type];
320
+ let initialScore = 0.5;
321
+ if (type === "conversation") initialScore = 0.8;
322
+ if (type === "tool") initialScore = 0.7;
323
+ if (type === "result") initialScore = 0.4;
324
+ return {
325
+ id: (0, import_node_crypto3.randomUUID)(),
326
+ content,
327
+ hash: hashContent(content),
328
+ timestamp: baseTime,
329
+ score: initialScore,
330
+ state: initialScore > 0.7 ? "active" : initialScore > 0.3 ? "ready" : "silent",
331
+ ttl: 24 * 3600,
332
+ // 24 hours default
333
+ accessCount: 0,
334
+ tags,
335
+ metadata: { type },
336
+ isBTSP: false
337
+ };
338
+ }
339
+ function parseGenericContext(context) {
340
+ const entries = [];
341
+ const now = Date.now();
342
+ const blocks = context.split(/\n\n+/);
343
+ for (const block of blocks) {
344
+ const trimmed = block.trim();
345
+ if (trimmed.length === 0) continue;
346
+ entries.push(createEntry(trimmed, "other", now));
347
+ }
348
+ return entries;
349
+ }
350
+
264
351
  // src/adapters/claude-code.ts
265
352
  var CLAUDE_CODE_PROFILE = {
266
353
  // More aggressive pruning for tool results (they can be verbose)
@@ -380,68 +467,6 @@ function createClaudeCodeAdapter(memory, config) {
380
467
  optimize
381
468
  };
382
469
  }
383
- function parseClaudeCodeContext(context) {
384
- const entries = [];
385
- const now = Date.now();
386
- const lines = context.split("\n");
387
- let currentBlock = [];
388
- let blockType = "other";
389
- for (const line of lines) {
390
- const trimmed = line.trim();
391
- if (trimmed.startsWith("User:") || trimmed.startsWith("Assistant:")) {
392
- if (currentBlock.length > 0) {
393
- entries.push(createEntry(currentBlock.join("\n"), blockType, now));
394
- currentBlock = [];
395
- }
396
- blockType = "conversation";
397
- currentBlock.push(line);
398
- } else if (trimmed.includes("<function_calls>") || trimmed.includes("<invoke>") || trimmed.includes("<tool_use>")) {
399
- if (currentBlock.length > 0) {
400
- entries.push(createEntry(currentBlock.join("\n"), blockType, now));
401
- currentBlock = [];
402
- }
403
- blockType = "tool";
404
- currentBlock.push(line);
405
- } else if (trimmed.includes("<function_results>") || trimmed.includes("</function_results>")) {
406
- if (currentBlock.length > 0 && blockType !== "result") {
407
- entries.push(createEntry(currentBlock.join("\n"), blockType, now));
408
- currentBlock = [];
409
- }
410
- blockType = "result";
411
- currentBlock.push(line);
412
- } else if (currentBlock.length > 0) {
413
- currentBlock.push(line);
414
- } else if (trimmed.length > 0) {
415
- currentBlock.push(line);
416
- blockType = "other";
417
- }
418
- }
419
- if (currentBlock.length > 0) {
420
- entries.push(createEntry(currentBlock.join("\n"), blockType, now));
421
- }
422
- return entries.filter((e) => e.content.trim().length > 0);
423
- }
424
- function createEntry(content, type, baseTime) {
425
- const tags = [type];
426
- let initialScore = 0.5;
427
- if (type === "conversation") initialScore = 0.8;
428
- if (type === "tool") initialScore = 0.7;
429
- if (type === "result") initialScore = 0.4;
430
- return {
431
- id: (0, import_node_crypto3.randomUUID)(),
432
- content,
433
- hash: hashContent(content),
434
- timestamp: baseTime,
435
- score: initialScore,
436
- state: initialScore > 0.7 ? "active" : initialScore > 0.3 ? "ready" : "silent",
437
- ttl: 24 * 3600,
438
- // 24 hours default
439
- accessCount: 0,
440
- tags,
441
- metadata: { type },
442
- isBTSP: false
443
- };
444
- }
445
470
 
446
471
  // src/adapters/generic.ts
447
472
  var import_node_crypto4 = require("crypto");
@@ -519,6 +544,459 @@ function createGenericAdapter(memory, config) {
519
544
  };
520
545
  }
521
546
 
547
+ // src/core/budget-pruner.ts
548
+ function createBudgetPruner(config) {
549
+ const { tokenBudget, decay } = config;
550
+ const engramScorer = createEngramScorer(decay);
551
+ function tokenize(text) {
552
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
553
+ }
554
+ function calculateTF(term, tokens) {
555
+ const count = tokens.filter((t) => t === term).length;
556
+ return Math.sqrt(count);
557
+ }
558
+ function calculateIDF(term, allEntries) {
559
+ const totalDocs = allEntries.length;
560
+ const docsWithTerm = allEntries.filter((entry) => {
561
+ const tokens = tokenize(entry.content);
562
+ return tokens.includes(term);
563
+ }).length;
564
+ if (docsWithTerm === 0) return 0;
565
+ return Math.log(totalDocs / docsWithTerm);
566
+ }
567
+ function calculateTFIDF(entry, allEntries) {
568
+ const tokens = tokenize(entry.content);
569
+ if (tokens.length === 0) return 0;
570
+ const uniqueTerms = [...new Set(tokens)];
571
+ let totalScore = 0;
572
+ for (const term of uniqueTerms) {
573
+ const tf = calculateTF(term, tokens);
574
+ const idf = calculateIDF(term, allEntries);
575
+ totalScore += tf * idf;
576
+ }
577
+ return totalScore / tokens.length;
578
+ }
579
+ function getStateMultiplier(entry) {
580
+ if (entry.isBTSP) return 2;
581
+ switch (entry.state) {
582
+ case "active":
583
+ return 2;
584
+ case "ready":
585
+ return 1;
586
+ case "silent":
587
+ return 0.5;
588
+ default:
589
+ return 1;
590
+ }
591
+ }
592
+ function priorityScore(entry, allEntries) {
593
+ const tfidf = calculateTFIDF(entry, allEntries);
594
+ const currentScore = engramScorer.calculateScore(entry);
595
+ const engramDecay = 1 - currentScore;
596
+ const stateMultiplier = getStateMultiplier(entry);
597
+ return tfidf * (1 - engramDecay) * stateMultiplier;
598
+ }
599
+ function pruneToFit(entries, budget = tokenBudget) {
600
+ if (entries.length === 0) {
601
+ return {
602
+ kept: [],
603
+ removed: [],
604
+ originalTokens: 0,
605
+ prunedTokens: 0,
606
+ budgetUtilization: 0
607
+ };
608
+ }
609
+ const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
610
+ const btspEntries = entries.filter((e) => e.isBTSP);
611
+ const regularEntries = entries.filter((e) => !e.isBTSP);
612
+ const btspTokens = btspEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
613
+ const scored = regularEntries.map((entry) => ({
614
+ entry,
615
+ score: priorityScore(entry, entries),
616
+ tokens: estimateTokens(entry.content)
617
+ }));
618
+ scored.sort((a, b) => b.score - a.score);
619
+ const kept = [...btspEntries];
620
+ const removed = [];
621
+ let currentTokens = btspTokens;
622
+ for (const item of scored) {
623
+ if (currentTokens + item.tokens <= budget) {
624
+ kept.push(item.entry);
625
+ currentTokens += item.tokens;
626
+ } else {
627
+ removed.push(item.entry);
628
+ }
629
+ }
630
+ const budgetUtilization = budget > 0 ? currentTokens / budget : 0;
631
+ return {
632
+ kept,
633
+ removed,
634
+ originalTokens,
635
+ prunedTokens: currentTokens,
636
+ budgetUtilization
637
+ };
638
+ }
639
+ return {
640
+ pruneToFit,
641
+ priorityScore
642
+ };
643
+ }
644
+ function createBudgetPrunerFromConfig(realtimeConfig, decayConfig, statesConfig) {
645
+ return createBudgetPruner({
646
+ tokenBudget: realtimeConfig.tokenBudget,
647
+ decay: decayConfig,
648
+ states: statesConfig
649
+ });
650
+ }
651
+
652
+ // src/core/metrics.ts
653
+ function createMetricsCollector() {
654
+ const optimizations = [];
655
+ let daemonMetrics = {
656
+ startTime: Date.now(),
657
+ sessionsWatched: 0,
658
+ totalOptimizations: 0,
659
+ totalTokensSaved: 0,
660
+ averageLatency: 0,
661
+ memoryUsage: 0
662
+ };
663
+ let cacheHits = 0;
664
+ let cacheMisses = 0;
665
+ function recordOptimization(metric) {
666
+ optimizations.push(metric);
667
+ daemonMetrics.totalOptimizations++;
668
+ daemonMetrics.totalTokensSaved += metric.tokensBefore - metric.tokensAfter;
669
+ if (metric.cacheHitRate > 0) {
670
+ const hits = Math.round(metric.entriesProcessed * metric.cacheHitRate);
671
+ cacheHits += hits;
672
+ cacheMisses += metric.entriesProcessed - hits;
673
+ }
674
+ daemonMetrics.averageLatency = (daemonMetrics.averageLatency * (daemonMetrics.totalOptimizations - 1) + metric.duration) / daemonMetrics.totalOptimizations;
675
+ if (optimizations.length > 1e3) {
676
+ optimizations.shift();
677
+ }
678
+ }
679
+ function updateDaemon(metric) {
680
+ daemonMetrics = {
681
+ ...daemonMetrics,
682
+ ...metric
683
+ };
684
+ }
685
+ function calculatePercentile(values, percentile) {
686
+ if (values.length === 0) return 0;
687
+ const sorted = [...values].sort((a, b) => a - b);
688
+ const index = Math.ceil(percentile / 100 * sorted.length) - 1;
689
+ return sorted[index] || 0;
690
+ }
691
+ function getSnapshot() {
692
+ const totalRuns = optimizations.length;
693
+ const totalDuration = optimizations.reduce((sum, m) => sum + m.duration, 0);
694
+ const totalTokensSaved = optimizations.reduce(
695
+ (sum, m) => sum + (m.tokensBefore - m.tokensAfter),
696
+ 0
697
+ );
698
+ const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
699
+ const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
700
+ const durations = optimizations.map((m) => m.duration);
701
+ const totalCacheQueries = cacheHits + cacheMisses;
702
+ const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
703
+ return {
704
+ timestamp: Date.now(),
705
+ optimization: {
706
+ totalRuns,
707
+ totalDuration,
708
+ totalTokensSaved,
709
+ averageReduction,
710
+ p50Latency: calculatePercentile(durations, 50),
711
+ p95Latency: calculatePercentile(durations, 95),
712
+ p99Latency: calculatePercentile(durations, 99)
713
+ },
714
+ cache: {
715
+ hitRate,
716
+ totalHits: cacheHits,
717
+ totalMisses: cacheMisses,
718
+ size: optimizations.reduce((sum, m) => sum + m.entriesKept, 0)
719
+ },
720
+ daemon: {
721
+ uptime: Date.now() - daemonMetrics.startTime,
722
+ sessionsWatched: daemonMetrics.sessionsWatched,
723
+ memoryUsage: daemonMetrics.memoryUsage
724
+ }
725
+ };
726
+ }
727
+ function exportMetrics() {
728
+ return JSON.stringify(getSnapshot(), null, 2);
729
+ }
730
+ function reset() {
731
+ optimizations.length = 0;
732
+ cacheHits = 0;
733
+ cacheMisses = 0;
734
+ daemonMetrics = {
735
+ startTime: Date.now(),
736
+ sessionsWatched: 0,
737
+ totalOptimizations: 0,
738
+ totalTokensSaved: 0,
739
+ averageLatency: 0,
740
+ memoryUsage: 0
741
+ };
742
+ }
743
+ return {
744
+ recordOptimization,
745
+ updateDaemon,
746
+ getSnapshot,
747
+ export: exportMetrics,
748
+ reset
749
+ };
750
+ }
751
+ var globalMetrics = null;
752
+ function getMetrics() {
753
+ if (!globalMetrics) {
754
+ globalMetrics = createMetricsCollector();
755
+ }
756
+ return globalMetrics;
757
+ }
758
+
759
+ // src/core/incremental-optimizer.ts
760
+ function createIncrementalOptimizer(config) {
761
+ const pruner = createBudgetPruner(config);
762
+ const { fullOptimizationInterval } = config;
763
+ let state = {
764
+ entryCache: /* @__PURE__ */ new Map(),
765
+ documentFrequency: /* @__PURE__ */ new Map(),
766
+ totalDocuments: 0,
767
+ updateCount: 0,
768
+ lastFullOptimization: Date.now()
769
+ };
770
+ function tokenize(text) {
771
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
772
+ }
773
+ function updateDocumentFrequency(entries, remove = false) {
774
+ for (const entry of entries) {
775
+ const tokens = tokenize(entry.content);
776
+ const uniqueTerms = [...new Set(tokens)];
777
+ for (const term of uniqueTerms) {
778
+ const current = state.documentFrequency.get(term) || 0;
779
+ const updated = remove ? Math.max(0, current - 1) : current + 1;
780
+ if (updated === 0) {
781
+ state.documentFrequency.delete(term);
782
+ } else {
783
+ state.documentFrequency.set(term, updated);
784
+ }
785
+ }
786
+ }
787
+ state.totalDocuments += remove ? -entries.length : entries.length;
788
+ state.totalDocuments = Math.max(0, state.totalDocuments);
789
+ }
790
+ function getCachedEntry(hash) {
791
+ const cached = state.entryCache.get(hash);
792
+ if (!cached) return null;
793
+ return cached.entry;
794
+ }
795
+ function cacheEntry(entry, score) {
796
+ state.entryCache.set(entry.hash, {
797
+ entry,
798
+ score,
799
+ timestamp: Date.now()
800
+ });
801
+ }
802
+ function optimizeIncremental(newEntries, budget) {
803
+ const startTime = Date.now();
804
+ state.updateCount++;
805
+ if (state.updateCount >= fullOptimizationInterval) {
806
+ const allEntries2 = Array.from(state.entryCache.values()).map((c) => c.entry);
807
+ return optimizeFull([...allEntries2, ...newEntries], budget);
808
+ }
809
+ const uncachedEntries = [];
810
+ const cachedEntries = [];
811
+ for (const entry of newEntries) {
812
+ const cached = getCachedEntry(entry.hash);
813
+ if (cached) {
814
+ cachedEntries.push(cached);
815
+ } else {
816
+ uncachedEntries.push(entry);
817
+ }
818
+ }
819
+ if (uncachedEntries.length > 0) {
820
+ updateDocumentFrequency(uncachedEntries, false);
821
+ }
822
+ const allEntries = [...cachedEntries, ...uncachedEntries];
823
+ for (const entry of uncachedEntries) {
824
+ const score = pruner.priorityScore(entry, allEntries);
825
+ cacheEntry(entry, score);
826
+ }
827
+ const currentEntries = Array.from(state.entryCache.values()).map((c) => c.entry);
828
+ const tokensBefore = currentEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
829
+ const result = pruner.pruneToFit(currentEntries, budget);
830
+ const tokensAfter = result.kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
831
+ for (const removed of result.removed) {
832
+ state.entryCache.delete(removed.hash);
833
+ }
834
+ if (result.removed.length > 0) {
835
+ updateDocumentFrequency(result.removed, true);
836
+ }
837
+ const duration = Date.now() - startTime;
838
+ const cacheHitRate = newEntries.length > 0 ? cachedEntries.length / newEntries.length : 0;
839
+ getMetrics().recordOptimization({
840
+ timestamp: Date.now(),
841
+ duration,
842
+ tokensBefore,
843
+ tokensAfter,
844
+ entriesProcessed: newEntries.length,
845
+ entriesKept: result.kept.length,
846
+ cacheHitRate,
847
+ memoryUsage: process.memoryUsage().heapUsed
848
+ });
849
+ return result;
850
+ }
851
+ function optimizeFull(allEntries, budget) {
852
+ const startTime = Date.now();
853
+ const tokensBefore = allEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
854
+ state.entryCache.clear();
855
+ state.documentFrequency.clear();
856
+ state.totalDocuments = 0;
857
+ state.updateCount = 0;
858
+ state.lastFullOptimization = Date.now();
859
+ updateDocumentFrequency(allEntries, false);
860
+ for (const entry of allEntries) {
861
+ const score = pruner.priorityScore(entry, allEntries);
862
+ cacheEntry(entry, score);
863
+ }
864
+ const result = pruner.pruneToFit(allEntries, budget);
865
+ const tokensAfter = result.kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
866
+ for (const removed of result.removed) {
867
+ state.entryCache.delete(removed.hash);
868
+ }
869
+ if (result.removed.length > 0) {
870
+ updateDocumentFrequency(result.removed, true);
871
+ }
872
+ const duration = Date.now() - startTime;
873
+ getMetrics().recordOptimization({
874
+ timestamp: Date.now(),
875
+ duration,
876
+ tokensBefore,
877
+ tokensAfter,
878
+ entriesProcessed: allEntries.length,
879
+ entriesKept: result.kept.length,
880
+ cacheHitRate: 0,
881
+ // Full optimization has no cache hits
882
+ memoryUsage: process.memoryUsage().heapUsed
883
+ });
884
+ return result;
885
+ }
886
+ function getState() {
887
+ return {
888
+ entryCache: new Map(state.entryCache),
889
+ documentFrequency: new Map(state.documentFrequency),
890
+ totalDocuments: state.totalDocuments,
891
+ updateCount: state.updateCount,
892
+ lastFullOptimization: state.lastFullOptimization
893
+ };
894
+ }
895
+ function restoreState(restoredState) {
896
+ state = {
897
+ entryCache: new Map(restoredState.entryCache),
898
+ documentFrequency: new Map(restoredState.documentFrequency),
899
+ totalDocuments: restoredState.totalDocuments,
900
+ updateCount: restoredState.updateCount,
901
+ lastFullOptimization: restoredState.lastFullOptimization
902
+ };
903
+ }
904
+ function reset() {
905
+ state = {
906
+ entryCache: /* @__PURE__ */ new Map(),
907
+ documentFrequency: /* @__PURE__ */ new Map(),
908
+ totalDocuments: 0,
909
+ updateCount: 0,
910
+ lastFullOptimization: Date.now()
911
+ };
912
+ }
913
+ function getStats() {
914
+ return {
915
+ cachedEntries: state.entryCache.size,
916
+ uniqueTerms: state.documentFrequency.size,
917
+ totalDocuments: state.totalDocuments,
918
+ updateCount: state.updateCount,
919
+ lastFullOptimization: state.lastFullOptimization
920
+ };
921
+ }
922
+ return {
923
+ optimizeIncremental,
924
+ optimizeFull,
925
+ getState,
926
+ restoreState,
927
+ reset,
928
+ getStats
929
+ };
930
+ }
931
+
932
+ // src/core/context-pipeline.ts
933
+ function createContextPipeline(config) {
934
+ const optimizer = createIncrementalOptimizer(config);
935
+ const { windowSize, tokenBudget } = config;
936
+ let totalIngested = 0;
937
+ let evictedEntries = 0;
938
+ let currentEntries = [];
939
+ let budgetUtilization = 0;
940
+ function ingest(content, metadata = {}) {
941
+ const newEntries = parseClaudeCodeContext(content);
942
+ if (newEntries.length === 0) return 0;
943
+ const entriesWithMetadata = newEntries.map((entry) => ({
944
+ ...entry,
945
+ metadata: { ...entry.metadata, ...metadata }
946
+ }));
947
+ const result = optimizer.optimizeIncremental(entriesWithMetadata, tokenBudget);
948
+ totalIngested += newEntries.length;
949
+ evictedEntries += result.removed.length;
950
+ currentEntries = result.kept;
951
+ budgetUtilization = result.budgetUtilization;
952
+ if (currentEntries.length > windowSize) {
953
+ const sorted = [...currentEntries].sort((a, b) => b.timestamp - a.timestamp);
954
+ const toKeep = sorted.slice(0, windowSize);
955
+ const toRemove = sorted.slice(windowSize);
956
+ currentEntries = toKeep;
957
+ evictedEntries += toRemove.length;
958
+ }
959
+ return newEntries.length;
960
+ }
961
+ function getContext() {
962
+ const sorted = [...currentEntries].sort((a, b) => a.timestamp - b.timestamp);
963
+ return sorted.map((e) => e.content).join("\n\n");
964
+ }
965
+ function getEntries() {
966
+ return [...currentEntries].sort((a, b) => a.timestamp - b.timestamp);
967
+ }
968
+ function getStats() {
969
+ const optimizerStats = optimizer.getStats();
970
+ const currentTokens = currentEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
971
+ return {
972
+ totalIngested,
973
+ currentEntries: currentEntries.length,
974
+ currentTokens,
975
+ budgetUtilization,
976
+ evictedEntries,
977
+ optimizer: {
978
+ cachedEntries: optimizerStats.cachedEntries,
979
+ uniqueTerms: optimizerStats.uniqueTerms,
980
+ updateCount: optimizerStats.updateCount
981
+ }
982
+ };
983
+ }
984
+ function clear() {
985
+ totalIngested = 0;
986
+ evictedEntries = 0;
987
+ currentEntries = [];
988
+ budgetUtilization = 0;
989
+ optimizer.reset();
990
+ }
991
+ return {
992
+ ingest,
993
+ getContext,
994
+ getEntries,
995
+ getStats,
996
+ clear
997
+ };
998
+ }
999
+
522
1000
  // src/core/kv-memory.ts
523
1001
  var import_node_fs = require("fs");
524
1002
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
@@ -888,6 +1366,408 @@ function createSleepCompressor() {
888
1366
  };
889
1367
  }
890
1368
 
1369
+ // src/daemon/daemon-process.ts
1370
+ var import_node_child_process = require("child_process");
1371
+ var import_node_fs2 = require("fs");
1372
+ var import_node_path = require("path");
1373
+ var import_node_url = require("url");
1374
+ function createDaemonCommand() {
1375
+ function isDaemonRunning(pidFile) {
1376
+ if (!(0, import_node_fs2.existsSync)(pidFile)) {
1377
+ return { running: false };
1378
+ }
1379
+ try {
1380
+ const pidStr = (0, import_node_fs2.readFileSync)(pidFile, "utf-8").trim();
1381
+ const pid = Number.parseInt(pidStr, 10);
1382
+ if (Number.isNaN(pid)) {
1383
+ return { running: false };
1384
+ }
1385
+ try {
1386
+ process.kill(pid, 0);
1387
+ return { running: true, pid };
1388
+ } catch {
1389
+ (0, import_node_fs2.unlinkSync)(pidFile);
1390
+ return { running: false };
1391
+ }
1392
+ } catch {
1393
+ return { running: false };
1394
+ }
1395
+ }
1396
+ function writePidFile(pidFile, pid) {
1397
+ const dir = (0, import_node_path.dirname)(pidFile);
1398
+ if (!(0, import_node_fs2.existsSync)(dir)) {
1399
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
1400
+ }
1401
+ (0, import_node_fs2.writeFileSync)(pidFile, String(pid), "utf-8");
1402
+ }
1403
+ function removePidFile(pidFile) {
1404
+ if ((0, import_node_fs2.existsSync)(pidFile)) {
1405
+ (0, import_node_fs2.unlinkSync)(pidFile);
1406
+ }
1407
+ }
1408
+ async function start(config) {
1409
+ const { pidFile, logFile } = config.realtime;
1410
+ const status2 = isDaemonRunning(pidFile);
1411
+ if (status2.running) {
1412
+ return {
1413
+ success: false,
1414
+ pid: status2.pid,
1415
+ message: `Daemon already running (PID ${status2.pid})`,
1416
+ error: "Already running"
1417
+ };
1418
+ }
1419
+ try {
1420
+ const __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
1421
+ const __dirname = (0, import_node_path.dirname)(__filename2);
1422
+ const daemonPath = (0, import_node_path.join)(__dirname, "index.js");
1423
+ const child = (0, import_node_child_process.fork)(daemonPath, [], {
1424
+ detached: true,
1425
+ stdio: "ignore",
1426
+ env: {
1427
+ ...process.env,
1428
+ SPARN_CONFIG: JSON.stringify(config),
1429
+ SPARN_PID_FILE: pidFile,
1430
+ SPARN_LOG_FILE: logFile
1431
+ }
1432
+ });
1433
+ child.unref();
1434
+ if (child.pid) {
1435
+ writePidFile(pidFile, child.pid);
1436
+ return {
1437
+ success: true,
1438
+ pid: child.pid,
1439
+ message: `Daemon started (PID ${child.pid})`
1440
+ };
1441
+ }
1442
+ return {
1443
+ success: false,
1444
+ message: "Failed to start daemon (no PID)",
1445
+ error: "No PID"
1446
+ };
1447
+ } catch (error) {
1448
+ return {
1449
+ success: false,
1450
+ message: "Failed to start daemon",
1451
+ error: error instanceof Error ? error.message : String(error)
1452
+ };
1453
+ }
1454
+ }
1455
+ async function stop(config) {
1456
+ const { pidFile } = config.realtime;
1457
+ const status2 = isDaemonRunning(pidFile);
1458
+ if (!status2.running || !status2.pid) {
1459
+ return {
1460
+ success: true,
1461
+ message: "Daemon not running"
1462
+ };
1463
+ }
1464
+ try {
1465
+ process.kill(status2.pid, "SIGTERM");
1466
+ const maxWait = 5e3;
1467
+ const interval = 100;
1468
+ let waited = 0;
1469
+ while (waited < maxWait) {
1470
+ try {
1471
+ process.kill(status2.pid, 0);
1472
+ await new Promise((resolve) => setTimeout(resolve, interval));
1473
+ waited += interval;
1474
+ } catch {
1475
+ removePidFile(pidFile);
1476
+ return {
1477
+ success: true,
1478
+ message: `Daemon stopped (PID ${status2.pid})`
1479
+ };
1480
+ }
1481
+ }
1482
+ try {
1483
+ process.kill(status2.pid, "SIGKILL");
1484
+ removePidFile(pidFile);
1485
+ return {
1486
+ success: true,
1487
+ message: `Daemon force killed (PID ${status2.pid})`
1488
+ };
1489
+ } catch {
1490
+ removePidFile(pidFile);
1491
+ return {
1492
+ success: true,
1493
+ message: `Daemon stopped (PID ${status2.pid})`
1494
+ };
1495
+ }
1496
+ } catch (error) {
1497
+ return {
1498
+ success: false,
1499
+ message: "Failed to stop daemon",
1500
+ error: error instanceof Error ? error.message : String(error)
1501
+ };
1502
+ }
1503
+ }
1504
+ async function status(config) {
1505
+ const { pidFile } = config.realtime;
1506
+ const daemonStatus = isDaemonRunning(pidFile);
1507
+ if (!daemonStatus.running || !daemonStatus.pid) {
1508
+ return {
1509
+ running: false,
1510
+ message: "Daemon not running"
1511
+ };
1512
+ }
1513
+ const metrics = getMetrics().getSnapshot();
1514
+ return {
1515
+ running: true,
1516
+ pid: daemonStatus.pid,
1517
+ uptime: metrics.daemon.uptime,
1518
+ sessionsWatched: metrics.daemon.sessionsWatched,
1519
+ tokensSaved: metrics.optimization.totalTokensSaved,
1520
+ message: `Daemon running (PID ${daemonStatus.pid})`
1521
+ };
1522
+ }
1523
+ return {
1524
+ start,
1525
+ stop,
1526
+ status
1527
+ };
1528
+ }
1529
+
1530
+ // src/daemon/file-tracker.ts
1531
+ var import_node_fs3 = require("fs");
1532
+ function createFileTracker() {
1533
+ const positions = /* @__PURE__ */ new Map();
1534
+ function readNewLines(filePath) {
1535
+ try {
1536
+ const stats = (0, import_node_fs3.statSync)(filePath);
1537
+ const currentSize = stats.size;
1538
+ const currentModified = stats.mtimeMs;
1539
+ let pos = positions.get(filePath);
1540
+ if (!pos) {
1541
+ pos = {
1542
+ path: filePath,
1543
+ position: 0,
1544
+ partialLine: "",
1545
+ lastModified: currentModified,
1546
+ lastSize: 0
1547
+ };
1548
+ positions.set(filePath, pos);
1549
+ }
1550
+ if (currentSize < pos.lastSize || currentSize === pos.position) {
1551
+ if (currentSize < pos.lastSize) {
1552
+ pos.position = 0;
1553
+ pos.partialLine = "";
1554
+ }
1555
+ return [];
1556
+ }
1557
+ const buffer = Buffer.alloc(currentSize - pos.position);
1558
+ const fd = (0, import_node_fs3.readFileSync)(filePath);
1559
+ fd.copy(buffer, 0, pos.position, currentSize);
1560
+ const newContent = (pos.partialLine + buffer.toString("utf-8")).split("\n");
1561
+ const partialLine = newContent.pop() || "";
1562
+ pos.position = currentSize;
1563
+ pos.partialLine = partialLine;
1564
+ pos.lastModified = currentModified;
1565
+ pos.lastSize = currentSize;
1566
+ return newContent.filter((line) => line.trim().length > 0);
1567
+ } catch (_error) {
1568
+ return [];
1569
+ }
1570
+ }
1571
+ function getPosition(filePath) {
1572
+ return positions.get(filePath) || null;
1573
+ }
1574
+ function resetPosition(filePath) {
1575
+ positions.delete(filePath);
1576
+ }
1577
+ function clearAll() {
1578
+ positions.clear();
1579
+ }
1580
+ function getTrackedFiles() {
1581
+ return Array.from(positions.keys());
1582
+ }
1583
+ return {
1584
+ readNewLines,
1585
+ getPosition,
1586
+ resetPosition,
1587
+ clearAll,
1588
+ getTrackedFiles
1589
+ };
1590
+ }
1591
+
1592
+ // src/daemon/session-watcher.ts
1593
+ var import_node_fs4 = require("fs");
1594
+ var import_node_os = require("os");
1595
+ var import_node_path2 = require("path");
1596
+ function createSessionWatcher(config) {
1597
+ const { config: sparnConfig, onOptimize, onError } = config;
1598
+ const { realtime, decay, states } = sparnConfig;
1599
+ const pipelines = /* @__PURE__ */ new Map();
1600
+ const fileTracker = createFileTracker();
1601
+ const watchers = [];
1602
+ const debounceTimers = /* @__PURE__ */ new Map();
1603
+ function getProjectsDir() {
1604
+ return (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "projects");
1605
+ }
1606
+ function getSessionId(filePath) {
1607
+ const filename = filePath.split(/[/\\]/).pop() || "";
1608
+ return filename.replace(/\.jsonl$/, "");
1609
+ }
1610
+ function getPipeline(sessionId) {
1611
+ let pipeline = pipelines.get(sessionId);
1612
+ if (!pipeline) {
1613
+ pipeline = createContextPipeline({
1614
+ tokenBudget: realtime.tokenBudget,
1615
+ decay,
1616
+ states,
1617
+ windowSize: realtime.windowSize,
1618
+ fullOptimizationInterval: 50
1619
+ // Full re-optimization every 50 incremental updates
1620
+ });
1621
+ pipelines.set(sessionId, pipeline);
1622
+ }
1623
+ return pipeline;
1624
+ }
1625
+ function handleFileChange(filePath) {
1626
+ const existingTimer = debounceTimers.get(filePath);
1627
+ if (existingTimer) {
1628
+ clearTimeout(existingTimer);
1629
+ }
1630
+ const timer = setTimeout(() => {
1631
+ try {
1632
+ const newLines = fileTracker.readNewLines(filePath);
1633
+ if (newLines.length === 0) return;
1634
+ const content = newLines.join("\n");
1635
+ const sessionId = getSessionId(filePath);
1636
+ const pipeline = getPipeline(sessionId);
1637
+ pipeline.ingest(content, { sessionId, filePath });
1638
+ const stats = pipeline.getStats();
1639
+ if (stats.currentTokens >= realtime.autoOptimizeThreshold) {
1640
+ getMetrics().updateDaemon({
1641
+ sessionsWatched: pipelines.size,
1642
+ memoryUsage: process.memoryUsage().heapUsed
1643
+ });
1644
+ if (onOptimize) {
1645
+ const sessionStats = computeSessionStats(sessionId, pipeline);
1646
+ onOptimize(sessionId, sessionStats);
1647
+ }
1648
+ }
1649
+ } catch (error) {
1650
+ if (onError) {
1651
+ onError(error instanceof Error ? error : new Error(String(error)));
1652
+ }
1653
+ } finally {
1654
+ debounceTimers.delete(filePath);
1655
+ }
1656
+ }, realtime.debounceMs);
1657
+ debounceTimers.set(filePath, timer);
1658
+ }
1659
+ function findJsonlFiles(dir) {
1660
+ const files = [];
1661
+ try {
1662
+ const entries = (0, import_node_fs4.readdirSync)(dir);
1663
+ for (const entry of entries) {
1664
+ const fullPath = (0, import_node_path2.join)(dir, entry);
1665
+ const stat = (0, import_node_fs4.statSync)(fullPath);
1666
+ if (stat.isDirectory()) {
1667
+ files.push(...findJsonlFiles(fullPath));
1668
+ } else if (entry.endsWith(".jsonl")) {
1669
+ const matches = realtime.watchPatterns.some((pattern) => {
1670
+ const regex = new RegExp(
1671
+ pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*").replace(/\./g, "\\.")
1672
+ );
1673
+ return regex.test(fullPath);
1674
+ });
1675
+ if (matches) {
1676
+ files.push(fullPath);
1677
+ }
1678
+ }
1679
+ }
1680
+ } catch (_error) {
1681
+ }
1682
+ return files;
1683
+ }
1684
+ function computeSessionStats(sessionId, pipeline) {
1685
+ const stats = pipeline.getStats();
1686
+ const entries = pipeline.getEntries();
1687
+ const totalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
1688
+ return {
1689
+ sessionId,
1690
+ totalTokens: stats.totalIngested,
1691
+ optimizedTokens: stats.currentTokens,
1692
+ reduction: totalTokens > 0 ? (totalTokens - stats.currentTokens) / totalTokens : 0,
1693
+ entryCount: stats.currentEntries,
1694
+ budgetUtilization: stats.budgetUtilization
1695
+ };
1696
+ }
1697
+ async function start() {
1698
+ const projectsDir = getProjectsDir();
1699
+ const jsonlFiles = findJsonlFiles(projectsDir);
1700
+ const watchedDirs = /* @__PURE__ */ new Set();
1701
+ for (const file of jsonlFiles) {
1702
+ const dir = (0, import_node_path2.dirname)(file);
1703
+ if (!watchedDirs.has(dir)) {
1704
+ const watcher = (0, import_node_fs4.watch)(dir, { recursive: false }, (_eventType, filename) => {
1705
+ if (filename?.endsWith(".jsonl")) {
1706
+ const fullPath = (0, import_node_path2.join)(dir, filename);
1707
+ handleFileChange(fullPath);
1708
+ }
1709
+ });
1710
+ watchers.push(watcher);
1711
+ watchedDirs.add(dir);
1712
+ }
1713
+ }
1714
+ const projectsWatcher = (0, import_node_fs4.watch)(projectsDir, { recursive: true }, (_eventType, filename) => {
1715
+ if (filename?.endsWith(".jsonl")) {
1716
+ const fullPath = (0, import_node_path2.join)(projectsDir, filename);
1717
+ handleFileChange(fullPath);
1718
+ }
1719
+ });
1720
+ watchers.push(projectsWatcher);
1721
+ getMetrics().updateDaemon({
1722
+ startTime: Date.now(),
1723
+ sessionsWatched: jsonlFiles.length,
1724
+ memoryUsage: process.memoryUsage().heapUsed
1725
+ });
1726
+ }
1727
+ function stop() {
1728
+ for (const watcher of watchers) {
1729
+ watcher.close();
1730
+ }
1731
+ watchers.length = 0;
1732
+ for (const timer of debounceTimers.values()) {
1733
+ clearTimeout(timer);
1734
+ }
1735
+ debounceTimers.clear();
1736
+ pipelines.clear();
1737
+ fileTracker.clearAll();
1738
+ }
1739
+ function getStats() {
1740
+ const stats = [];
1741
+ for (const [sessionId, pipeline] of pipelines.entries()) {
1742
+ stats.push(computeSessionStats(sessionId, pipeline));
1743
+ }
1744
+ return stats;
1745
+ }
1746
+ function getSessionStats(sessionId) {
1747
+ const pipeline = pipelines.get(sessionId);
1748
+ if (!pipeline) return null;
1749
+ return computeSessionStats(sessionId, pipeline);
1750
+ }
1751
+ function optimizeSession(sessionId) {
1752
+ const pipeline = pipelines.get(sessionId);
1753
+ if (!pipeline) return;
1754
+ const entries = pipeline.getEntries();
1755
+ pipeline.clear();
1756
+ pipeline.ingest(entries.map((e) => e.content).join("\n\n"));
1757
+ if (onOptimize) {
1758
+ const stats = computeSessionStats(sessionId, pipeline);
1759
+ onOptimize(sessionId, stats);
1760
+ }
1761
+ }
1762
+ return {
1763
+ start,
1764
+ stop,
1765
+ getStats,
1766
+ getSessionStats,
1767
+ optimizeSession
1768
+ };
1769
+ }
1770
+
891
1771
  // src/types/config.ts
892
1772
  var DEFAULT_CONFIG = {
893
1773
  pruning: {
@@ -908,7 +1788,17 @@ var DEFAULT_CONFIG = {
908
1788
  sounds: false,
909
1789
  verbose: false
910
1790
  },
911
- autoConsolidate: null
1791
+ autoConsolidate: null,
1792
+ realtime: {
1793
+ tokenBudget: 5e4,
1794
+ autoOptimizeThreshold: 8e4,
1795
+ watchPatterns: ["**/*.jsonl"],
1796
+ pidFile: ".sparn/daemon.pid",
1797
+ logFile: ".sparn/daemon.log",
1798
+ debounceMs: 5e3,
1799
+ incremental: true,
1800
+ windowSize: 500
1801
+ }
912
1802
  };
913
1803
 
914
1804
  // src/utils/logger.ts
@@ -934,15 +1824,25 @@ function createLogger(verbose = false) {
934
1824
  0 && (module.exports = {
935
1825
  DEFAULT_CONFIG,
936
1826
  createBTSPEmbedder,
1827
+ createBudgetPruner,
1828
+ createBudgetPrunerFromConfig,
937
1829
  createClaudeCodeAdapter,
938
1830
  createConfidenceStates,
1831
+ createContextPipeline,
1832
+ createDaemonCommand,
939
1833
  createEngramScorer,
1834
+ createEntry,
1835
+ createFileTracker,
940
1836
  createGenericAdapter,
1837
+ createIncrementalOptimizer,
941
1838
  createKVMemory,
942
1839
  createLogger,
1840
+ createSessionWatcher,
943
1841
  createSleepCompressor,
944
1842
  createSparsePruner,
945
1843
  estimateTokens,
946
- hashContent
1844
+ hashContent,
1845
+ parseClaudeCodeContext,
1846
+ parseGenericContext
947
1847
  });
948
1848
  //# sourceMappingURL=index.cjs.map