@matthewfl/pi-contemplator 0.1.5 → 0.1.6
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.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|
|
@@ -586,18 +586,21 @@ export class Contemplator {
|
|
|
586
586
|
}
|
|
587
587
|
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
588
588
|
const observerBacklogTokens = rawTokensSinceObservationCoverage(branchEntries);
|
|
589
|
-
|
|
589
|
+
const waitingForCapturedObserverBacklog = this.runtime.observerBacklogBlocking || (
|
|
590
|
+
!this.runtime.consolidationInFlight &&
|
|
590
591
|
observerBacklogTokens >= this.runtime.config.observeAfterTokens &&
|
|
591
|
-
|
|
592
|
-
)
|
|
592
|
+
this.runtime.lastObserverError === undefined
|
|
593
|
+
);
|
|
594
|
+
if (waitingForCapturedObserverBacklog) {
|
|
593
595
|
// A catch-up observer pipeline may append several partial batches while it
|
|
594
|
-
// drains
|
|
595
|
-
// fragments to the contemplator one at a time.
|
|
596
|
-
//
|
|
596
|
+
// drains the finite source snapshot captured at launch. Do not feed those
|
|
597
|
+
// fragments to the contemplator one at a time. Source appended concurrently
|
|
598
|
+
// belongs to the next snapshot and must not extend this waiting period.
|
|
597
599
|
this.publishState("observer");
|
|
598
600
|
debugLog("contemplator.waiting", {
|
|
599
601
|
reason: "observer_backlog",
|
|
600
602
|
observerBacklogTokens,
|
|
603
|
+
observerBacklogBlocking: this.runtime.observerBacklogBlocking,
|
|
601
604
|
observeAfterTokens: this.runtime.config.observeAfterTokens,
|
|
602
605
|
});
|
|
603
606
|
return;
|
|
@@ -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));
|
|
@@ -408,10 +434,12 @@ async function runObserverStage(
|
|
|
408
434
|
runtime: Runtime,
|
|
409
435
|
ctx: ConsolidationCtx,
|
|
410
436
|
resolveModel: (stage: "observer") => Promise<ResolvedModel | undefined>,
|
|
411
|
-
options: { force?: boolean; entries?: Entry[]; contextGeneration?: number } = {},
|
|
437
|
+
options: { force?: boolean; entries?: Entry[]; throughSourceEntryId?: string; contextGeneration?: number } = {},
|
|
412
438
|
): Promise<StageOutcome> {
|
|
413
439
|
const entries = options.entries ?? (ctx.sessionManager.getBranch() as Entry[]);
|
|
414
|
-
const tokens =
|
|
440
|
+
const tokens = options.throughSourceEntryId === undefined
|
|
441
|
+
? rawTokensSinceObservationCoverage(entries)
|
|
442
|
+
: rawTokensSinceObservationCoverageThrough(entries, options.throughSourceEntryId);
|
|
415
443
|
if (!options.force && tokens < runtime.config.observeAfterTokens) return "continue";
|
|
416
444
|
|
|
417
445
|
// Resolve the model before building the chunk: the default chunk cap
|
|
@@ -424,7 +452,7 @@ async function runObserverStage(
|
|
|
424
452
|
}
|
|
425
453
|
|
|
426
454
|
const lastCoverageIdx = latestCoverageIndex(entries, OM_OBSERVATIONS_RECORDED);
|
|
427
|
-
const backlogEntries = sourceEntriesAfter(entries, lastCoverageIdx);
|
|
455
|
+
const backlogEntries = sourceEntriesAfter(entries, lastCoverageIdx, options.throughSourceEntryId);
|
|
428
456
|
|
|
429
457
|
// Budget the text that is actually sent to the observer, including source
|
|
430
458
|
// labels and rendered message content. Complete entries are kept intact.
|
package/src/runtime.ts
CHANGED
|
@@ -182,6 +182,8 @@ export class Runtime {
|
|
|
182
182
|
private settingsUpdateListener: ((ctx: MemoryUpdateCtx, settings: SettingsUpdate) => void) | undefined;
|
|
183
183
|
private contextGeneration = 0;
|
|
184
184
|
consolidationPhase: ConsolidationPhase | undefined;
|
|
185
|
+
/** True only while the observer is draining the finite source snapshot captured at pipeline start. */
|
|
186
|
+
observerBacklogBlocking = false;
|
|
185
187
|
compactInFlight = false;
|
|
186
188
|
compactRequested = false;
|
|
187
189
|
/** Agent-authored instructions to deliver after an explicit compact_context request. */
|
|
@@ -274,6 +276,7 @@ export class Runtime {
|
|
|
274
276
|
this.consolidationInFlight = false;
|
|
275
277
|
this.consolidationPromise = null;
|
|
276
278
|
this.consolidationPhase = undefined;
|
|
279
|
+
this.observerBacklogBlocking = false;
|
|
277
280
|
this.summarizerInFlight = false;
|
|
278
281
|
this.summarizerPromise = null;
|
|
279
282
|
this.reviewInFlight = false;
|
|
@@ -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;
|