@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.
- package/.env.example +5 -0
- package/boba-prompt.md +107 -0
- package/dist/consolidation/consolidator.d.ts.map +1 -1
- package/dist/consolidation/plan.d.ts.map +1 -0
- package/dist/index.js +62 -17
- package/dist/retrieval/hybrid.d.ts.map +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/web/chat-handler.d.ts.map +1 -1
- package/nixpacks.toml +11 -0
- package/package.json +2 -1
- package/railway.json +13 -0
- package/src/consolidation/consolidator.ts +343 -19
- package/src/consolidation/plan.ts +444 -0
- package/src/index.ts +65 -19
- package/src/retrieval/hybrid.ts +63 -3
- package/src/storage/database.ts +307 -0
- package/src/transport/http.ts +111 -0
- package/src/transport/index.ts +24 -0
- package/src/web/chat-handler.ts +58 -15
- package/src/web/static/app.js +612 -360
- package/src/web/static/index.html +377 -130
- package/src/web/static/style.css +1249 -672
|
@@ -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
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
753
|
+
if (this.search) {
|
|
754
|
+
await this.search.indexMemory(memory);
|
|
755
|
+
}
|
|
635
756
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
|
650
|
-
memoriesCreated
|
|
651
|
-
digestsCreated
|
|
652
|
-
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
|
}
|