@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthewfl/pi-contemplator",
3
- "version": "0.1.5",
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.3",
54
- "@earendil-works/pi-ai": "^0.84.3",
55
- "@earendil-works/pi-coding-agent": "^0.84.3",
56
- "@earendil-works/pi-tui": "^0.84.3",
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 || unprocessedSummaries >= this.runtime.config.contemplatorMinNewSummaries) {
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
- if (
580
+ const waitingForCapturedObserverBacklog = this.runtime.observerBacklogBlocking || (
581
+ !this.runtime.consolidationInFlight &&
590
582
  observerBacklogTokens >= this.runtime.config.observeAfterTokens &&
591
- (this.runtime.consolidationInFlight || this.runtime.lastObserverError === undefined)
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 old source. Do not let primary-agent checkpoints feed those
595
- // fragments to the contemplator one at a time. The pipeline emits one
596
- // memory update after the backlog falls below the observer trigger.
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 || newSummaries.length > 0 || newReviews.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 || this.pending.summaries.length >= this.runtime.config.contemplatorMinNewSummaries;
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, summaryCount: update.summaries.length, reviewCount: update.reviews.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, summaryCount: update.summaries.length, reviewCount: update.reviews.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 and cited summaries produced by other agent loops. Some memories summarize user messages. Pay extra attention to memories about the user’s intent, priorities, constraints, corrections, and desired outcome.
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.pendingSummaries} summaries / ${state.pendingReviews} reviews pending`;
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}`;
@@ -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" | "contemplatorMinNewSummaries" | "contemplatorMinTurns" | "summarizerRetriggerTokens" | "summarizerSamplingThresholdTokens";
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)"],
@@ -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.pendingSummaries} summaries / ${runtime.contemplatorState.pendingReviews} reviews pending; ${runtime.contemplatorState.responsesSinceRun} / ${runtime.config.contemplatorMinTurns} primary responses; ${contemplatorWaitingLabel(runtime.contemplatorState.waitingFor)}`,
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
- return entries.slice(index + 1).filter(isSourceEntry);
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 (runtime.lastObserverError) runtime.notifyMemoryUpdate(ctx);
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
- // A large backlog is drained in bounded, oldest-first chunks. The normal
232
- // trigger threshold controls when the batch stops; a static compaction
233
- // snapshot is intentionally processed only once. Coverage must advance on
234
- // every iteration, otherwise stop rather than spin on a failed chunk.
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 = rawTokensSinceObservationCoverage(afterEntries);
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
- runtime.notifyMemoryUpdate(ctx);
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 = rawTokensSinceObservationCoverage(entries);
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" | "contemplatorMinNewSummaries" | "contemplatorMinTurns"
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", "contemplatorMinNewSummaries", "contemplatorMinTurns",
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;