@199-bio/engram 0.8.1 → 0.10.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.
@@ -16,6 +16,7 @@ import { EngramDatabase, Memory, Digest, Episode } from "../storage/database.js"
16
16
  import { getAnthropicApiKey } from "../settings.js";
17
17
  import { KnowledgeGraph } from "../graph/knowledge-graph.js";
18
18
  import { HybridSearch } from "../retrieval/hybrid.js";
19
+ import { ConsolidationPlan, BacklogAssessment, ConsolidationProgress } from "./plan.js";
19
20
 
20
21
  const CONSOLIDATION_SYSTEM = `You are a high-quality memory consolidation system for a personal AI assistant. Your goal is to create comprehensive, nuanced digests that preserve the richness of human experience and relationships.
21
22
 
@@ -616,42 +617,365 @@ Respond with JSON only.`;
616
617
  }
617
618
 
618
619
  /**
619
- * Run full consolidation cycle (episodes memories → digests)
620
+ * Assess current backlog and return a plan
621
+ */
622
+ assessBacklog(): BacklogAssessment {
623
+ const plan = new ConsolidationPlan(this.db);
624
+ return plan.assessBacklog();
625
+ }
626
+
627
+ /**
628
+ * Run full consolidation cycle with safety SOP
620
629
  * This is the "sleep cycle" that should run periodically
630
+ *
631
+ * SOP (Standard Operating Procedure):
632
+ * 1. Assess backlog and check budget
633
+ * 2. Check for incomplete runs to resume
634
+ * 3. Create checkpoint for tracking
635
+ * 4. Process with rate limiting and validation
636
+ * 5. Check rollback triggers after each batch
637
+ * 6. Mark complete or fail with error
621
638
  */
622
- async runSleepCycle(): Promise<{
639
+ async runSleepCycle(options: {
640
+ force?: boolean; // Ignore budget limits
641
+ maxBatches?: number; // Override max batches
642
+ } = {}): Promise<{
623
643
  episodesProcessed: number;
624
644
  memoriesCreated: number;
625
645
  digestsCreated: number;
626
646
  contradictionsFound: number;
627
647
  connectionsDecayed: number;
628
648
  logsCleanedUp: number;
649
+ tokensUsed: number;
650
+ estimatedCost: number;
651
+ aborted: boolean;
652
+ abortReason?: string;
629
653
  }> {
630
- console.error("[Consolidator] Starting sleep cycle...");
654
+ const plan = new ConsolidationPlan(this.db);
655
+
656
+ // Check for incomplete run to resume
657
+ const incomplete = plan.checkForResume();
658
+ if (incomplete) {
659
+ console.error(`[Consolidator] Resuming incomplete run ${incomplete.run_id} from phase ${incomplete.phase}`);
660
+ plan.resumeFrom(incomplete);
661
+ }
662
+
663
+ // Assess backlog
664
+ const assessment = plan.assessBacklog();
665
+ console.error(`[Consolidator] Assessment: ${assessment.unconsolidatedEpisodes} episodes, ${assessment.unconsolidatedMemories} memories`);
666
+ console.error(`[Consolidator] Budget: $${assessment.dailySpent.toFixed(2)} / $${assessment.dailyBudget.toFixed(2)} (remaining: $${assessment.budgetRemaining.toFixed(2)})`);
667
+
668
+ if (assessment.isRecoveryMode) {
669
+ console.error(`[Consolidator] RECOVERY MODE: Large backlog detected, processing conservatively`);
670
+ }
671
+
672
+ // Check if we can proceed
673
+ if (!options.force && !assessment.canProceed) {
674
+ console.error(`[Consolidator] Budget exceeded, skipping consolidation`);
675
+ return {
676
+ episodesProcessed: 0,
677
+ memoriesCreated: 0,
678
+ digestsCreated: 0,
679
+ contradictionsFound: 0,
680
+ connectionsDecayed: 0,
681
+ logsCleanedUp: 0,
682
+ tokensUsed: 0,
683
+ estimatedCost: 0,
684
+ aborted: true,
685
+ abortReason: "Daily budget exceeded",
686
+ };
687
+ }
688
+
689
+ const maxBatches = options.maxBatches ?? assessment.recommendedBatches;
690
+ let totalTokens = 0;
691
+ let totalCost = 0;
692
+ let aborted = false;
693
+ let abortReason: string | undefined;
694
+
695
+ // Create checkpoint
696
+ const totalBatches = assessment.phases
697
+ .filter(p => p.phase === "episodes" || p.phase === "memories")
698
+ .reduce((sum, p) => sum + Math.min(p.batchCount, maxBatches), 0);
699
+
700
+ if (!incomplete) {
701
+ plan.createCheckpoint("episodes", totalBatches);
702
+ }
703
+
704
+ console.error(`[Consolidator] Starting sleep cycle (max ${maxBatches} batches per phase)...`);
705
+
706
+ // ============ Phase 1: Episodes → Memories ============
707
+ let episodesProcessed = incomplete?.episodes_processed || 0;
708
+ let memoriesCreated = 0;
709
+ let entitiesCreated = 0;
710
+
711
+ if (assessment.unconsolidatedEpisodes >= 4 && !aborted) {
712
+ plan.updateProgress({ phase: "episodes" });
713
+ console.error(`[Consolidator] Phase 1: Processing episodes...`);
714
+
715
+ const episodeBatchSize = 20;
716
+ const episodes = plan.getPrioritizedEpisodes(maxBatches * episodeBatchSize);
717
+
718
+ // Group by session
719
+ const sessionGroups = new Map<string, Episode[]>();
720
+ for (const ep of episodes) {
721
+ const existing = sessionGroups.get(ep.session_id) || [];
722
+ existing.push(ep);
723
+ sessionGroups.set(ep.session_id, existing);
724
+ }
725
+
726
+ let batchIndex = 0;
727
+ for (const [sessionId, sessionEpisodes] of sessionGroups) {
728
+ if (batchIndex >= maxBatches) break;
729
+ if (sessionEpisodes.length < 2) continue;
730
+
731
+ try {
732
+ // Rate limiting delay
733
+ if (batchIndex > 0) {
734
+ await plan.delay();
735
+ }
736
+
737
+ const result = await this.extractMemoriesFromEpisodes(sessionEpisodes);
738
+ plan.recordApiCall(result !== null);
739
+
740
+ if (result && result.memories.length > 0) {
741
+ for (const mem of result.memories) {
742
+ const memory = this.db.createMemory(
743
+ mem.content,
744
+ "episode_consolidation",
745
+ mem.importance,
746
+ {
747
+ eventTime: mem.event_time ? new Date(mem.event_time) : undefined,
748
+ emotionalWeight: mem.emotional_weight,
749
+ }
750
+ );
751
+ memoriesCreated++;
631
752
 
632
- // Step 1: Process episodes into memories
633
- const episodeResult = await this.consolidateEpisodes();
634
- console.error(`[Consolidator] Episodes: ${episodeResult.episodesProcessed} → ${episodeResult.memoriesCreated} memories`);
753
+ if (this.search) {
754
+ await this.search.indexMemory(memory);
755
+ }
635
756
 
636
- // Step 2: Consolidate memories into digests
637
- const memoryResult = await this.consolidate();
638
- console.error(`[Consolidator] Memories: ${memoryResult.memoriesProcessed} → ${memoryResult.digestsCreated} digests`);
757
+ if (this.graph) {
758
+ for (const ent of mem.entities || []) {
759
+ const entity = this.graph.getOrCreateEntity(
760
+ ent.name,
761
+ ent.type as "person" | "place" | "concept" | "event" | "organization"
762
+ );
763
+ this.db.addObservation(entity.id, mem.content, memory.id, 1.0);
764
+ entitiesCreated++;
765
+ }
639
766
 
640
- // Step 3: Decay unused Hebbian connections (memories that haven't fired together recently)
641
- const connectionsDecayed = this.db.decayConnections(30, 0.9);
642
- console.error(`[Consolidator] Connections decayed: ${connectionsDecayed}`);
767
+ for (const rel of mem.relationships || []) {
768
+ try {
769
+ const fromEntity = this.graph.getOrCreateEntity(rel.from, "person");
770
+ const toEntity = this.graph.getOrCreateEntity(rel.to, "person");
771
+ this.graph.relate(fromEntity.name, toEntity.name, rel.type);
772
+ } catch {
773
+ // Skip invalid relationships
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
643
779
 
644
- // Step 4: Clean up old retrieval logs
645
- const logsCleanedUp = this.db.cleanupRetrievalLogs(7);
646
- console.error(`[Consolidator] Retrieval logs cleaned: ${logsCleanedUp}`);
780
+ this.db.markEpisodesConsolidated(sessionEpisodes.map(e => e.id));
781
+ episodesProcessed += sessionEpisodes.length;
782
+ batchIndex++;
783
+
784
+ // Estimate tokens (Haiku)
785
+ const batchTokens = 3000; // Conservative estimate
786
+ totalTokens += batchTokens;
787
+ totalCost += plan.calculateCost("haiku", 2000, 1000);
788
+
789
+ plan.updateProgress({
790
+ batchesCompleted: batchIndex,
791
+ episodesProcessed,
792
+ tokensUsed: totalTokens,
793
+ estimatedCost: totalCost,
794
+ });
795
+
796
+ // Check rollback triggers
797
+ const triggers = plan.checkRollbackTriggers();
798
+ const fired = triggers.filter(t => t.triggered);
799
+ if (fired.length > 0) {
800
+ aborted = true;
801
+ abortReason = fired.map(t => t.message).join("; ");
802
+ console.error(`[Consolidator] ROLLBACK TRIGGERED: ${abortReason}`);
803
+ break;
804
+ }
805
+
806
+ } catch (error) {
807
+ const errMsg = error instanceof Error ? error.message : String(error);
808
+ plan.recordError(errMsg);
809
+ plan.recordApiCall(false);
810
+ console.error(`[Consolidator] Episode batch failed: ${errMsg}`);
811
+ }
812
+ }
813
+
814
+ console.error(`[Consolidator] Episodes: ${episodesProcessed} → ${memoriesCreated} memories`);
815
+ }
816
+
817
+ // ============ Phase 2: Memories → Digests ============
818
+ let digestsCreated = incomplete?.digests_created || 0;
819
+ let contradictionsFound = incomplete?.contradictions_found || 0;
820
+ let memoriesConsolidated = incomplete?.memories_processed || 0;
821
+
822
+ if (assessment.unconsolidatedMemories >= 5 && !aborted) {
823
+ plan.updateProgress({ phase: "memories" });
824
+ console.error(`[Consolidator] Phase 2: Consolidating memories...`);
825
+
826
+ const batchSize = 15;
827
+ const memories = plan.getPrioritizedMemories(maxBatches * batchSize);
828
+
829
+ for (let i = 0; i < memories.length && !aborted; i += batchSize) {
830
+ if (i / batchSize >= maxBatches) break;
831
+
832
+ const batch = memories.slice(i, i + batchSize);
833
+ if (batch.length < 3) break;
834
+
835
+ try {
836
+ // Rate limiting delay
837
+ if (i > 0) {
838
+ await plan.delay();
839
+ }
840
+
841
+ const result = await this.consolidateBatch(batch);
842
+ plan.recordApiCall(result !== null);
843
+ plan.recordDigest(result === null || !result.digest);
844
+
845
+ if (result) {
846
+ const memoryIds = batch.map(m => m.id);
847
+ const periodStart = new Date(Math.min(...batch.map(m => m.timestamp.getTime())));
848
+ const periodEnd = new Date(Math.max(...batch.map(m => m.timestamp.getTime())));
849
+
850
+ this.db.createDigest(result.digest, 1, memoryIds, {
851
+ topic: result.topic,
852
+ periodStart,
853
+ periodEnd,
854
+ });
855
+ digestsCreated++;
856
+ memoriesConsolidated += batch.length;
857
+
858
+ for (const c of result.contradictions) {
859
+ if (c.memory_ids.length >= 2) {
860
+ const [idA, idB] = c.memory_ids.slice(0, 2);
861
+ const memA = batch.find(m => m.id === idA);
862
+ const memB = batch.find(m => m.id === idB);
863
+
864
+ if (memA && memB) {
865
+ this.db.createContradiction(memA.id, memB.id, c.description);
866
+ contradictionsFound++;
867
+ }
868
+ }
869
+ }
870
+ }
871
+
872
+ // Estimate tokens (Opus with thinking)
873
+ const batchTokens = 15000; // Conservative estimate
874
+ totalTokens += batchTokens;
875
+ totalCost += plan.calculateCost("opus", 3000, 2000, 10000);
876
+
877
+ plan.updateProgress({
878
+ batchesCompleted: (i / batchSize) + 1,
879
+ memoriesProcessed: memoriesConsolidated,
880
+ digestsCreated,
881
+ contradictionsFound,
882
+ tokensUsed: totalTokens,
883
+ estimatedCost: totalCost,
884
+ });
885
+
886
+ // Check rollback triggers
887
+ const triggers = plan.checkRollbackTriggers();
888
+ const fired = triggers.filter(t => t.triggered);
889
+ if (fired.length > 0) {
890
+ aborted = true;
891
+ abortReason = fired.map(t => t.message).join("; ");
892
+ console.error(`[Consolidator] ROLLBACK TRIGGERED: ${abortReason}`);
893
+ break;
894
+ }
895
+
896
+ } catch (error) {
897
+ const errMsg = error instanceof Error ? error.message : String(error);
898
+ plan.recordError(errMsg);
899
+ plan.recordApiCall(false);
900
+ console.error(`[Consolidator] Memory batch failed: ${errMsg}`);
901
+ }
902
+ }
903
+
904
+ console.error(`[Consolidator] Memories: ${memoriesConsolidated} → ${digestsCreated} digests`);
905
+ }
906
+
907
+ // ============ Phase 3: Decay connections ============
908
+ let connectionsDecayed = 0;
909
+ if (!aborted) {
910
+ plan.updateProgress({ phase: "decay" });
911
+ connectionsDecayed = this.db.decayConnections(30, 0.9);
912
+ console.error(`[Consolidator] Connections decayed: ${connectionsDecayed}`);
913
+ }
914
+
915
+ // ============ Phase 4: Cleanup ============
916
+ let logsCleanedUp = 0;
917
+ if (!aborted) {
918
+ plan.updateProgress({ phase: "cleanup" });
919
+ logsCleanedUp = this.db.cleanupRetrievalLogs(7);
920
+ console.error(`[Consolidator] Retrieval logs cleaned: ${logsCleanedUp}`);
921
+ }
922
+
923
+ // Mark complete or failed
924
+ if (aborted) {
925
+ plan.fail(abortReason || "Unknown error");
926
+ } else {
927
+ plan.complete();
928
+ }
929
+
930
+ console.error(`[Consolidator] Sleep cycle complete. Tokens: ${totalTokens}, Cost: $${totalCost.toFixed(4)}`);
647
931
 
648
932
  return {
649
- episodesProcessed: episodeResult.episodesProcessed,
650
- memoriesCreated: episodeResult.memoriesCreated,
651
- digestsCreated: memoryResult.digestsCreated,
652
- contradictionsFound: memoryResult.contradictionsFound,
933
+ episodesProcessed,
934
+ memoriesCreated,
935
+ digestsCreated,
936
+ contradictionsFound,
653
937
  connectionsDecayed,
654
938
  logsCleanedUp,
939
+ tokensUsed: totalTokens,
940
+ estimatedCost: totalCost,
941
+ aborted,
942
+ abortReason,
655
943
  };
656
944
  }
945
+
946
+ /**
947
+ * Get consolidation progress for the current/latest run
948
+ */
949
+ getConsolidationProgress(): ConsolidationProgress | null {
950
+ const plan = new ConsolidationPlan(this.db);
951
+ const checkpoint = plan.checkForResume();
952
+ if (checkpoint) {
953
+ plan.resumeFrom(checkpoint);
954
+ return plan.getProgress();
955
+ }
956
+
957
+ // Get most recent completed run
958
+ const recent = this.db.getRecentCheckpoints(1);
959
+ if (recent.length > 0) {
960
+ return {
961
+ runId: recent[0].run_id,
962
+ phase: recent[0].phase,
963
+ batchesCompleted: recent[0].batches_completed,
964
+ batchesTotal: recent[0].batches_total,
965
+ memoriesProcessed: recent[0].memories_processed,
966
+ episodesProcessed: recent[0].episodes_processed,
967
+ digestsCreated: recent[0].digests_created,
968
+ contradictionsFound: recent[0].contradictions_found,
969
+ tokensUsed: recent[0].tokens_used,
970
+ estimatedCost: recent[0].estimated_cost_usd,
971
+ errors: recent[0].error ? [recent[0].error] : [],
972
+ startedAt: recent[0].started_at,
973
+ elapsedMs: recent[0].completed_at
974
+ ? recent[0].completed_at.getTime() - recent[0].started_at.getTime()
975
+ : Date.now() - recent[0].started_at.getTime(),
976
+ };
977
+ }
978
+
979
+ return null;
980
+ }
657
981
  }