@matthewfl/pi-contemplator 0.1.8 → 0.1.9

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.8",
3
+ "version": "0.1.9",
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",
@@ -242,7 +242,6 @@ export async function runConsolidationPipeline(
242
242
  const pipelineEntries = options.observerEntries ?? (ctx.sessionManager.getBranch() as Entry[]);
243
243
  const initialCoverage = latestCoverageIndex(pipelineEntries, OM_OBSERVATIONS_RECORDED);
244
244
  const catchUpThroughId = sourceEntriesAfter(pipelineEntries, initialCoverage).at(-1)?.id;
245
- const beforeFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
246
245
  runtime.consolidationPhase = "observer";
247
246
  runtime.observerBacklogBlocking = catchUpThroughId !== undefined;
248
247
  try {
@@ -254,6 +253,7 @@ export async function runConsolidationPipeline(
254
253
  while (true) {
255
254
  const beforeEntries = ctx.sessionManager.getBranch() as Entry[];
256
255
  const beforeCoverage = latestCoverageIndex(beforeEntries, OM_OBSERVATIONS_RECORDED);
256
+ const beforeObservationIds = new Set(foldLedger(beforeEntries).observations.map((memory) => memory.id));
257
257
  const observerOutcome = await runObserverStage(pi, runtime, ctx, resolveModel, {
258
258
  force: options.forceObserver === true,
259
259
  entries: options.observerEntries,
@@ -261,9 +261,18 @@ export async function runConsolidationPipeline(
261
261
  contextGeneration,
262
262
  });
263
263
  if (observerOutcome === "abort") return;
264
- if (options.observerEntries) break;
265
264
 
266
265
  const afterEntries = ctx.sessionManager.getBranch() as Entry[];
266
+ if (shouldScheduleSummarizerFromObserver(options)) {
267
+ const passAddedObservations = foldLedger(afterEntries).observations.some((memory) => !beforeObservationIds.has(memory.id));
268
+ // A finite observer backlog can take hours to drain while the primary
269
+ // agent is idle. Let each completed chunk feed the independent
270
+ // summarizer; waiting for the entire observer pipeline can otherwise
271
+ // leave an oversized old pool untouched indefinitely.
272
+ if (passAddedObservations) scheduleSummarizer(pi, runtime, ctx);
273
+ }
274
+ if (options.observerEntries) break;
275
+
267
276
  const afterCoverage = latestCoverageIndex(afterEntries, OM_OBSERVATIONS_RECORDED);
268
277
  const remainingTokens = catchUpThroughId === undefined
269
278
  ? 0
@@ -279,10 +288,6 @@ export async function runConsolidationPipeline(
279
288
  // pipeline's blocking backlog. Clear before notifying the contemplator.
280
289
  if (contextGeneration === runtime.getContextGeneration()) runtime.observerBacklogBlocking = false;
281
290
  }
282
- const afterFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
283
- const beforeIds = new Set(beforeFold.observations.map((item) => item.id));
284
- const added = afterFold.observations.filter((item) => !beforeIds.has(item.id));
285
- if (added.length > 0 && shouldScheduleSummarizerFromObserver(options)) scheduleSummarizer(pi, runtime, ctx);
286
291
  if (contextGeneration === runtime.getContextGeneration()) runtime.notifyMemoryUpdate?.(ctx);
287
292
  }
288
293
 
@@ -312,10 +317,14 @@ export function summarizerTriggerAfterRun(
312
317
  targetTokens: number,
313
318
  postRunOldTokens: number,
314
319
  retriggerTokens: number,
320
+ failedAttemptStartOldTokens?: number,
315
321
  ): number | undefined {
316
- return successfullyCompleted
317
- ? nextSummarizerTriggerTokens(targetTokens, postRunOldTokens, retriggerTokens)
318
- : currentTriggerTokens;
322
+ if (successfullyCompleted) return nextSummarizerTriggerTokens(targetTokens, postRunOldTokens, retriggerTokens);
323
+ if (failedAttemptStartOldTokens === undefined) return currentTriggerTokens;
324
+ // Back off a model pass from the pool it actually received. Growth that
325
+ // arrived while it was running must count toward the retry rather than being
326
+ // swallowed by a threshold based on the larger post-run pool.
327
+ return Math.max(currentTriggerTokens ?? targetTokens, failedAttemptStartOldTokens + retriggerTokens);
319
328
  }
320
329
 
321
330
  function syncAndScheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: ConsolidationCtx): void {
@@ -324,14 +333,27 @@ function syncAndScheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: Cons
324
333
  }
325
334
 
326
335
  export function scheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: ConsolidationCtx): void {
327
- if (runtime.config.passive || !runtime.config.summarizerEnabled || runtime.summarizerInFlight) return;
336
+ if (runtime.config.passive || !runtime.config.summarizerEnabled) return;
337
+ if (runtime.summarizerInFlight) {
338
+ // Do not lose observer/activity checkpoints that arrive during a long run.
339
+ // The tracked task rechecks once its single-flight lock has been released.
340
+ runtime.summarizerRecheckPending = true;
341
+ return;
342
+ }
343
+ runtime.summarizerRecheckPending = false;
328
344
  const entries = ctx.sessionManager.getBranch() as Entry[];
329
345
  const pools = currentMemoryPools(runtime, entries);
330
- if (pools.oldTokens <= summarizerTriggerTokens(runtime)) return;
346
+ const targetTokens = runtime.config.oldMemoryPoolTargetTokens;
347
+ const nextTriggerTokens = summarizerTriggerTokens(runtime);
348
+ // Initial/healthy eligibility strictly exceeds the advisory target. Once a
349
+ // growth backoff is installed, reaching that +N threshold is sufficient; it
350
+ // must not require an accidental extra token beyond the configured amount.
351
+ if (pools.oldTokens <= targetTokens || nextTriggerTokens > targetTokens && pools.oldTokens < nextTriggerTokens) return;
352
+ const runStartOldTokens = pools.oldTokens;
331
353
  const generation = runtime.getContextGeneration();
332
354
  const runId = `summarizer-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
333
355
  const sessionMetadata = debugSessionMetadata(ctx);
334
- void runtime.launchSummarizerTask(ctx, async () => withDebugLogContext({
356
+ const task = runtime.launchSummarizerTask(ctx, async () => withDebugLogContext({
335
357
  enabled: runtime.config.debugLog === true,
336
358
  cwd: ctx.cwd,
337
359
  ...sessionMetadata,
@@ -415,19 +437,28 @@ export function scheduleSummarizer(pi: ExtensionAPI, runtime: Runtime, ctx: Cons
415
437
  const postRunPools = currentMemoryPools(runtime, ctx.sessionManager.getBranch() as Entry[]);
416
438
  const target = runtime.config.oldMemoryPoolTargetTokens;
417
439
  runtime.summarizerNextTriggerTokens = summarizerTriggerAfterRun(
418
- // A failed/no-progress model pass must not be retried with the
419
- // identical pool at every primary-agent checkpoint. Require fresh
420
- // old-memory growth before trying another (potentially different)
421
- // sample. Model-resolution failures spend no tokens and stay eligible.
422
- successfullyCompleted || modelRunAttempted,
440
+ successfullyCompleted,
423
441
  runtime.summarizerNextTriggerTokens,
424
442
  target,
425
443
  postRunPools.oldTokens,
426
444
  runtime.config.summarizerRetriggerTokens,
445
+ // A failed/no-progress model pass should not retry an identical
446
+ // prompt at every checkpoint. Anchor the growth backoff to the pool
447
+ // seen at launch so concurrent growth is not accidentally erased.
448
+ modelRunAttempted ? runStartOldTokens : undefined,
427
449
  );
428
450
  }
429
451
  }
430
452
  }));
453
+ void task?.then(() => {
454
+ if (generation !== runtime.getContextGeneration()) return;
455
+ if (!runtime.summarizerRecheckPending) return;
456
+ // launchTrackedTask has released summarizerInFlight before resolving.
457
+ // Coalesce every checkpoint received during the old run into one fresh
458
+ // ledger/threshold evaluation.
459
+ runtime.summarizerRecheckPending = false;
460
+ scheduleSummarizer(pi, runtime, ctx);
461
+ });
431
462
  }
432
463
 
433
464
  async function runObserverStage(
package/src/runtime.ts CHANGED
@@ -176,6 +176,8 @@ export class Runtime {
176
176
  summarizerPromise: Promise<void> | null = null;
177
177
  /** Old-pool token threshold for the next pass; undefined means configured target. */
178
178
  summarizerNextTriggerTokens: number | undefined;
179
+ /** A scheduling checkpoint arrived while the single-flight summarizer lock was held. */
180
+ summarizerRecheckPending = false;
179
181
  private memoryUpdateListener: ((ctx: MemoryUpdateCtx) => void) | undefined;
180
182
  private agentActivityListener: ((ctx: MemoryUpdateCtx) => void) | undefined;
181
183
  private settingsUpdateListener: ((ctx: MemoryUpdateCtx, settings: SettingsUpdate) => void) | undefined;
@@ -277,6 +279,7 @@ export class Runtime {
277
279
  this.observerBacklogBlocking = false;
278
280
  this.summarizerInFlight = false;
279
281
  this.summarizerPromise = null;
282
+ this.summarizerRecheckPending = false;
280
283
  this.reviewInFlight = false;
281
284
  this.reviewPromise = null;
282
285
  this.compactInFlight = false;