@matthewfl/pi-contemplator 0.1.5 → 0.1.7
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/package.json +5 -5
- package/src/agents/contemplator/agent.ts +15 -35
- package/src/agents/contemplator/prompts.ts +1 -1
- package/src/commands/contemplator-view.ts +1 -1
- package/src/commands/settings.ts +1 -3
- package/src/commands/status.ts +1 -1
- package/src/config.ts +0 -3
- package/src/hooks/consolidation-trigger.ts +41 -12
- package/src/runtime.ts +5 -5
- package/src/session-ledger/progress.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "A Pi extension that keeps long-running agentic sessions on track with background memory, contemplation, and structural review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"@earendil-works/pi-tui": "*"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-agent-core": "^0.84.
|
|
54
|
-
"@earendil-works/pi-ai": "^0.84.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "^0.84.
|
|
56
|
-
"@earendil-works/pi-tui": "^0.84.
|
|
53
|
+
"@earendil-works/pi-agent-core": "^0.84.4",
|
|
54
|
+
"@earendil-works/pi-ai": "^0.84.4",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
56
|
+
"@earendil-works/pi-tui": "^0.84.4",
|
|
57
57
|
"@types/node": "^22.0.0",
|
|
58
58
|
"typebox": "^1.1.38",
|
|
59
59
|
"typescript": "^5.6.0",
|
|
@@ -21,7 +21,6 @@ import { createWorkerStallWatchdog } from "../../worker-watchdog.js";
|
|
|
21
21
|
|
|
22
22
|
interface PendingUpdate {
|
|
23
23
|
observations: string[];
|
|
24
|
-
summaries: string[];
|
|
25
24
|
reviews: string[];
|
|
26
25
|
mainAgentOutputTokens: number;
|
|
27
26
|
mainAgentToolCalls: number;
|
|
@@ -219,7 +218,6 @@ export class Contemplator {
|
|
|
219
218
|
/** Bounds retries of one poisoned memory update so future updates can run. */
|
|
220
219
|
private consecutiveFlushFailures = 0;
|
|
221
220
|
private seenObservationIds = new Set<string>();
|
|
222
|
-
private seenSummaryIds = new Set<string>();
|
|
223
221
|
private seenReviewIds = new Set<string>();
|
|
224
222
|
private inFlightReviewKeys = new Set<string>();
|
|
225
223
|
private inFlightReviewIds = new Set<string>();
|
|
@@ -304,7 +302,6 @@ export class Contemplator {
|
|
|
304
302
|
this.history = [];
|
|
305
303
|
this.pending = undefined;
|
|
306
304
|
this.seenObservationIds.clear();
|
|
307
|
-
this.seenSummaryIds.clear();
|
|
308
305
|
this.seenReviewIds.clear();
|
|
309
306
|
this.inFlightReviewKeys.clear();
|
|
310
307
|
this.inFlightReviewIds.clear();
|
|
@@ -321,7 +318,6 @@ export class Contemplator {
|
|
|
321
318
|
this.runtime.contemplatorState = {
|
|
322
319
|
running: false,
|
|
323
320
|
pendingObservations: 0,
|
|
324
|
-
pendingSummaries: 0,
|
|
325
321
|
pendingReviews: 0,
|
|
326
322
|
responsesSinceRun: 0,
|
|
327
323
|
waitingFor: "idle",
|
|
@@ -431,7 +427,6 @@ export class Contemplator {
|
|
|
431
427
|
...this.runtime.contemplatorState,
|
|
432
428
|
running: this.running,
|
|
433
429
|
pendingObservations: this.pending?.observations.length ?? 0,
|
|
434
|
-
pendingSummaries: this.pending?.summaries.length ?? 0,
|
|
435
430
|
pendingReviews: this.pending?.reviews.length ?? 0,
|
|
436
431
|
responsesSinceRun: this.turnsSinceRun,
|
|
437
432
|
waitingFor,
|
|
@@ -457,7 +452,6 @@ export class Contemplator {
|
|
|
457
452
|
this.reviewerSessions.clear();
|
|
458
453
|
resetProjection = fullProjection(entries);
|
|
459
454
|
this.seenObservationIds.clear();
|
|
460
|
-
this.seenSummaryIds.clear();
|
|
461
455
|
this.seenReviewIds.clear();
|
|
462
456
|
this.pending = undefined;
|
|
463
457
|
this.turnsSinceRun = 0;
|
|
@@ -465,7 +459,6 @@ export class Contemplator {
|
|
|
465
459
|
this.runtime.contemplatorState = {
|
|
466
460
|
running: false,
|
|
467
461
|
pendingObservations: 0,
|
|
468
|
-
pendingSummaries: 0,
|
|
469
462
|
pendingReviews: 0,
|
|
470
463
|
responsesSinceRun: 0,
|
|
471
464
|
waitingFor: "idle",
|
|
@@ -547,12 +540,10 @@ export class Contemplator {
|
|
|
547
540
|
for (const id of memoryReferenceIds(text)) coveredIds.add(id);
|
|
548
541
|
}
|
|
549
542
|
this.seenObservationIds = new Set(resetProjection.observations.filter((item) => coveredIds.has(item.id)).map((item) => item.id));
|
|
550
|
-
this.seenSummaryIds = new Set(resetProjection.summaries.filter((item) => coveredIds.has(item.id)).map((item) => item.id));
|
|
551
543
|
this.seenReviewIds = new Set((resetProjection.reviews ?? []).filter((item) => coveredIds.has(item.id)).map((item) => item.id));
|
|
552
544
|
const unprocessedObservations = resetProjection.observations.length - this.seenObservationIds.size;
|
|
553
|
-
const unprocessedSummaries = resetProjection.summaries.length - this.seenSummaryIds.size;
|
|
554
545
|
const unprocessedReviews = (resetProjection.reviews?.length ?? 0) - this.seenReviewIds.size;
|
|
555
|
-
if (unprocessedReviews > 0 || unprocessedObservations >= this.runtime.config.contemplatorMinNewObservations
|
|
546
|
+
if (unprocessedReviews > 0 || unprocessedObservations >= this.runtime.config.contemplatorMinNewObservations) {
|
|
556
547
|
this.turnsSinceRun = this.runtime.config.contemplatorMinTurns;
|
|
557
548
|
}
|
|
558
549
|
}
|
|
@@ -586,49 +577,45 @@ export class Contemplator {
|
|
|
586
577
|
}
|
|
587
578
|
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
588
579
|
const observerBacklogTokens = rawTokensSinceObservationCoverage(branchEntries);
|
|
589
|
-
|
|
580
|
+
const waitingForCapturedObserverBacklog = this.runtime.observerBacklogBlocking || (
|
|
581
|
+
!this.runtime.consolidationInFlight &&
|
|
590
582
|
observerBacklogTokens >= this.runtime.config.observeAfterTokens &&
|
|
591
|
-
|
|
592
|
-
)
|
|
583
|
+
this.runtime.lastObserverError === undefined
|
|
584
|
+
);
|
|
585
|
+
if (waitingForCapturedObserverBacklog) {
|
|
593
586
|
// A catch-up observer pipeline may append several partial batches while it
|
|
594
|
-
// drains
|
|
595
|
-
// fragments to the contemplator one at a time.
|
|
596
|
-
//
|
|
587
|
+
// drains the finite source snapshot captured at launch. Do not feed those
|
|
588
|
+
// fragments to the contemplator one at a time. Source appended concurrently
|
|
589
|
+
// belongs to the next snapshot and must not extend this waiting period.
|
|
597
590
|
this.publishState("observer");
|
|
598
591
|
debugLog("contemplator.waiting", {
|
|
599
592
|
reason: "observer_backlog",
|
|
600
593
|
observerBacklogTokens,
|
|
594
|
+
observerBacklogBlocking: this.runtime.observerBacklogBlocking,
|
|
601
595
|
observeAfterTokens: this.runtime.config.observeAfterTokens,
|
|
602
596
|
});
|
|
603
597
|
return;
|
|
604
598
|
}
|
|
605
599
|
const projection = fullProjection(branchEntries);
|
|
606
600
|
const observations = projection.observations.map((item) => `[${item.id}] ${item.content}`);
|
|
607
|
-
const summaries = projection.summaries.map((item) => `[${item.id}] ${item.content}`);
|
|
608
601
|
const reviews = projection.reviews ?? [];
|
|
609
602
|
const newObservationItems = projection.observations.filter((item) => !this.seenObservationIds.has(item.id));
|
|
610
|
-
const newSummaryItems = projection.summaries.filter((item) => !this.seenSummaryIds.has(item.id));
|
|
611
603
|
const newReviewItems = reviews.filter((item) => !this.seenReviewIds.has(item.id));
|
|
612
604
|
const newObservations = newObservationItems.map((item) => `[${item.id}] ${item.content}`);
|
|
613
|
-
const newSummaries = newSummaryItems.map((item) => `[${item.id}] ${item.content}`);
|
|
614
605
|
const newReviews = newReviewItems.map(reviewSummaryLine);
|
|
615
606
|
for (const item of newObservationItems) this.seenObservationIds.add(item.id);
|
|
616
|
-
for (const item of newSummaryItems) this.seenSummaryIds.add(item.id);
|
|
617
607
|
for (const item of newReviewItems) this.seenReviewIds.add(item.id);
|
|
618
608
|
debugLog("contemplator.update", {
|
|
619
609
|
observationCount: observations.length,
|
|
620
|
-
summaryCount: summaries.length,
|
|
621
610
|
newObservationCount: newObservations.length,
|
|
622
|
-
newSummaryCount: newSummaries.length,
|
|
623
611
|
newReviewCount: newReviews.length,
|
|
624
612
|
turnsSinceRun: this.turnsSinceRun,
|
|
625
613
|
pending: this.pending !== undefined,
|
|
626
614
|
running: this.running,
|
|
627
615
|
});
|
|
628
|
-
if (newObservations.length > 0 ||
|
|
616
|
+
if (newObservations.length > 0 || newReviews.length > 0) {
|
|
629
617
|
this.pending = {
|
|
630
618
|
observations: mergeMemoryLines(this.pending?.observations ?? [], newObservations),
|
|
631
|
-
summaries: mergeMemoryLines(this.pending?.summaries ?? [], newSummaries),
|
|
632
619
|
reviews: mergeMemoryLines(this.pending?.reviews ?? [], newReviews),
|
|
633
620
|
mainAgentOutputTokens: assistantOutputTokens(branchEntries),
|
|
634
621
|
mainAgentToolCalls: assistantToolCallCount(branchEntries),
|
|
@@ -645,7 +632,7 @@ export class Contemplator {
|
|
|
645
632
|
this.pending.mainAgentOutputTokens = assistantOutputTokens(branchEntries);
|
|
646
633
|
this.pending.mainAgentToolCalls = assistantToolCallCount(branchEntries);
|
|
647
634
|
this.pending.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntries);
|
|
648
|
-
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations
|
|
635
|
+
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations;
|
|
649
636
|
if (this.probeCooldownPendingIds.size > 0) {
|
|
650
637
|
this.publishState("probe");
|
|
651
638
|
debugLog("contemplator.waiting", { reason: "probe_delivery", pendingProbeCount: this.probeCooldownPendingIds.size });
|
|
@@ -658,14 +645,12 @@ export class Contemplator {
|
|
|
658
645
|
turnsSinceRun: this.turnsSinceRun,
|
|
659
646
|
minTurns: this.runtime.config.contemplatorMinTurns,
|
|
660
647
|
minNewObservations: this.runtime.config.contemplatorMinNewObservations,
|
|
661
|
-
minNewSummaries: this.runtime.config.contemplatorMinNewSummaries,
|
|
662
648
|
});
|
|
663
649
|
return;
|
|
664
650
|
}
|
|
665
651
|
this.publishState(this.running ? "running" : "ready");
|
|
666
652
|
debugLog("contemplator.triggered", {
|
|
667
653
|
pendingObservationCount: this.pending.observations.length,
|
|
668
|
-
pendingSummaryCount: this.pending.summaries.length,
|
|
669
654
|
pendingReviewCount: this.pending.reviews.length,
|
|
670
655
|
turnsSinceRun: this.turnsSinceRun,
|
|
671
656
|
});
|
|
@@ -702,7 +687,6 @@ export class Contemplator {
|
|
|
702
687
|
this.publishState("running", { lastStartedAt: startedAt, lastError: undefined });
|
|
703
688
|
debugLog("contemplator.start", {
|
|
704
689
|
newObservationCount: update.observations.length,
|
|
705
|
-
newSummaryCount: update.summaries.length,
|
|
706
690
|
newReviewCount: update.reviews.length,
|
|
707
691
|
historyMessageCount: this.history.length,
|
|
708
692
|
});
|
|
@@ -724,14 +708,13 @@ export class Contemplator {
|
|
|
724
708
|
const pending = this.pending as PendingUpdate | undefined;
|
|
725
709
|
this.pending = {
|
|
726
710
|
observations: mergeMemoryLines(pending?.observations ?? [], update.observations),
|
|
727
|
-
summaries: mergeMemoryLines(pending?.summaries ?? [], update.summaries),
|
|
728
711
|
reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
|
|
729
712
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
730
713
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
731
714
|
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
732
715
|
};
|
|
733
716
|
} else {
|
|
734
|
-
debugLog("contemplator.poisoned_update_released", { reason: resolved.reason, observationCount: update.observations.length,
|
|
717
|
+
debugLog("contemplator.poisoned_update_released", { reason: resolved.reason, observationCount: update.observations.length, reviewCount: update.reviews.length });
|
|
735
718
|
this.consecutiveFlushFailures = 0;
|
|
736
719
|
}
|
|
737
720
|
this.turnsSinceRun = 0; // Back off until fresh primary responses arrive; never retry every checkpoint.
|
|
@@ -756,7 +739,6 @@ export class Contemplator {
|
|
|
756
739
|
const reviewerEnabled = this.runtime.config.reviewerEnabled;
|
|
757
740
|
const updateSections: string[] = [];
|
|
758
741
|
if (update.observations.length > 0) updateSections.push(`OBSERVATIONS:\n${update.observations.join("\n")}`);
|
|
759
|
-
if (update.summaries.length > 0) updateSections.push(`SUMMARIES:\n${update.summaries.join("\n")}`);
|
|
760
742
|
if (update.reviews.length > 0) updateSections.push(`REVIEWS:\n${update.reviews.join("\n")}`);
|
|
761
743
|
const updateBody = updateSections.length > 0 ? updateSections.join("\n\n") : "(no new memories)";
|
|
762
744
|
const finalActionNames = reviewerEnabled
|
|
@@ -922,14 +904,13 @@ export class Contemplator {
|
|
|
922
904
|
const pending = this.pending as PendingUpdate | undefined;
|
|
923
905
|
this.pending = {
|
|
924
906
|
observations: mergeMemoryLines(pending?.observations ?? [], update.observations),
|
|
925
|
-
summaries: mergeMemoryLines(pending?.summaries ?? [], update.summaries),
|
|
926
907
|
reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
|
|
927
908
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
928
909
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
929
910
|
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
930
911
|
};
|
|
931
912
|
} else {
|
|
932
|
-
debugLog("contemplator.poisoned_update_released", { reason: failureMessage, observationCount: update.observations.length,
|
|
913
|
+
debugLog("contemplator.poisoned_update_released", { reason: failureMessage, observationCount: update.observations.length, reviewCount: update.reviews.length });
|
|
933
914
|
this.consecutiveFlushFailures = 0;
|
|
934
915
|
}
|
|
935
916
|
this.turnsSinceRun = 0; // Back off until fresh primary responses arrive; never retry every checkpoint.
|
|
@@ -947,8 +928,7 @@ export class Contemplator {
|
|
|
947
928
|
const waitingForProbe = this.probeCooldownPendingIds.size > 0;
|
|
948
929
|
const pendingHasEnoughMemories = this.pending !== undefined && (
|
|
949
930
|
this.pending.reviews.length > 0 ||
|
|
950
|
-
this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations
|
|
951
|
-
this.pending.summaries.length >= this.runtime.config.contemplatorMinNewSummaries
|
|
931
|
+
this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations
|
|
952
932
|
);
|
|
953
933
|
const waitingFor = waitingForProbe
|
|
954
934
|
? "probe"
|
|
@@ -6,7 +6,7 @@ export function buildContemplatorSystemPrompt(
|
|
|
6
6
|
|
|
7
7
|
Neither you nor the primary agent should be assumed to know the correct solution. You are jointly exploring a problem space from different perspectives. The primary agent interacts with the actual environment and carries out the work. You maintain a longer-term view of the reasoning, evidence, assumptions, alternatives, unresolved questions${reviewerEnabled ? ", and recurring structural patterns" : ""} that emerge over time.
|
|
8
8
|
|
|
9
|
-
You receive incremental observations
|
|
9
|
+
You receive incremental observations about primary-agent activity. Pay extra attention to memories about the user’s intent, priorities, constraints, corrections, and desired outcome. Older summarized memories remain available through search_memories and recall when you need historical context, but summary maintenance is not itself a new event.
|
|
10
10
|
|
|
11
11
|
You see only the memory ledger, not the primary agent’s live activity. Your understanding may be incomplete or slightly stale. Do not infer inactivity, failure, or lack of progress from missing recent results. A result may simply not have reached memory yet.
|
|
12
12
|
|
|
@@ -59,7 +59,7 @@ function renderMessage(message: StoredMessage, compacted: boolean): string {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function liveStateLine(state: ContemplatorRunState): string {
|
|
62
|
-
const pending = `${state.pendingObservations} observations / ${state.
|
|
62
|
+
const pending = `${state.pendingObservations} observations / ${state.pendingReviews} reviews pending`;
|
|
63
63
|
const timing = `Last start: ${state.lastStartedAt === undefined ? "not run this launch" : new Date(state.lastStartedAt).toISOString()} · Last end: ${state.lastCompletedAt === undefined ? "not completed this launch" : new Date(state.lastCompletedAt).toISOString()}`;
|
|
64
64
|
const error = state.lastError ? `\nLast error: ${state.lastError}` : "";
|
|
65
65
|
if (state.running) return `LIVE · running for ${Math.max(0, Math.floor((Date.now() - (state.lastStartedAt ?? Date.now())) / 60_000))}m · ${pending}\n${timing}${error}`;
|
package/src/commands/settings.ts
CHANGED
|
@@ -10,7 +10,7 @@ type ModelRegistryLike = {
|
|
|
10
10
|
getAll(): Array<{ provider: string; id: string }>;
|
|
11
11
|
find?(provider: string, id: string): { contextWindow?: number } | undefined;
|
|
12
12
|
};
|
|
13
|
-
type NumberSetting = "observeAfterTokens" | "compactAfterTokens" | "observerChunkMaxTokens" | "newMemoryPoolMaxTokens" | "oldMemoryPoolTargetTokens" | "agentMaxTurns" | "contemplatorMinNewObservations" | "
|
|
13
|
+
type NumberSetting = "observeAfterTokens" | "compactAfterTokens" | "observerChunkMaxTokens" | "newMemoryPoolMaxTokens" | "oldMemoryPoolTargetTokens" | "agentMaxTurns" | "contemplatorMinNewObservations" | "contemplatorMinTurns" | "summarizerRetriggerTokens" | "summarizerSamplingThresholdTokens";
|
|
14
14
|
type BooleanSetting = "contemplatorEnabled" | "showContemplatorMessages" | "reviewerEnabled" | "summarizerEnabled" | "compactionObserverEnabled" | "showWorkerNotifications" | "passive" | "debugLog";
|
|
15
15
|
|
|
16
16
|
function modelLabel(model: ConfiguredModel | undefined): string {
|
|
@@ -216,7 +216,6 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
216
216
|
`Contemplator model: ${hasOverride(settings, "contemplatorModel") ? modelLabel(runtime.config.contemplatorModel) : `${modelLabel(runtime.getDefaultConfig().contemplatorModel)} (default)`}`,
|
|
217
217
|
`Show contemplator messages: ${scalarLabel(runtime, "showContemplatorMessages")}`,
|
|
218
218
|
`Contemplator new-observation trigger (count): ${scalarLabel(runtime, "contemplatorMinNewObservations")}`,
|
|
219
|
-
`Contemplator new-summary trigger (count): ${scalarLabel(runtime, "contemplatorMinNewSummaries")}`,
|
|
220
219
|
`Contemplator response spacing (count): ${scalarLabel(runtime, "contemplatorMinTurns")}`,
|
|
221
220
|
`Summarizer enabled: ${scalarLabel(runtime, "summarizerEnabled")}`,
|
|
222
221
|
`New memory pool protection budget (tokens): ${scalarLabel(runtime, "newMemoryPoolMaxTokens")}`,
|
|
@@ -272,7 +271,6 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
272
271
|
["New memory pool protection budget (tokens):", "newMemoryPoolMaxTokens", "New memory pool protection budget (tokens)"],
|
|
273
272
|
["Old memory pool target (tokens, advisory):", "oldMemoryPoolTargetTokens", "Old memory pool target (tokens, advisory)"],
|
|
274
273
|
["Contemplator new-observation trigger (count):", "contemplatorMinNewObservations", "Contemplator new-observation trigger (count)"],
|
|
275
|
-
["Contemplator new-summary trigger (count):", "contemplatorMinNewSummaries", "Contemplator new-summary trigger (count)"],
|
|
276
274
|
["Contemplator response spacing (count):", "contemplatorMinTurns", "Contemplator response spacing (count)"],
|
|
277
275
|
["Summarizer old-pool retrigger growth (tokens):", "summarizerRetriggerTokens", "Summarizer old-pool retrigger growth (tokens)"],
|
|
278
276
|
["Summarizer input cap before sampling (tokens):", "summarizerSamplingThresholdTokens", "Summarizer input cap before sampling (tokens)"],
|
package/src/commands/status.ts
CHANGED
|
@@ -130,7 +130,7 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
130
130
|
`Cumulative agent time: ${formatDuration(agentActiveTimeMs(entries))}`,
|
|
131
131
|
`Observe source during compaction: ${runtime.config.compactionObserverEnabled === false ? "disabled" : "enabled"}`,
|
|
132
132
|
`Contemplator: ${runtime.config.contemplatorEnabled ? "enabled" : "disabled"}`,
|
|
133
|
-
`Contemplator trigger: ${runtime.contemplatorState.pendingObservations} observations / ${runtime.contemplatorState.
|
|
133
|
+
`Contemplator trigger: ${runtime.contemplatorState.pendingObservations} observations / ${runtime.contemplatorState.pendingReviews} reviews pending; ${runtime.contemplatorState.responsesSinceRun} / ${runtime.config.contemplatorMinTurns} primary responses; ${contemplatorWaitingLabel(runtime.contemplatorState.waitingFor)}`,
|
|
134
134
|
`Contemplator model: ${runtime.config.contemplatorModel ? `${runtime.config.contemplatorModel.provider}/${runtime.config.contemplatorModel.id}` : "current session model"}`,
|
|
135
135
|
`Contemplator messages: ${runtime.config.showContemplatorMessages ? "visible" : "hidden"}`,
|
|
136
136
|
`Structural reviewer: ${runtime.config.reviewerEnabled === false ? "disabled" : "enabled"}`,
|
package/src/config.ts
CHANGED
|
@@ -60,7 +60,6 @@ export interface Config {
|
|
|
60
60
|
/** Optional model override used only by short-lived structural reviewers. */
|
|
61
61
|
reviewerModel?: ConfiguredModel;
|
|
62
62
|
contemplatorMinNewObservations: number;
|
|
63
|
-
contemplatorMinNewSummaries: number;
|
|
64
63
|
/** Minimum primary-model responses after contemplator completion, or after delivery of its probe, before the next run. */
|
|
65
64
|
contemplatorMinTurns: number;
|
|
66
65
|
/** Stateless loss-aware summarizer for the old memory pool. */
|
|
@@ -87,7 +86,6 @@ export const DEFAULTS: Config = {
|
|
|
87
86
|
showContemplatorMessages: true,
|
|
88
87
|
reviewerEnabled: true,
|
|
89
88
|
contemplatorMinNewObservations: 8,
|
|
90
|
-
contemplatorMinNewSummaries: 1,
|
|
91
89
|
contemplatorMinTurns: 10,
|
|
92
90
|
summarizerEnabled: true,
|
|
93
91
|
summarizerRetriggerTokens: 2_000,
|
|
@@ -210,7 +208,6 @@ function normalizeSettingsConfig(value: Record<string, unknown>): Partial<Config
|
|
|
210
208
|
"oldMemoryPoolTargetTokens",
|
|
211
209
|
"agentMaxTurns",
|
|
212
210
|
"contemplatorMinNewObservations",
|
|
213
|
-
"contemplatorMinNewSummaries",
|
|
214
211
|
"contemplatorMinTurns",
|
|
215
212
|
"summarizerRetriggerTokens",
|
|
216
213
|
"summarizerSamplingThresholdTokens",
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
observationToSummaryLine,
|
|
19
19
|
partitionMemoryPools,
|
|
20
20
|
rawTokensSinceObservationCoverage,
|
|
21
|
+
rawTokensSinceObservationCoverageThrough,
|
|
21
22
|
summaryToSummaryLine,
|
|
22
23
|
type Entry,
|
|
23
24
|
} from "../session-ledger/index.js";
|
|
@@ -51,8 +52,12 @@ export function createSummarizerStallWatchdog(
|
|
|
51
52
|
return watchdog;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function sourceEntriesAfter(entries: Entry[], index: number): Entry[] {
|
|
55
|
-
|
|
55
|
+
function sourceEntriesAfter(entries: Entry[], index: number, throughEntryId?: string): Entry[] {
|
|
56
|
+
const throughIndex = throughEntryId === undefined
|
|
57
|
+
? entries.length - 1
|
|
58
|
+
: entries.findIndex((entry) => entry.id === throughEntryId);
|
|
59
|
+
if (throughIndex < 0 || throughIndex <= index) return [];
|
|
60
|
+
return entries.slice(index + 1, throughIndex + 1).filter(isSourceEntry);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
function appendEntry(pi: ExtensionAPI, customType: string, data: unknown): void {
|
|
@@ -161,6 +166,7 @@ function maybeLaunchConsolidation(pi: ExtensionAPI, runtime: Runtime, ctx: Conso
|
|
|
161
166
|
};
|
|
162
167
|
|
|
163
168
|
const sessionMetadata = debugSessionMetadata(ctx);
|
|
169
|
+
const launchGeneration = runtime.getContextGeneration();
|
|
164
170
|
const task = runtime.launchConsolidationTask(ctx, async () => withDebugLogContext({
|
|
165
171
|
enabled: runtime.config.debugLog === true,
|
|
166
172
|
cwd: ctx.cwd,
|
|
@@ -174,7 +180,15 @@ function maybeLaunchConsolidation(pi: ExtensionAPI, runtime: Runtime, ctx: Conso
|
|
|
174
180
|
// degraded mode rather than leaving all advisory work gated forever. Future
|
|
175
181
|
// primary activity still retries the observer.
|
|
176
182
|
void task.then(() => {
|
|
177
|
-
if (
|
|
183
|
+
if (launchGeneration !== runtime.getContextGeneration()) return;
|
|
184
|
+
if (runtime.lastObserverError) {
|
|
185
|
+
runtime.notifyMemoryUpdate(ctx);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Source appended while the finite catch-up snapshot was running belongs
|
|
189
|
+
// to a later snapshot. Recheck after the lock is released; the completed
|
|
190
|
+
// pipeline has already given the contemplator its memory-update opportunity.
|
|
191
|
+
maybeLaunchConsolidation(pi, runtime, ctx);
|
|
178
192
|
});
|
|
179
193
|
}
|
|
180
194
|
|
|
@@ -225,19 +239,25 @@ export async function runConsolidationPipeline(
|
|
|
225
239
|
const resolveModel = makeModelResolver(runtime, ctx);
|
|
226
240
|
const contextGeneration = runtime.getContextGeneration();
|
|
227
241
|
|
|
242
|
+
const pipelineEntries = options.observerEntries ?? (ctx.sessionManager.getBranch() as Entry[]);
|
|
243
|
+
const initialCoverage = latestCoverageIndex(pipelineEntries, OM_OBSERVATIONS_RECORDED);
|
|
244
|
+
const catchUpThroughId = sourceEntriesAfter(pipelineEntries, initialCoverage).at(-1)?.id;
|
|
228
245
|
const beforeFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
229
246
|
runtime.consolidationPhase = "observer";
|
|
247
|
+
runtime.observerBacklogBlocking = catchUpThroughId !== undefined;
|
|
230
248
|
try {
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
249
|
+
// Drain only the finite source snapshot captured at pipeline launch, using
|
|
250
|
+
// bounded oldest-first chunks. Concurrent source belongs to a later pipeline,
|
|
251
|
+
// so a fast primary agent cannot indefinitely extend this blocking backlog.
|
|
252
|
+
// A static compaction snapshot is intentionally processed only once. Coverage
|
|
253
|
+
// must advance on every iteration, otherwise stop rather than spin.
|
|
235
254
|
while (true) {
|
|
236
255
|
const beforeEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
237
256
|
const beforeCoverage = latestCoverageIndex(beforeEntries, OM_OBSERVATIONS_RECORDED);
|
|
238
257
|
const observerOutcome = await runObserverStage(pi, runtime, ctx, resolveModel, {
|
|
239
258
|
force: options.forceObserver === true,
|
|
240
259
|
entries: options.observerEntries,
|
|
260
|
+
throughSourceEntryId: options.observerEntries ? undefined : catchUpThroughId,
|
|
241
261
|
contextGeneration,
|
|
242
262
|
});
|
|
243
263
|
if (observerOutcome === "abort") return;
|
|
@@ -245,13 +265,19 @@ export async function runConsolidationPipeline(
|
|
|
245
265
|
|
|
246
266
|
const afterEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
247
267
|
const afterCoverage = latestCoverageIndex(afterEntries, OM_OBSERVATIONS_RECORDED);
|
|
248
|
-
const remainingTokens =
|
|
268
|
+
const remainingTokens = catchUpThroughId === undefined
|
|
269
|
+
? 0
|
|
270
|
+
: rawTokensSinceObservationCoverageThrough(afterEntries, catchUpThroughId);
|
|
249
271
|
if (afterCoverage <= beforeCoverage || remainingTokens < runtime.config.observeAfterTokens) break;
|
|
250
272
|
debugLog("observer.backlog_continue", { remainingTokens, afterCoverage });
|
|
251
273
|
}
|
|
252
274
|
} catch (error) {
|
|
253
275
|
debugLog("observer.error", { errorMessage: runtime.recordConsolidationStageError(ctx, "observer", error) });
|
|
254
276
|
return;
|
|
277
|
+
} finally {
|
|
278
|
+
// New source appended after catchUpThroughId was never part of this
|
|
279
|
+
// pipeline's blocking backlog. Clear before notifying the contemplator.
|
|
280
|
+
if (contextGeneration === runtime.getContextGeneration()) runtime.observerBacklogBlocking = false;
|
|
255
281
|
}
|
|
256
282
|
const afterFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
257
283
|
const beforeIds = new Set(beforeFold.observations.map((item) => item.id));
|
|
@@ -367,7 +393,8 @@ export function scheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: Cons
|
|
|
367
393
|
runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun!, status: "completed", summary };
|
|
368
394
|
debugLog("summarizer.appended", { summaries: result.commit.summaries.length, consumed: result.commit.metrics.consumedMemoryCount, sampled: result.sample?.sampled ?? false });
|
|
369
395
|
if (shouldNotifyWorker(runtime, ctx)) ctx.ui?.notify(`pi-contemplator: summarizer completed — ${summary}`, "info");
|
|
370
|
-
|
|
396
|
+
// Summaries compact older memories; they are not new events and must not
|
|
397
|
+
// wake or enter the contemplator's incremental update stream.
|
|
371
398
|
} else {
|
|
372
399
|
successfullyCompleted = true;
|
|
373
400
|
runtime.lastSummarizerRun = { ...runtime.lastSummarizerRun!, status: "completed", summary: "No safe summaries were created." };
|
|
@@ -408,10 +435,12 @@ async function runObserverStage(
|
|
|
408
435
|
runtime: Runtime,
|
|
409
436
|
ctx: ConsolidationCtx,
|
|
410
437
|
resolveModel: (stage: "observer") => Promise<ResolvedModel | undefined>,
|
|
411
|
-
options: { force?: boolean; entries?: Entry[]; contextGeneration?: number } = {},
|
|
438
|
+
options: { force?: boolean; entries?: Entry[]; throughSourceEntryId?: string; contextGeneration?: number } = {},
|
|
412
439
|
): Promise<StageOutcome> {
|
|
413
440
|
const entries = options.entries ?? (ctx.sessionManager.getBranch() as Entry[]);
|
|
414
|
-
const tokens =
|
|
441
|
+
const tokens = options.throughSourceEntryId === undefined
|
|
442
|
+
? rawTokensSinceObservationCoverage(entries)
|
|
443
|
+
: rawTokensSinceObservationCoverageThrough(entries, options.throughSourceEntryId);
|
|
415
444
|
if (!options.force && tokens < runtime.config.observeAfterTokens) return "continue";
|
|
416
445
|
|
|
417
446
|
// Resolve the model before building the chunk: the default chunk cap
|
|
@@ -424,7 +453,7 @@ async function runObserverStage(
|
|
|
424
453
|
}
|
|
425
454
|
|
|
426
455
|
const lastCoverageIdx = latestCoverageIndex(entries, OM_OBSERVATIONS_RECORDED);
|
|
427
|
-
const backlogEntries = sourceEntriesAfter(entries, lastCoverageIdx);
|
|
456
|
+
const backlogEntries = sourceEntriesAfter(entries, lastCoverageIdx, options.throughSourceEntryId);
|
|
428
457
|
|
|
429
458
|
// Budget the text that is actually sent to the observer, including source
|
|
430
459
|
// labels and rendered message content. Complete entries are kept intact.
|
package/src/runtime.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type SessionSettings = Partial<Pick<Config,
|
|
|
25
25
|
| "compactAfterTokensMode" | "compactAfterTokensRatio"
|
|
26
26
|
| "newMemoryPoolMaxTokens" | "oldMemoryPoolTargetTokens" | "agentMaxTurns"
|
|
27
27
|
| "showWorkerNotifications" | "passive" | "compactionObserverEnabled" | "contemplatorEnabled" | "showContemplatorMessages" | "reviewerEnabled"
|
|
28
|
-
| "contemplatorMinNewObservations" | "
|
|
28
|
+
| "contemplatorMinNewObservations" | "contemplatorMinTurns"
|
|
29
29
|
| "summarizerEnabled" | "summarizerRetriggerTokens" | "summarizerSamplingThresholdTokens"
|
|
30
30
|
| "debugLog"
|
|
31
31
|
>> & {
|
|
@@ -80,7 +80,6 @@ export interface SummarizerRunView {
|
|
|
80
80
|
export interface ContemplatorRunState {
|
|
81
81
|
running: boolean;
|
|
82
82
|
pendingObservations: number;
|
|
83
|
-
pendingSummaries: number;
|
|
84
83
|
pendingReviews: number;
|
|
85
84
|
/** Completed primary-model responses since the current completion/probe-delivery spacing anchor. */
|
|
86
85
|
responsesSinceRun: number;
|
|
@@ -140,7 +139,7 @@ export function computeSessionSettings(entries: readonly unknown[]): SessionSett
|
|
|
140
139
|
const numberKeys = [
|
|
141
140
|
"observeAfterTokens", "observerChunkMaxTokens", "compactAfterTokens",
|
|
142
141
|
"newMemoryPoolMaxTokens", "oldMemoryPoolTargetTokens", "agentMaxTurns",
|
|
143
|
-
"contemplatorMinNewObservations", "
|
|
142
|
+
"contemplatorMinNewObservations", "contemplatorMinTurns",
|
|
144
143
|
"summarizerRetriggerTokens", "summarizerSamplingThresholdTokens",
|
|
145
144
|
] as const;
|
|
146
145
|
for (const key of booleanKeys) if (typeof data[key] === "boolean") restored[key] = data[key];
|
|
@@ -182,6 +181,8 @@ export class Runtime {
|
|
|
182
181
|
private settingsUpdateListener: ((ctx: MemoryUpdateCtx, settings: SettingsUpdate) => void) | undefined;
|
|
183
182
|
private contextGeneration = 0;
|
|
184
183
|
consolidationPhase: ConsolidationPhase | undefined;
|
|
184
|
+
/** True only while the observer is draining the finite source snapshot captured at pipeline start. */
|
|
185
|
+
observerBacklogBlocking = false;
|
|
185
186
|
compactInFlight = false;
|
|
186
187
|
compactRequested = false;
|
|
187
188
|
/** Agent-authored instructions to deliver after an explicit compact_context request. */
|
|
@@ -207,7 +208,6 @@ export class Runtime {
|
|
|
207
208
|
contemplatorState: ContemplatorRunState = {
|
|
208
209
|
running: false,
|
|
209
210
|
pendingObservations: 0,
|
|
210
|
-
pendingSummaries: 0,
|
|
211
211
|
pendingReviews: 0,
|
|
212
212
|
responsesSinceRun: 0,
|
|
213
213
|
waitingFor: "idle",
|
|
@@ -274,6 +274,7 @@ export class Runtime {
|
|
|
274
274
|
this.consolidationInFlight = false;
|
|
275
275
|
this.consolidationPromise = null;
|
|
276
276
|
this.consolidationPhase = undefined;
|
|
277
|
+
this.observerBacklogBlocking = false;
|
|
277
278
|
this.summarizerInFlight = false;
|
|
278
279
|
this.summarizerPromise = null;
|
|
279
280
|
this.reviewInFlight = false;
|
|
@@ -296,7 +297,6 @@ export class Runtime {
|
|
|
296
297
|
this.contemplatorState = {
|
|
297
298
|
running: false,
|
|
298
299
|
pendingObservations: 0,
|
|
299
|
-
pendingSummaries: 0,
|
|
300
300
|
pendingReviews: 0,
|
|
301
301
|
responsesSinceRun: 0,
|
|
302
302
|
waitingFor: "idle",
|
|
@@ -138,6 +138,23 @@ export function rawTokensSinceObservationCoverage(entries: Entry[]): number {
|
|
|
138
138
|
return rawTokensSinceCoverage(entries, OM_OBSERVATIONS_RECORDED);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Sum uncovered observer source only through a fixed branch entry. Source
|
|
143
|
+
* appended after that boundary is intentionally excluded so one catch-up pass
|
|
144
|
+
* has a finite backlog even while the primary agent keeps producing output.
|
|
145
|
+
*/
|
|
146
|
+
export function rawTokensSinceObservationCoverageThrough(entries: Entry[], throughEntryId: string): number {
|
|
147
|
+
const throughIndex = entryIndexForId(entries, throughEntryId);
|
|
148
|
+
if (throughIndex < 0) return 0;
|
|
149
|
+
const coverageIndex = latestCoverageIndex(entries, OM_OBSERVATIONS_RECORDED);
|
|
150
|
+
if (coverageIndex >= throughIndex) return 0;
|
|
151
|
+
let total = 0;
|
|
152
|
+
for (let i = Math.max(0, coverageIndex + 1); i <= throughIndex; i++) {
|
|
153
|
+
if (isSourceEntry(entries[i])) total += estimateEntryTokens(entries[i]);
|
|
154
|
+
}
|
|
155
|
+
return total;
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
export function findLastCompactionIndex(entries: Entry[]): number {
|
|
142
159
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
143
160
|
if (entries[i].type === "compaction") return i;
|