@199-bio/engram 0.8.0 → 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 +170 -9
- 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 +381 -29
- package/src/consolidation/plan.ts +444 -0
- package/src/index.ts +181 -10
- package/src/retrieval/hybrid.ts +69 -5
- package/src/storage/database.ts +358 -38
- package/src/transport/http.ts +111 -0
- package/src/transport/index.ts +24 -0
- package/src/web/chat-handler.ts +116 -70
- 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
|
|
|
@@ -112,6 +113,7 @@ interface ConsolidateOptions {
|
|
|
112
113
|
|
|
113
114
|
export class Consolidator {
|
|
114
115
|
private client: Anthropic | null = null;
|
|
116
|
+
private cachedApiKey: string | null = null;
|
|
115
117
|
private db: EngramDatabase;
|
|
116
118
|
private graph: KnowledgeGraph | null = null;
|
|
117
119
|
private search: HybridSearch | null = null;
|
|
@@ -125,14 +127,36 @@ export class Consolidator {
|
|
|
125
127
|
this.graph = graph || null;
|
|
126
128
|
this.search = search || null;
|
|
127
129
|
|
|
130
|
+
// Initial check
|
|
131
|
+
this.ensureClient();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Ensure client is configured with latest API key
|
|
136
|
+
* Lazy initialization: checks for new/updated API key each call
|
|
137
|
+
*/
|
|
138
|
+
private ensureClient(): Anthropic | null {
|
|
128
139
|
const apiKey = getAnthropicApiKey();
|
|
129
|
-
|
|
140
|
+
|
|
141
|
+
if (!apiKey) {
|
|
142
|
+
this.client = null;
|
|
143
|
+
this.cachedApiKey = null;
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Only recreate client if API key changed
|
|
148
|
+
if (apiKey !== this.cachedApiKey) {
|
|
149
|
+
console.error(`[Engram] Consolidator: API key ${this.cachedApiKey ? "updated" : "configured"}`);
|
|
130
150
|
this.client = new Anthropic({ apiKey });
|
|
151
|
+
this.cachedApiKey = apiKey;
|
|
131
152
|
}
|
|
153
|
+
|
|
154
|
+
return this.client;
|
|
132
155
|
}
|
|
133
156
|
|
|
134
157
|
isConfigured(): boolean {
|
|
135
|
-
|
|
158
|
+
// Re-check in case API key was added after startup
|
|
159
|
+
return this.ensureClient() !== null;
|
|
136
160
|
}
|
|
137
161
|
|
|
138
162
|
/**
|
|
@@ -144,7 +168,8 @@ export class Consolidator {
|
|
|
144
168
|
contradictionsFound: number;
|
|
145
169
|
memoriesProcessed: number;
|
|
146
170
|
}> {
|
|
147
|
-
|
|
171
|
+
const client = this.ensureClient();
|
|
172
|
+
if (!client) {
|
|
148
173
|
throw new Error("Consolidator not configured - set ANTHROPIC_API_KEY");
|
|
149
174
|
}
|
|
150
175
|
|
|
@@ -221,7 +246,8 @@ export class Consolidator {
|
|
|
221
246
|
private async consolidateBatch(
|
|
222
247
|
memories: Memory[]
|
|
223
248
|
): Promise<ConsolidationResult | null> {
|
|
224
|
-
|
|
249
|
+
const client = this.ensureClient();
|
|
250
|
+
if (!client) return null;
|
|
225
251
|
|
|
226
252
|
// Format memories for the prompt
|
|
227
253
|
const memoriesText = memories
|
|
@@ -246,7 +272,7 @@ ${memoriesText}
|
|
|
246
272
|
Create a detailed digest that preserves all important information. Respond with JSON only.`;
|
|
247
273
|
|
|
248
274
|
try {
|
|
249
|
-
const response = await
|
|
275
|
+
const response = await client.messages.create({
|
|
250
276
|
model: "claude-opus-4-5-20251101",
|
|
251
277
|
max_tokens: 16000,
|
|
252
278
|
temperature: 1, // Required for extended thinking
|
|
@@ -290,7 +316,8 @@ Create a detailed digest that preserves all important information. Respond with
|
|
|
290
316
|
* Create an entity profile by consolidating all observations about an entity
|
|
291
317
|
*/
|
|
292
318
|
async consolidateEntity(entityId: string): Promise<Digest | null> {
|
|
293
|
-
|
|
319
|
+
const client = this.ensureClient();
|
|
320
|
+
if (!client) {
|
|
294
321
|
throw new Error("Consolidator not configured - set ANTHROPIC_API_KEY");
|
|
295
322
|
}
|
|
296
323
|
|
|
@@ -335,7 +362,7 @@ ${memoriesText}
|
|
|
335
362
|
Create a rich, detailed profile. Do not summarize away important nuances. Respond with JSON only.`;
|
|
336
363
|
|
|
337
364
|
try {
|
|
338
|
-
const response = await
|
|
365
|
+
const response = await client.messages.create({
|
|
339
366
|
model: "claude-opus-4-5-20251101",
|
|
340
367
|
max_tokens: 16000,
|
|
341
368
|
temperature: 1, // Required for extended thinking
|
|
@@ -444,7 +471,8 @@ Create a rich, detailed profile. Do not summarize away important nuances. Respon
|
|
|
444
471
|
memoriesCreated: number;
|
|
445
472
|
entitiesCreated: number;
|
|
446
473
|
}> {
|
|
447
|
-
|
|
474
|
+
const client = this.ensureClient();
|
|
475
|
+
if (!client) {
|
|
448
476
|
throw new Error("Consolidator not configured - set ANTHROPIC_API_KEY");
|
|
449
477
|
}
|
|
450
478
|
|
|
@@ -537,7 +565,8 @@ Create a rich, detailed profile. Do not summarize away important nuances. Respon
|
|
|
537
565
|
private async extractMemoriesFromEpisodes(
|
|
538
566
|
episodes: Episode[]
|
|
539
567
|
): Promise<EpisodeExtractionResult | null> {
|
|
540
|
-
|
|
568
|
+
const client = this.ensureClient();
|
|
569
|
+
if (!client) return null;
|
|
541
570
|
|
|
542
571
|
// Format conversation
|
|
543
572
|
const conversationText = episodes
|
|
@@ -555,7 +584,7 @@ Respond with JSON only.`;
|
|
|
555
584
|
|
|
556
585
|
try {
|
|
557
586
|
// Use Haiku for speed/cost (no extended thinking needed)
|
|
558
|
-
const response = await
|
|
587
|
+
const response = await client.messages.create({
|
|
559
588
|
model: "claude-haiku-4-5-20251201",
|
|
560
589
|
max_tokens: 4000,
|
|
561
590
|
messages: [
|
|
@@ -588,42 +617,365 @@ Respond with JSON only.`;
|
|
|
588
617
|
}
|
|
589
618
|
|
|
590
619
|
/**
|
|
591
|
-
*
|
|
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
|
|
592
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
|
|
593
638
|
*/
|
|
594
|
-
async runSleepCycle(
|
|
639
|
+
async runSleepCycle(options: {
|
|
640
|
+
force?: boolean; // Ignore budget limits
|
|
641
|
+
maxBatches?: number; // Override max batches
|
|
642
|
+
} = {}): Promise<{
|
|
595
643
|
episodesProcessed: number;
|
|
596
644
|
memoriesCreated: number;
|
|
597
645
|
digestsCreated: number;
|
|
598
646
|
contradictionsFound: number;
|
|
599
647
|
connectionsDecayed: number;
|
|
600
648
|
logsCleanedUp: number;
|
|
649
|
+
tokensUsed: number;
|
|
650
|
+
estimatedCost: number;
|
|
651
|
+
aborted: boolean;
|
|
652
|
+
abortReason?: string;
|
|
601
653
|
}> {
|
|
602
|
-
|
|
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++;
|
|
752
|
+
|
|
753
|
+
if (this.search) {
|
|
754
|
+
await this.search.indexMemory(memory);
|
|
755
|
+
}
|
|
756
|
+
|
|
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
|
+
}
|
|
766
|
+
|
|
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
|
+
}
|
|
779
|
+
|
|
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
|
+
}
|
|
603
895
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
+
}
|
|
607
914
|
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
}
|
|
611
922
|
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
923
|
+
// Mark complete or failed
|
|
924
|
+
if (aborted) {
|
|
925
|
+
plan.fail(abortReason || "Unknown error");
|
|
926
|
+
} else {
|
|
927
|
+
plan.complete();
|
|
928
|
+
}
|
|
615
929
|
|
|
616
|
-
|
|
617
|
-
const logsCleanedUp = this.db.cleanupRetrievalLogs(7);
|
|
618
|
-
console.error(`[Consolidator] Retrieval logs cleaned: ${logsCleanedUp}`);
|
|
930
|
+
console.error(`[Consolidator] Sleep cycle complete. Tokens: ${totalTokens}, Cost: $${totalCost.toFixed(4)}`);
|
|
619
931
|
|
|
620
932
|
return {
|
|
621
|
-
episodesProcessed
|
|
622
|
-
memoriesCreated
|
|
623
|
-
digestsCreated
|
|
624
|
-
contradictionsFound
|
|
933
|
+
episodesProcessed,
|
|
934
|
+
memoriesCreated,
|
|
935
|
+
digestsCreated,
|
|
936
|
+
contradictionsFound,
|
|
625
937
|
connectionsDecayed,
|
|
626
938
|
logsCleanedUp,
|
|
939
|
+
tokensUsed: totalTokens,
|
|
940
|
+
estimatedCost: totalCost,
|
|
941
|
+
aborted,
|
|
942
|
+
abortReason,
|
|
627
943
|
};
|
|
628
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
|
+
}
|
|
629
981
|
}
|