@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.js CHANGED
@@ -1,6 +1,3 @@
1
- // src/adapters/claude-code.ts
2
- import { randomUUID as randomUUID2 } from "crypto";
3
-
4
1
  // src/core/btsp-embedder.ts
5
2
  import { randomUUID } from "crypto";
6
3
 
@@ -214,6 +211,82 @@ function createSparsePruner(config) {
214
211
  };
215
212
  }
216
213
 
214
+ // src/utils/context-parser.ts
215
+ import { randomUUID as randomUUID2 } from "crypto";
216
+ function parseClaudeCodeContext(context) {
217
+ const entries = [];
218
+ const now = Date.now();
219
+ const lines = context.split("\n");
220
+ let currentBlock = [];
221
+ let blockType = "other";
222
+ for (const line of lines) {
223
+ const trimmed = line.trim();
224
+ if (trimmed.startsWith("User:") || trimmed.startsWith("Assistant:")) {
225
+ if (currentBlock.length > 0) {
226
+ entries.push(createEntry(currentBlock.join("\n"), blockType, now));
227
+ currentBlock = [];
228
+ }
229
+ blockType = "conversation";
230
+ currentBlock.push(line);
231
+ } else if (trimmed.includes("<function_calls>") || trimmed.includes("<invoke>") || trimmed.includes("<tool_use>")) {
232
+ if (currentBlock.length > 0) {
233
+ entries.push(createEntry(currentBlock.join("\n"), blockType, now));
234
+ currentBlock = [];
235
+ }
236
+ blockType = "tool";
237
+ currentBlock.push(line);
238
+ } else if (trimmed.includes("<function_results>") || trimmed.includes("</function_results>")) {
239
+ if (currentBlock.length > 0 && blockType !== "result") {
240
+ entries.push(createEntry(currentBlock.join("\n"), blockType, now));
241
+ currentBlock = [];
242
+ }
243
+ blockType = "result";
244
+ currentBlock.push(line);
245
+ } else if (currentBlock.length > 0) {
246
+ currentBlock.push(line);
247
+ } else if (trimmed.length > 0) {
248
+ currentBlock.push(line);
249
+ blockType = "other";
250
+ }
251
+ }
252
+ if (currentBlock.length > 0) {
253
+ entries.push(createEntry(currentBlock.join("\n"), blockType, now));
254
+ }
255
+ return entries.filter((e) => e.content.trim().length > 0);
256
+ }
257
+ function createEntry(content, type, baseTime) {
258
+ const tags = [type];
259
+ let initialScore = 0.5;
260
+ if (type === "conversation") initialScore = 0.8;
261
+ if (type === "tool") initialScore = 0.7;
262
+ if (type === "result") initialScore = 0.4;
263
+ return {
264
+ id: randomUUID2(),
265
+ content,
266
+ hash: hashContent(content),
267
+ timestamp: baseTime,
268
+ score: initialScore,
269
+ state: initialScore > 0.7 ? "active" : initialScore > 0.3 ? "ready" : "silent",
270
+ ttl: 24 * 3600,
271
+ // 24 hours default
272
+ accessCount: 0,
273
+ tags,
274
+ metadata: { type },
275
+ isBTSP: false
276
+ };
277
+ }
278
+ function parseGenericContext(context) {
279
+ const entries = [];
280
+ const now = Date.now();
281
+ const blocks = context.split(/\n\n+/);
282
+ for (const block of blocks) {
283
+ const trimmed = block.trim();
284
+ if (trimmed.length === 0) continue;
285
+ entries.push(createEntry(trimmed, "other", now));
286
+ }
287
+ return entries;
288
+ }
289
+
217
290
  // src/adapters/claude-code.ts
218
291
  var CLAUDE_CODE_PROFILE = {
219
292
  // More aggressive pruning for tool results (they can be verbose)
@@ -333,68 +406,6 @@ function createClaudeCodeAdapter(memory, config) {
333
406
  optimize
334
407
  };
335
408
  }
336
- function parseClaudeCodeContext(context) {
337
- const entries = [];
338
- const now = Date.now();
339
- const lines = context.split("\n");
340
- let currentBlock = [];
341
- let blockType = "other";
342
- for (const line of lines) {
343
- const trimmed = line.trim();
344
- if (trimmed.startsWith("User:") || trimmed.startsWith("Assistant:")) {
345
- if (currentBlock.length > 0) {
346
- entries.push(createEntry(currentBlock.join("\n"), blockType, now));
347
- currentBlock = [];
348
- }
349
- blockType = "conversation";
350
- currentBlock.push(line);
351
- } else if (trimmed.includes("<function_calls>") || trimmed.includes("<invoke>") || trimmed.includes("<tool_use>")) {
352
- if (currentBlock.length > 0) {
353
- entries.push(createEntry(currentBlock.join("\n"), blockType, now));
354
- currentBlock = [];
355
- }
356
- blockType = "tool";
357
- currentBlock.push(line);
358
- } else if (trimmed.includes("<function_results>") || trimmed.includes("</function_results>")) {
359
- if (currentBlock.length > 0 && blockType !== "result") {
360
- entries.push(createEntry(currentBlock.join("\n"), blockType, now));
361
- currentBlock = [];
362
- }
363
- blockType = "result";
364
- currentBlock.push(line);
365
- } else if (currentBlock.length > 0) {
366
- currentBlock.push(line);
367
- } else if (trimmed.length > 0) {
368
- currentBlock.push(line);
369
- blockType = "other";
370
- }
371
- }
372
- if (currentBlock.length > 0) {
373
- entries.push(createEntry(currentBlock.join("\n"), blockType, now));
374
- }
375
- return entries.filter((e) => e.content.trim().length > 0);
376
- }
377
- function createEntry(content, type, baseTime) {
378
- const tags = [type];
379
- let initialScore = 0.5;
380
- if (type === "conversation") initialScore = 0.8;
381
- if (type === "tool") initialScore = 0.7;
382
- if (type === "result") initialScore = 0.4;
383
- return {
384
- id: randomUUID2(),
385
- content,
386
- hash: hashContent(content),
387
- timestamp: baseTime,
388
- score: initialScore,
389
- state: initialScore > 0.7 ? "active" : initialScore > 0.3 ? "ready" : "silent",
390
- ttl: 24 * 3600,
391
- // 24 hours default
392
- accessCount: 0,
393
- tags,
394
- metadata: { type },
395
- isBTSP: false
396
- };
397
- }
398
409
 
399
410
  // src/adapters/generic.ts
400
411
  import { randomUUID as randomUUID3 } from "crypto";
@@ -472,6 +483,459 @@ function createGenericAdapter(memory, config) {
472
483
  };
473
484
  }
474
485
 
486
+ // src/core/budget-pruner.ts
487
+ function createBudgetPruner(config) {
488
+ const { tokenBudget, decay } = config;
489
+ const engramScorer = createEngramScorer(decay);
490
+ function tokenize(text) {
491
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
492
+ }
493
+ function calculateTF(term, tokens) {
494
+ const count = tokens.filter((t) => t === term).length;
495
+ return Math.sqrt(count);
496
+ }
497
+ function calculateIDF(term, allEntries) {
498
+ const totalDocs = allEntries.length;
499
+ const docsWithTerm = allEntries.filter((entry) => {
500
+ const tokens = tokenize(entry.content);
501
+ return tokens.includes(term);
502
+ }).length;
503
+ if (docsWithTerm === 0) return 0;
504
+ return Math.log(totalDocs / docsWithTerm);
505
+ }
506
+ function calculateTFIDF(entry, allEntries) {
507
+ const tokens = tokenize(entry.content);
508
+ if (tokens.length === 0) return 0;
509
+ const uniqueTerms = [...new Set(tokens)];
510
+ let totalScore = 0;
511
+ for (const term of uniqueTerms) {
512
+ const tf = calculateTF(term, tokens);
513
+ const idf = calculateIDF(term, allEntries);
514
+ totalScore += tf * idf;
515
+ }
516
+ return totalScore / tokens.length;
517
+ }
518
+ function getStateMultiplier(entry) {
519
+ if (entry.isBTSP) return 2;
520
+ switch (entry.state) {
521
+ case "active":
522
+ return 2;
523
+ case "ready":
524
+ return 1;
525
+ case "silent":
526
+ return 0.5;
527
+ default:
528
+ return 1;
529
+ }
530
+ }
531
+ function priorityScore(entry, allEntries) {
532
+ const tfidf = calculateTFIDF(entry, allEntries);
533
+ const currentScore = engramScorer.calculateScore(entry);
534
+ const engramDecay = 1 - currentScore;
535
+ const stateMultiplier = getStateMultiplier(entry);
536
+ return tfidf * (1 - engramDecay) * stateMultiplier;
537
+ }
538
+ function pruneToFit(entries, budget = tokenBudget) {
539
+ if (entries.length === 0) {
540
+ return {
541
+ kept: [],
542
+ removed: [],
543
+ originalTokens: 0,
544
+ prunedTokens: 0,
545
+ budgetUtilization: 0
546
+ };
547
+ }
548
+ const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
549
+ const btspEntries = entries.filter((e) => e.isBTSP);
550
+ const regularEntries = entries.filter((e) => !e.isBTSP);
551
+ const btspTokens = btspEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
552
+ const scored = regularEntries.map((entry) => ({
553
+ entry,
554
+ score: priorityScore(entry, entries),
555
+ tokens: estimateTokens(entry.content)
556
+ }));
557
+ scored.sort((a, b) => b.score - a.score);
558
+ const kept = [...btspEntries];
559
+ const removed = [];
560
+ let currentTokens = btspTokens;
561
+ for (const item of scored) {
562
+ if (currentTokens + item.tokens <= budget) {
563
+ kept.push(item.entry);
564
+ currentTokens += item.tokens;
565
+ } else {
566
+ removed.push(item.entry);
567
+ }
568
+ }
569
+ const budgetUtilization = budget > 0 ? currentTokens / budget : 0;
570
+ return {
571
+ kept,
572
+ removed,
573
+ originalTokens,
574
+ prunedTokens: currentTokens,
575
+ budgetUtilization
576
+ };
577
+ }
578
+ return {
579
+ pruneToFit,
580
+ priorityScore
581
+ };
582
+ }
583
+ function createBudgetPrunerFromConfig(realtimeConfig, decayConfig, statesConfig) {
584
+ return createBudgetPruner({
585
+ tokenBudget: realtimeConfig.tokenBudget,
586
+ decay: decayConfig,
587
+ states: statesConfig
588
+ });
589
+ }
590
+
591
+ // src/core/metrics.ts
592
+ function createMetricsCollector() {
593
+ const optimizations = [];
594
+ let daemonMetrics = {
595
+ startTime: Date.now(),
596
+ sessionsWatched: 0,
597
+ totalOptimizations: 0,
598
+ totalTokensSaved: 0,
599
+ averageLatency: 0,
600
+ memoryUsage: 0
601
+ };
602
+ let cacheHits = 0;
603
+ let cacheMisses = 0;
604
+ function recordOptimization(metric) {
605
+ optimizations.push(metric);
606
+ daemonMetrics.totalOptimizations++;
607
+ daemonMetrics.totalTokensSaved += metric.tokensBefore - metric.tokensAfter;
608
+ if (metric.cacheHitRate > 0) {
609
+ const hits = Math.round(metric.entriesProcessed * metric.cacheHitRate);
610
+ cacheHits += hits;
611
+ cacheMisses += metric.entriesProcessed - hits;
612
+ }
613
+ daemonMetrics.averageLatency = (daemonMetrics.averageLatency * (daemonMetrics.totalOptimizations - 1) + metric.duration) / daemonMetrics.totalOptimizations;
614
+ if (optimizations.length > 1e3) {
615
+ optimizations.shift();
616
+ }
617
+ }
618
+ function updateDaemon(metric) {
619
+ daemonMetrics = {
620
+ ...daemonMetrics,
621
+ ...metric
622
+ };
623
+ }
624
+ function calculatePercentile(values, percentile) {
625
+ if (values.length === 0) return 0;
626
+ const sorted = [...values].sort((a, b) => a - b);
627
+ const index = Math.ceil(percentile / 100 * sorted.length) - 1;
628
+ return sorted[index] || 0;
629
+ }
630
+ function getSnapshot() {
631
+ const totalRuns = optimizations.length;
632
+ const totalDuration = optimizations.reduce((sum, m) => sum + m.duration, 0);
633
+ const totalTokensSaved = optimizations.reduce(
634
+ (sum, m) => sum + (m.tokensBefore - m.tokensAfter),
635
+ 0
636
+ );
637
+ const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
638
+ const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
639
+ const durations = optimizations.map((m) => m.duration);
640
+ const totalCacheQueries = cacheHits + cacheMisses;
641
+ const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
642
+ return {
643
+ timestamp: Date.now(),
644
+ optimization: {
645
+ totalRuns,
646
+ totalDuration,
647
+ totalTokensSaved,
648
+ averageReduction,
649
+ p50Latency: calculatePercentile(durations, 50),
650
+ p95Latency: calculatePercentile(durations, 95),
651
+ p99Latency: calculatePercentile(durations, 99)
652
+ },
653
+ cache: {
654
+ hitRate,
655
+ totalHits: cacheHits,
656
+ totalMisses: cacheMisses,
657
+ size: optimizations.reduce((sum, m) => sum + m.entriesKept, 0)
658
+ },
659
+ daemon: {
660
+ uptime: Date.now() - daemonMetrics.startTime,
661
+ sessionsWatched: daemonMetrics.sessionsWatched,
662
+ memoryUsage: daemonMetrics.memoryUsage
663
+ }
664
+ };
665
+ }
666
+ function exportMetrics() {
667
+ return JSON.stringify(getSnapshot(), null, 2);
668
+ }
669
+ function reset() {
670
+ optimizations.length = 0;
671
+ cacheHits = 0;
672
+ cacheMisses = 0;
673
+ daemonMetrics = {
674
+ startTime: Date.now(),
675
+ sessionsWatched: 0,
676
+ totalOptimizations: 0,
677
+ totalTokensSaved: 0,
678
+ averageLatency: 0,
679
+ memoryUsage: 0
680
+ };
681
+ }
682
+ return {
683
+ recordOptimization,
684
+ updateDaemon,
685
+ getSnapshot,
686
+ export: exportMetrics,
687
+ reset
688
+ };
689
+ }
690
+ var globalMetrics = null;
691
+ function getMetrics() {
692
+ if (!globalMetrics) {
693
+ globalMetrics = createMetricsCollector();
694
+ }
695
+ return globalMetrics;
696
+ }
697
+
698
+ // src/core/incremental-optimizer.ts
699
+ function createIncrementalOptimizer(config) {
700
+ const pruner = createBudgetPruner(config);
701
+ const { fullOptimizationInterval } = config;
702
+ let state = {
703
+ entryCache: /* @__PURE__ */ new Map(),
704
+ documentFrequency: /* @__PURE__ */ new Map(),
705
+ totalDocuments: 0,
706
+ updateCount: 0,
707
+ lastFullOptimization: Date.now()
708
+ };
709
+ function tokenize(text) {
710
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
711
+ }
712
+ function updateDocumentFrequency(entries, remove = false) {
713
+ for (const entry of entries) {
714
+ const tokens = tokenize(entry.content);
715
+ const uniqueTerms = [...new Set(tokens)];
716
+ for (const term of uniqueTerms) {
717
+ const current = state.documentFrequency.get(term) || 0;
718
+ const updated = remove ? Math.max(0, current - 1) : current + 1;
719
+ if (updated === 0) {
720
+ state.documentFrequency.delete(term);
721
+ } else {
722
+ state.documentFrequency.set(term, updated);
723
+ }
724
+ }
725
+ }
726
+ state.totalDocuments += remove ? -entries.length : entries.length;
727
+ state.totalDocuments = Math.max(0, state.totalDocuments);
728
+ }
729
+ function getCachedEntry(hash) {
730
+ const cached = state.entryCache.get(hash);
731
+ if (!cached) return null;
732
+ return cached.entry;
733
+ }
734
+ function cacheEntry(entry, score) {
735
+ state.entryCache.set(entry.hash, {
736
+ entry,
737
+ score,
738
+ timestamp: Date.now()
739
+ });
740
+ }
741
+ function optimizeIncremental(newEntries, budget) {
742
+ const startTime = Date.now();
743
+ state.updateCount++;
744
+ if (state.updateCount >= fullOptimizationInterval) {
745
+ const allEntries2 = Array.from(state.entryCache.values()).map((c) => c.entry);
746
+ return optimizeFull([...allEntries2, ...newEntries], budget);
747
+ }
748
+ const uncachedEntries = [];
749
+ const cachedEntries = [];
750
+ for (const entry of newEntries) {
751
+ const cached = getCachedEntry(entry.hash);
752
+ if (cached) {
753
+ cachedEntries.push(cached);
754
+ } else {
755
+ uncachedEntries.push(entry);
756
+ }
757
+ }
758
+ if (uncachedEntries.length > 0) {
759
+ updateDocumentFrequency(uncachedEntries, false);
760
+ }
761
+ const allEntries = [...cachedEntries, ...uncachedEntries];
762
+ for (const entry of uncachedEntries) {
763
+ const score = pruner.priorityScore(entry, allEntries);
764
+ cacheEntry(entry, score);
765
+ }
766
+ const currentEntries = Array.from(state.entryCache.values()).map((c) => c.entry);
767
+ const tokensBefore = currentEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
768
+ const result = pruner.pruneToFit(currentEntries, budget);
769
+ const tokensAfter = result.kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
770
+ for (const removed of result.removed) {
771
+ state.entryCache.delete(removed.hash);
772
+ }
773
+ if (result.removed.length > 0) {
774
+ updateDocumentFrequency(result.removed, true);
775
+ }
776
+ const duration = Date.now() - startTime;
777
+ const cacheHitRate = newEntries.length > 0 ? cachedEntries.length / newEntries.length : 0;
778
+ getMetrics().recordOptimization({
779
+ timestamp: Date.now(),
780
+ duration,
781
+ tokensBefore,
782
+ tokensAfter,
783
+ entriesProcessed: newEntries.length,
784
+ entriesKept: result.kept.length,
785
+ cacheHitRate,
786
+ memoryUsage: process.memoryUsage().heapUsed
787
+ });
788
+ return result;
789
+ }
790
+ function optimizeFull(allEntries, budget) {
791
+ const startTime = Date.now();
792
+ const tokensBefore = allEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
793
+ state.entryCache.clear();
794
+ state.documentFrequency.clear();
795
+ state.totalDocuments = 0;
796
+ state.updateCount = 0;
797
+ state.lastFullOptimization = Date.now();
798
+ updateDocumentFrequency(allEntries, false);
799
+ for (const entry of allEntries) {
800
+ const score = pruner.priorityScore(entry, allEntries);
801
+ cacheEntry(entry, score);
802
+ }
803
+ const result = pruner.pruneToFit(allEntries, budget);
804
+ const tokensAfter = result.kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
805
+ for (const removed of result.removed) {
806
+ state.entryCache.delete(removed.hash);
807
+ }
808
+ if (result.removed.length > 0) {
809
+ updateDocumentFrequency(result.removed, true);
810
+ }
811
+ const duration = Date.now() - startTime;
812
+ getMetrics().recordOptimization({
813
+ timestamp: Date.now(),
814
+ duration,
815
+ tokensBefore,
816
+ tokensAfter,
817
+ entriesProcessed: allEntries.length,
818
+ entriesKept: result.kept.length,
819
+ cacheHitRate: 0,
820
+ // Full optimization has no cache hits
821
+ memoryUsage: process.memoryUsage().heapUsed
822
+ });
823
+ return result;
824
+ }
825
+ function getState() {
826
+ return {
827
+ entryCache: new Map(state.entryCache),
828
+ documentFrequency: new Map(state.documentFrequency),
829
+ totalDocuments: state.totalDocuments,
830
+ updateCount: state.updateCount,
831
+ lastFullOptimization: state.lastFullOptimization
832
+ };
833
+ }
834
+ function restoreState(restoredState) {
835
+ state = {
836
+ entryCache: new Map(restoredState.entryCache),
837
+ documentFrequency: new Map(restoredState.documentFrequency),
838
+ totalDocuments: restoredState.totalDocuments,
839
+ updateCount: restoredState.updateCount,
840
+ lastFullOptimization: restoredState.lastFullOptimization
841
+ };
842
+ }
843
+ function reset() {
844
+ state = {
845
+ entryCache: /* @__PURE__ */ new Map(),
846
+ documentFrequency: /* @__PURE__ */ new Map(),
847
+ totalDocuments: 0,
848
+ updateCount: 0,
849
+ lastFullOptimization: Date.now()
850
+ };
851
+ }
852
+ function getStats() {
853
+ return {
854
+ cachedEntries: state.entryCache.size,
855
+ uniqueTerms: state.documentFrequency.size,
856
+ totalDocuments: state.totalDocuments,
857
+ updateCount: state.updateCount,
858
+ lastFullOptimization: state.lastFullOptimization
859
+ };
860
+ }
861
+ return {
862
+ optimizeIncremental,
863
+ optimizeFull,
864
+ getState,
865
+ restoreState,
866
+ reset,
867
+ getStats
868
+ };
869
+ }
870
+
871
+ // src/core/context-pipeline.ts
872
+ function createContextPipeline(config) {
873
+ const optimizer = createIncrementalOptimizer(config);
874
+ const { windowSize, tokenBudget } = config;
875
+ let totalIngested = 0;
876
+ let evictedEntries = 0;
877
+ let currentEntries = [];
878
+ let budgetUtilization = 0;
879
+ function ingest(content, metadata = {}) {
880
+ const newEntries = parseClaudeCodeContext(content);
881
+ if (newEntries.length === 0) return 0;
882
+ const entriesWithMetadata = newEntries.map((entry) => ({
883
+ ...entry,
884
+ metadata: { ...entry.metadata, ...metadata }
885
+ }));
886
+ const result = optimizer.optimizeIncremental(entriesWithMetadata, tokenBudget);
887
+ totalIngested += newEntries.length;
888
+ evictedEntries += result.removed.length;
889
+ currentEntries = result.kept;
890
+ budgetUtilization = result.budgetUtilization;
891
+ if (currentEntries.length > windowSize) {
892
+ const sorted = [...currentEntries].sort((a, b) => b.timestamp - a.timestamp);
893
+ const toKeep = sorted.slice(0, windowSize);
894
+ const toRemove = sorted.slice(windowSize);
895
+ currentEntries = toKeep;
896
+ evictedEntries += toRemove.length;
897
+ }
898
+ return newEntries.length;
899
+ }
900
+ function getContext() {
901
+ const sorted = [...currentEntries].sort((a, b) => a.timestamp - b.timestamp);
902
+ return sorted.map((e) => e.content).join("\n\n");
903
+ }
904
+ function getEntries() {
905
+ return [...currentEntries].sort((a, b) => a.timestamp - b.timestamp);
906
+ }
907
+ function getStats() {
908
+ const optimizerStats = optimizer.getStats();
909
+ const currentTokens = currentEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
910
+ return {
911
+ totalIngested,
912
+ currentEntries: currentEntries.length,
913
+ currentTokens,
914
+ budgetUtilization,
915
+ evictedEntries,
916
+ optimizer: {
917
+ cachedEntries: optimizerStats.cachedEntries,
918
+ uniqueTerms: optimizerStats.uniqueTerms,
919
+ updateCount: optimizerStats.updateCount
920
+ }
921
+ };
922
+ }
923
+ function clear() {
924
+ totalIngested = 0;
925
+ evictedEntries = 0;
926
+ currentEntries = [];
927
+ budgetUtilization = 0;
928
+ optimizer.reset();
929
+ }
930
+ return {
931
+ ingest,
932
+ getContext,
933
+ getEntries,
934
+ getStats,
935
+ clear
936
+ };
937
+ }
938
+
475
939
  // src/core/kv-memory.ts
476
940
  import { copyFileSync, existsSync } from "fs";
477
941
  import Database from "better-sqlite3";
@@ -841,6 +1305,408 @@ function createSleepCompressor() {
841
1305
  };
842
1306
  }
843
1307
 
1308
+ // src/daemon/daemon-process.ts
1309
+ import { fork } from "child_process";
1310
+ import { existsSync as existsSync2, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
1311
+ import { dirname, join } from "path";
1312
+ import { fileURLToPath } from "url";
1313
+ function createDaemonCommand() {
1314
+ function isDaemonRunning(pidFile) {
1315
+ if (!existsSync2(pidFile)) {
1316
+ return { running: false };
1317
+ }
1318
+ try {
1319
+ const pidStr = readFileSync(pidFile, "utf-8").trim();
1320
+ const pid = Number.parseInt(pidStr, 10);
1321
+ if (Number.isNaN(pid)) {
1322
+ return { running: false };
1323
+ }
1324
+ try {
1325
+ process.kill(pid, 0);
1326
+ return { running: true, pid };
1327
+ } catch {
1328
+ unlinkSync(pidFile);
1329
+ return { running: false };
1330
+ }
1331
+ } catch {
1332
+ return { running: false };
1333
+ }
1334
+ }
1335
+ function writePidFile(pidFile, pid) {
1336
+ const dir = dirname(pidFile);
1337
+ if (!existsSync2(dir)) {
1338
+ mkdirSync(dir, { recursive: true });
1339
+ }
1340
+ writeFileSync(pidFile, String(pid), "utf-8");
1341
+ }
1342
+ function removePidFile(pidFile) {
1343
+ if (existsSync2(pidFile)) {
1344
+ unlinkSync(pidFile);
1345
+ }
1346
+ }
1347
+ async function start(config) {
1348
+ const { pidFile, logFile } = config.realtime;
1349
+ const status2 = isDaemonRunning(pidFile);
1350
+ if (status2.running) {
1351
+ return {
1352
+ success: false,
1353
+ pid: status2.pid,
1354
+ message: `Daemon already running (PID ${status2.pid})`,
1355
+ error: "Already running"
1356
+ };
1357
+ }
1358
+ try {
1359
+ const __filename2 = fileURLToPath(import.meta.url);
1360
+ const __dirname2 = dirname(__filename2);
1361
+ const daemonPath = join(__dirname2, "index.js");
1362
+ const child = fork(daemonPath, [], {
1363
+ detached: true,
1364
+ stdio: "ignore",
1365
+ env: {
1366
+ ...process.env,
1367
+ SPARN_CONFIG: JSON.stringify(config),
1368
+ SPARN_PID_FILE: pidFile,
1369
+ SPARN_LOG_FILE: logFile
1370
+ }
1371
+ });
1372
+ child.unref();
1373
+ if (child.pid) {
1374
+ writePidFile(pidFile, child.pid);
1375
+ return {
1376
+ success: true,
1377
+ pid: child.pid,
1378
+ message: `Daemon started (PID ${child.pid})`
1379
+ };
1380
+ }
1381
+ return {
1382
+ success: false,
1383
+ message: "Failed to start daemon (no PID)",
1384
+ error: "No PID"
1385
+ };
1386
+ } catch (error) {
1387
+ return {
1388
+ success: false,
1389
+ message: "Failed to start daemon",
1390
+ error: error instanceof Error ? error.message : String(error)
1391
+ };
1392
+ }
1393
+ }
1394
+ async function stop(config) {
1395
+ const { pidFile } = config.realtime;
1396
+ const status2 = isDaemonRunning(pidFile);
1397
+ if (!status2.running || !status2.pid) {
1398
+ return {
1399
+ success: true,
1400
+ message: "Daemon not running"
1401
+ };
1402
+ }
1403
+ try {
1404
+ process.kill(status2.pid, "SIGTERM");
1405
+ const maxWait = 5e3;
1406
+ const interval = 100;
1407
+ let waited = 0;
1408
+ while (waited < maxWait) {
1409
+ try {
1410
+ process.kill(status2.pid, 0);
1411
+ await new Promise((resolve) => setTimeout(resolve, interval));
1412
+ waited += interval;
1413
+ } catch {
1414
+ removePidFile(pidFile);
1415
+ return {
1416
+ success: true,
1417
+ message: `Daemon stopped (PID ${status2.pid})`
1418
+ };
1419
+ }
1420
+ }
1421
+ try {
1422
+ process.kill(status2.pid, "SIGKILL");
1423
+ removePidFile(pidFile);
1424
+ return {
1425
+ success: true,
1426
+ message: `Daemon force killed (PID ${status2.pid})`
1427
+ };
1428
+ } catch {
1429
+ removePidFile(pidFile);
1430
+ return {
1431
+ success: true,
1432
+ message: `Daemon stopped (PID ${status2.pid})`
1433
+ };
1434
+ }
1435
+ } catch (error) {
1436
+ return {
1437
+ success: false,
1438
+ message: "Failed to stop daemon",
1439
+ error: error instanceof Error ? error.message : String(error)
1440
+ };
1441
+ }
1442
+ }
1443
+ async function status(config) {
1444
+ const { pidFile } = config.realtime;
1445
+ const daemonStatus = isDaemonRunning(pidFile);
1446
+ if (!daemonStatus.running || !daemonStatus.pid) {
1447
+ return {
1448
+ running: false,
1449
+ message: "Daemon not running"
1450
+ };
1451
+ }
1452
+ const metrics = getMetrics().getSnapshot();
1453
+ return {
1454
+ running: true,
1455
+ pid: daemonStatus.pid,
1456
+ uptime: metrics.daemon.uptime,
1457
+ sessionsWatched: metrics.daemon.sessionsWatched,
1458
+ tokensSaved: metrics.optimization.totalTokensSaved,
1459
+ message: `Daemon running (PID ${daemonStatus.pid})`
1460
+ };
1461
+ }
1462
+ return {
1463
+ start,
1464
+ stop,
1465
+ status
1466
+ };
1467
+ }
1468
+
1469
+ // src/daemon/file-tracker.ts
1470
+ import { readFileSync as readFileSync2, statSync } from "fs";
1471
+ function createFileTracker() {
1472
+ const positions = /* @__PURE__ */ new Map();
1473
+ function readNewLines(filePath) {
1474
+ try {
1475
+ const stats = statSync(filePath);
1476
+ const currentSize = stats.size;
1477
+ const currentModified = stats.mtimeMs;
1478
+ let pos = positions.get(filePath);
1479
+ if (!pos) {
1480
+ pos = {
1481
+ path: filePath,
1482
+ position: 0,
1483
+ partialLine: "",
1484
+ lastModified: currentModified,
1485
+ lastSize: 0
1486
+ };
1487
+ positions.set(filePath, pos);
1488
+ }
1489
+ if (currentSize < pos.lastSize || currentSize === pos.position) {
1490
+ if (currentSize < pos.lastSize) {
1491
+ pos.position = 0;
1492
+ pos.partialLine = "";
1493
+ }
1494
+ return [];
1495
+ }
1496
+ const buffer = Buffer.alloc(currentSize - pos.position);
1497
+ const fd = readFileSync2(filePath);
1498
+ fd.copy(buffer, 0, pos.position, currentSize);
1499
+ const newContent = (pos.partialLine + buffer.toString("utf-8")).split("\n");
1500
+ const partialLine = newContent.pop() || "";
1501
+ pos.position = currentSize;
1502
+ pos.partialLine = partialLine;
1503
+ pos.lastModified = currentModified;
1504
+ pos.lastSize = currentSize;
1505
+ return newContent.filter((line) => line.trim().length > 0);
1506
+ } catch (_error) {
1507
+ return [];
1508
+ }
1509
+ }
1510
+ function getPosition(filePath) {
1511
+ return positions.get(filePath) || null;
1512
+ }
1513
+ function resetPosition(filePath) {
1514
+ positions.delete(filePath);
1515
+ }
1516
+ function clearAll() {
1517
+ positions.clear();
1518
+ }
1519
+ function getTrackedFiles() {
1520
+ return Array.from(positions.keys());
1521
+ }
1522
+ return {
1523
+ readNewLines,
1524
+ getPosition,
1525
+ resetPosition,
1526
+ clearAll,
1527
+ getTrackedFiles
1528
+ };
1529
+ }
1530
+
1531
+ // src/daemon/session-watcher.ts
1532
+ import { readdirSync, statSync as statSync2, watch } from "fs";
1533
+ import { homedir } from "os";
1534
+ import { dirname as dirname2, join as join2 } from "path";
1535
+ function createSessionWatcher(config) {
1536
+ const { config: sparnConfig, onOptimize, onError } = config;
1537
+ const { realtime, decay, states } = sparnConfig;
1538
+ const pipelines = /* @__PURE__ */ new Map();
1539
+ const fileTracker = createFileTracker();
1540
+ const watchers = [];
1541
+ const debounceTimers = /* @__PURE__ */ new Map();
1542
+ function getProjectsDir() {
1543
+ return join2(homedir(), ".claude", "projects");
1544
+ }
1545
+ function getSessionId(filePath) {
1546
+ const filename = filePath.split(/[/\\]/).pop() || "";
1547
+ return filename.replace(/\.jsonl$/, "");
1548
+ }
1549
+ function getPipeline(sessionId) {
1550
+ let pipeline = pipelines.get(sessionId);
1551
+ if (!pipeline) {
1552
+ pipeline = createContextPipeline({
1553
+ tokenBudget: realtime.tokenBudget,
1554
+ decay,
1555
+ states,
1556
+ windowSize: realtime.windowSize,
1557
+ fullOptimizationInterval: 50
1558
+ // Full re-optimization every 50 incremental updates
1559
+ });
1560
+ pipelines.set(sessionId, pipeline);
1561
+ }
1562
+ return pipeline;
1563
+ }
1564
+ function handleFileChange(filePath) {
1565
+ const existingTimer = debounceTimers.get(filePath);
1566
+ if (existingTimer) {
1567
+ clearTimeout(existingTimer);
1568
+ }
1569
+ const timer = setTimeout(() => {
1570
+ try {
1571
+ const newLines = fileTracker.readNewLines(filePath);
1572
+ if (newLines.length === 0) return;
1573
+ const content = newLines.join("\n");
1574
+ const sessionId = getSessionId(filePath);
1575
+ const pipeline = getPipeline(sessionId);
1576
+ pipeline.ingest(content, { sessionId, filePath });
1577
+ const stats = pipeline.getStats();
1578
+ if (stats.currentTokens >= realtime.autoOptimizeThreshold) {
1579
+ getMetrics().updateDaemon({
1580
+ sessionsWatched: pipelines.size,
1581
+ memoryUsage: process.memoryUsage().heapUsed
1582
+ });
1583
+ if (onOptimize) {
1584
+ const sessionStats = computeSessionStats(sessionId, pipeline);
1585
+ onOptimize(sessionId, sessionStats);
1586
+ }
1587
+ }
1588
+ } catch (error) {
1589
+ if (onError) {
1590
+ onError(error instanceof Error ? error : new Error(String(error)));
1591
+ }
1592
+ } finally {
1593
+ debounceTimers.delete(filePath);
1594
+ }
1595
+ }, realtime.debounceMs);
1596
+ debounceTimers.set(filePath, timer);
1597
+ }
1598
+ function findJsonlFiles(dir) {
1599
+ const files = [];
1600
+ try {
1601
+ const entries = readdirSync(dir);
1602
+ for (const entry of entries) {
1603
+ const fullPath = join2(dir, entry);
1604
+ const stat = statSync2(fullPath);
1605
+ if (stat.isDirectory()) {
1606
+ files.push(...findJsonlFiles(fullPath));
1607
+ } else if (entry.endsWith(".jsonl")) {
1608
+ const matches = realtime.watchPatterns.some((pattern) => {
1609
+ const regex = new RegExp(
1610
+ pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*").replace(/\./g, "\\.")
1611
+ );
1612
+ return regex.test(fullPath);
1613
+ });
1614
+ if (matches) {
1615
+ files.push(fullPath);
1616
+ }
1617
+ }
1618
+ }
1619
+ } catch (_error) {
1620
+ }
1621
+ return files;
1622
+ }
1623
+ function computeSessionStats(sessionId, pipeline) {
1624
+ const stats = pipeline.getStats();
1625
+ const entries = pipeline.getEntries();
1626
+ const totalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
1627
+ return {
1628
+ sessionId,
1629
+ totalTokens: stats.totalIngested,
1630
+ optimizedTokens: stats.currentTokens,
1631
+ reduction: totalTokens > 0 ? (totalTokens - stats.currentTokens) / totalTokens : 0,
1632
+ entryCount: stats.currentEntries,
1633
+ budgetUtilization: stats.budgetUtilization
1634
+ };
1635
+ }
1636
+ async function start() {
1637
+ const projectsDir = getProjectsDir();
1638
+ const jsonlFiles = findJsonlFiles(projectsDir);
1639
+ const watchedDirs = /* @__PURE__ */ new Set();
1640
+ for (const file of jsonlFiles) {
1641
+ const dir = dirname2(file);
1642
+ if (!watchedDirs.has(dir)) {
1643
+ const watcher = watch(dir, { recursive: false }, (_eventType, filename) => {
1644
+ if (filename?.endsWith(".jsonl")) {
1645
+ const fullPath = join2(dir, filename);
1646
+ handleFileChange(fullPath);
1647
+ }
1648
+ });
1649
+ watchers.push(watcher);
1650
+ watchedDirs.add(dir);
1651
+ }
1652
+ }
1653
+ const projectsWatcher = watch(projectsDir, { recursive: true }, (_eventType, filename) => {
1654
+ if (filename?.endsWith(".jsonl")) {
1655
+ const fullPath = join2(projectsDir, filename);
1656
+ handleFileChange(fullPath);
1657
+ }
1658
+ });
1659
+ watchers.push(projectsWatcher);
1660
+ getMetrics().updateDaemon({
1661
+ startTime: Date.now(),
1662
+ sessionsWatched: jsonlFiles.length,
1663
+ memoryUsage: process.memoryUsage().heapUsed
1664
+ });
1665
+ }
1666
+ function stop() {
1667
+ for (const watcher of watchers) {
1668
+ watcher.close();
1669
+ }
1670
+ watchers.length = 0;
1671
+ for (const timer of debounceTimers.values()) {
1672
+ clearTimeout(timer);
1673
+ }
1674
+ debounceTimers.clear();
1675
+ pipelines.clear();
1676
+ fileTracker.clearAll();
1677
+ }
1678
+ function getStats() {
1679
+ const stats = [];
1680
+ for (const [sessionId, pipeline] of pipelines.entries()) {
1681
+ stats.push(computeSessionStats(sessionId, pipeline));
1682
+ }
1683
+ return stats;
1684
+ }
1685
+ function getSessionStats(sessionId) {
1686
+ const pipeline = pipelines.get(sessionId);
1687
+ if (!pipeline) return null;
1688
+ return computeSessionStats(sessionId, pipeline);
1689
+ }
1690
+ function optimizeSession(sessionId) {
1691
+ const pipeline = pipelines.get(sessionId);
1692
+ if (!pipeline) return;
1693
+ const entries = pipeline.getEntries();
1694
+ pipeline.clear();
1695
+ pipeline.ingest(entries.map((e) => e.content).join("\n\n"));
1696
+ if (onOptimize) {
1697
+ const stats = computeSessionStats(sessionId, pipeline);
1698
+ onOptimize(sessionId, stats);
1699
+ }
1700
+ }
1701
+ return {
1702
+ start,
1703
+ stop,
1704
+ getStats,
1705
+ getSessionStats,
1706
+ optimizeSession
1707
+ };
1708
+ }
1709
+
844
1710
  // src/types/config.ts
845
1711
  var DEFAULT_CONFIG = {
846
1712
  pruning: {
@@ -861,7 +1727,17 @@ var DEFAULT_CONFIG = {
861
1727
  sounds: false,
862
1728
  verbose: false
863
1729
  },
864
- autoConsolidate: null
1730
+ autoConsolidate: null,
1731
+ realtime: {
1732
+ tokenBudget: 5e4,
1733
+ autoOptimizeThreshold: 8e4,
1734
+ watchPatterns: ["**/*.jsonl"],
1735
+ pidFile: ".sparn/daemon.pid",
1736
+ logFile: ".sparn/daemon.log",
1737
+ debounceMs: 5e3,
1738
+ incremental: true,
1739
+ windowSize: 500
1740
+ }
865
1741
  };
866
1742
 
867
1743
  // src/utils/logger.ts
@@ -886,15 +1762,25 @@ function createLogger(verbose = false) {
886
1762
  export {
887
1763
  DEFAULT_CONFIG,
888
1764
  createBTSPEmbedder,
1765
+ createBudgetPruner,
1766
+ createBudgetPrunerFromConfig,
889
1767
  createClaudeCodeAdapter,
890
1768
  createConfidenceStates,
1769
+ createContextPipeline,
1770
+ createDaemonCommand,
891
1771
  createEngramScorer,
1772
+ createEntry,
1773
+ createFileTracker,
892
1774
  createGenericAdapter,
1775
+ createIncrementalOptimizer,
893
1776
  createKVMemory,
894
1777
  createLogger,
1778
+ createSessionWatcher,
895
1779
  createSleepCompressor,
896
1780
  createSparsePruner,
897
1781
  estimateTokens,
898
- hashContent
1782
+ hashContent,
1783
+ parseClaudeCodeContext,
1784
+ parseGenericContext
899
1785
  };
900
1786
  //# sourceMappingURL=index.js.map