@matthewfl/pi-contemplator 0.1.4 → 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 +1 -1
- package/src/agents/contemplator/agent.ts +47 -12
- package/src/commands/contemplator-view.ts +3 -1
- package/src/commands/status.ts +1 -0
- package/src/config.ts +1 -1
- package/src/hooks/consolidation-trigger.ts +39 -11
- package/src/runtime.ts +5 -2
- 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.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",
|
|
@@ -229,9 +229,11 @@ export class Contemplator {
|
|
|
229
229
|
private deliveredProbeIds = new Set<string>();
|
|
230
230
|
/** Probe ids passed to pi.sendMessage by this live extension runtime. */
|
|
231
231
|
private queuedProbeIds = new Set<string>();
|
|
232
|
+
/** Probes whose provider-context delivery will establish the next response-spacing anchor. */
|
|
233
|
+
private probeCooldownPendingIds = new Set<string>();
|
|
232
234
|
private sessionGeneration = 0;
|
|
233
235
|
private latestCtx: MemoryUpdateCtx | undefined;
|
|
234
|
-
/** Completed primary-model responses since the
|
|
236
|
+
/** Completed primary-model responses since the current completion/probe-delivery spacing anchor. */
|
|
235
237
|
private turnsSinceRun = 0;
|
|
236
238
|
/** Used to avoid counting the final turn_end after its assistant message_end. */
|
|
237
239
|
private assistantResponsesInCurrentTurn = 0;
|
|
@@ -311,6 +313,7 @@ export class Contemplator {
|
|
|
311
313
|
this.reviewerSessions.clear();
|
|
312
314
|
this.deliveredProbeIds.clear();
|
|
313
315
|
this.queuedProbeIds.clear();
|
|
316
|
+
this.probeCooldownPendingIds.clear();
|
|
314
317
|
this.latestCtx = undefined;
|
|
315
318
|
this.turnsSinceRun = 0;
|
|
316
319
|
this.assistantResponsesInCurrentTurn = 0;
|
|
@@ -359,9 +362,12 @@ export class Contemplator {
|
|
|
359
362
|
});
|
|
360
363
|
this.pi.on("context", (event: any, ctx: ExtensionContext) => {
|
|
361
364
|
const deliveredMessages = event.messages?.filter((message: any) => message?.role === "custom" && message.customType === CONTEMPLATOR_SUGGESTION && typeof message.details?.probeId === "string") ?? [];
|
|
365
|
+
let cooldownAnchored = false;
|
|
362
366
|
for (const delivered of deliveredMessages) {
|
|
363
367
|
if (this.deliveredProbeIds.has(delivered.details.probeId)) continue;
|
|
364
368
|
this.deliveredProbeIds.add(delivered.details.probeId);
|
|
369
|
+
this.probeCooldownPendingIds.delete(delivered.details.probeId);
|
|
370
|
+
cooldownAnchored = true;
|
|
365
371
|
// Once Pi includes the probe in a provider context it is no longer in
|
|
366
372
|
// either in-memory delivery queue. Keeping this id indefinitely caused
|
|
367
373
|
// later tree restores to suppress a genuinely needed requeue.
|
|
@@ -375,6 +381,12 @@ export class Contemplator {
|
|
|
375
381
|
this.markTipPersisted(ctx);
|
|
376
382
|
debugLog("contemplator.suggestion_delivered", { probeId: delivered.details.probeId });
|
|
377
383
|
}
|
|
384
|
+
if (cooldownAnchored) {
|
|
385
|
+
// Probe spacing begins only once Pi proves the probe reached an actual
|
|
386
|
+
// provider context. Responses generated before this point do not count.
|
|
387
|
+
this.turnsSinceRun = 0;
|
|
388
|
+
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
389
|
+
}
|
|
378
390
|
});
|
|
379
391
|
this.pi.on("turn_end", (_event: any, ctx: ExtensionContext) => {
|
|
380
392
|
this.persistAgentActivity(ctx);
|
|
@@ -437,6 +449,7 @@ export class Contemplator {
|
|
|
437
449
|
let resetProjection: ReturnType<typeof fullProjection> | undefined;
|
|
438
450
|
if (resetTracking) {
|
|
439
451
|
this.deliveredProbeIds.clear();
|
|
452
|
+
this.probeCooldownPendingIds.clear();
|
|
440
453
|
if (!retainQueuedIds) this.queuedProbeIds.clear();
|
|
441
454
|
this.inFlightReviewIds.clear();
|
|
442
455
|
this.resolvingReviewIds.clear();
|
|
@@ -545,6 +558,9 @@ export class Contemplator {
|
|
|
545
558
|
}
|
|
546
559
|
this.restoredTipId = tipId;
|
|
547
560
|
for (const [probeId, question] of undeliveredSuggestions) {
|
|
561
|
+
// An undelivered durable probe remains the cooldown anchor even when Pi's
|
|
562
|
+
// live queue survived an extension reload and must not be duplicated.
|
|
563
|
+
this.probeCooldownPendingIds.add(probeId);
|
|
548
564
|
// A durable custom_message proves only that Pi inserted the probe at some
|
|
549
565
|
// point; it does not prove an in-memory queue still owns it, and compaction
|
|
550
566
|
// may have removed it from active model context. Suppress requeue only for
|
|
@@ -570,18 +586,21 @@ export class Contemplator {
|
|
|
570
586
|
}
|
|
571
587
|
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
572
588
|
const observerBacklogTokens = rawTokensSinceObservationCoverage(branchEntries);
|
|
573
|
-
|
|
589
|
+
const waitingForCapturedObserverBacklog = this.runtime.observerBacklogBlocking || (
|
|
590
|
+
!this.runtime.consolidationInFlight &&
|
|
574
591
|
observerBacklogTokens >= this.runtime.config.observeAfterTokens &&
|
|
575
|
-
|
|
576
|
-
)
|
|
592
|
+
this.runtime.lastObserverError === undefined
|
|
593
|
+
);
|
|
594
|
+
if (waitingForCapturedObserverBacklog) {
|
|
577
595
|
// A catch-up observer pipeline may append several partial batches while it
|
|
578
|
-
// drains
|
|
579
|
-
// fragments to the contemplator one at a time.
|
|
580
|
-
//
|
|
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.
|
|
581
599
|
this.publishState("observer");
|
|
582
600
|
debugLog("contemplator.waiting", {
|
|
583
601
|
reason: "observer_backlog",
|
|
584
602
|
observerBacklogTokens,
|
|
603
|
+
observerBacklogBlocking: this.runtime.observerBacklogBlocking,
|
|
585
604
|
observeAfterTokens: this.runtime.config.observeAfterTokens,
|
|
586
605
|
});
|
|
587
606
|
return;
|
|
@@ -620,7 +639,7 @@ export class Contemplator {
|
|
|
620
639
|
};
|
|
621
640
|
}
|
|
622
641
|
if (!this.pending) {
|
|
623
|
-
this.publishState(this.running ? "running" : "idle");
|
|
642
|
+
this.publishState(this.running ? "running" : this.probeCooldownPendingIds.size > 0 ? "probe" : "idle");
|
|
624
643
|
return;
|
|
625
644
|
}
|
|
626
645
|
// Activity values are cumulative send-time snapshots, not values frozen when
|
|
@@ -630,6 +649,11 @@ export class Contemplator {
|
|
|
630
649
|
this.pending.mainAgentToolCalls = assistantToolCallCount(branchEntries);
|
|
631
650
|
this.pending.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntries);
|
|
632
651
|
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations || this.pending.summaries.length >= this.runtime.config.contemplatorMinNewSummaries;
|
|
652
|
+
if (this.probeCooldownPendingIds.size > 0) {
|
|
653
|
+
this.publishState("probe");
|
|
654
|
+
debugLog("contemplator.waiting", { reason: "probe_delivery", pendingProbeCount: this.probeCooldownPendingIds.size });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
633
657
|
if (!enoughMemories || this.turnsSinceRun < this.runtime.config.contemplatorMinTurns) {
|
|
634
658
|
this.publishState(!enoughMemories ? "memories" : "responses");
|
|
635
659
|
debugLog("contemplator.waiting", {
|
|
@@ -676,6 +700,7 @@ export class Contemplator {
|
|
|
676
700
|
let failureMessage: string | undefined;
|
|
677
701
|
let workerNotified = false;
|
|
678
702
|
let promptPersisted = false;
|
|
703
|
+
let emittedProbeId: string | undefined;
|
|
679
704
|
let workerWatchdog: ReturnType<typeof createWorkerStallWatchdog> | undefined;
|
|
680
705
|
this.publishState("running", { lastStartedAt: startedAt, lastError: undefined });
|
|
681
706
|
debugLog("contemplator.start", {
|
|
@@ -863,7 +888,7 @@ export class Contemplator {
|
|
|
863
888
|
this.markTipPersisted(ctx);
|
|
864
889
|
}
|
|
865
890
|
}
|
|
866
|
-
if (intervention?.kind === "probe" && sessionGeneration === this.sessionGeneration) this.queueProbe(ctx, intervention.question, "send_probe");
|
|
891
|
+
if (intervention?.kind === "probe" && sessionGeneration === this.sessionGeneration) emittedProbeId = this.queueProbe(ctx, intervention.question, "send_probe");
|
|
867
892
|
if (intervention?.kind === "review" && this.runtime.config.reviewerEnabled && sessionGeneration === this.sessionGeneration) {
|
|
868
893
|
const reviewerModel = await this.runtime.resolveModel({
|
|
869
894
|
model: ctx.model,
|
|
@@ -917,13 +942,21 @@ export class Contemplator {
|
|
|
917
942
|
if (flushEpoch !== this.flushEpoch) return;
|
|
918
943
|
this.running = false;
|
|
919
944
|
if (!failed) this.consecutiveFlushFailures = 0;
|
|
945
|
+
// Normal runs establish their spacing anchor at completion. A probe run
|
|
946
|
+
// instead anchors at provider-context delivery: if delivery already occurred
|
|
947
|
+
// during this run, retain responses counted since it; otherwise the pending
|
|
948
|
+
// probe gate blocks launches until the context event resets the counter.
|
|
949
|
+
if (emittedProbeId === undefined) this.turnsSinceRun = 0;
|
|
950
|
+
const waitingForProbe = this.probeCooldownPendingIds.size > 0;
|
|
920
951
|
const pendingHasEnoughMemories = this.pending !== undefined && (
|
|
921
952
|
this.pending.reviews.length > 0 ||
|
|
922
953
|
this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations ||
|
|
923
954
|
this.pending.summaries.length >= this.runtime.config.contemplatorMinNewSummaries
|
|
924
955
|
);
|
|
925
|
-
const waitingFor =
|
|
926
|
-
? "
|
|
956
|
+
const waitingFor = waitingForProbe
|
|
957
|
+
? "probe"
|
|
958
|
+
: !this.pending
|
|
959
|
+
? "idle"
|
|
927
960
|
: !pendingHasEnoughMemories
|
|
928
961
|
? "memories"
|
|
929
962
|
: this.turnsSinceRun < this.runtime.config.contemplatorMinTurns
|
|
@@ -948,7 +981,7 @@ export class Contemplator {
|
|
|
948
981
|
}
|
|
949
982
|
}
|
|
950
983
|
|
|
951
|
-
private queueProbe(ctx: MemoryUpdateCtx, question: string, source: "send_probe" | "restore", existingProbeId?: string):
|
|
984
|
+
private queueProbe(ctx: MemoryUpdateCtx, question: string, source: "send_probe" | "restore", existingProbeId?: string): string {
|
|
952
985
|
const probeId = existingProbeId ?? `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
953
986
|
// Persist intent before touching Pi's in-memory queue. A crash in between
|
|
954
987
|
// leaves a recoverable pending probe rather than an invisible lost one.
|
|
@@ -967,6 +1000,7 @@ export class Contemplator {
|
|
|
967
1000
|
// unrelated observer update or compaction callback cannot restore and enqueue
|
|
968
1001
|
// a duplicate while the original idle steer is still pending.
|
|
969
1002
|
this.queuedProbeIds.add(probeId);
|
|
1003
|
+
this.probeCooldownPendingIds.add(probeId);
|
|
970
1004
|
this.pi.sendMessage({
|
|
971
1005
|
customType: CONTEMPLATOR_SUGGESTION,
|
|
972
1006
|
content: `Background contemplator probe (advisory):\n${question}\n\nReferenced memories can be reviewed using the recall tool.`,
|
|
@@ -981,6 +1015,7 @@ export class Contemplator {
|
|
|
981
1015
|
triggerTurn: "omitted",
|
|
982
1016
|
source,
|
|
983
1017
|
});
|
|
1018
|
+
return probeId;
|
|
984
1019
|
}
|
|
985
1020
|
|
|
986
1021
|
private queueStructuralReview(options: QueueStructuralReviewOptions): void {
|
|
@@ -65,6 +65,8 @@ function liveStateLine(state: ContemplatorRunState): string {
|
|
|
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}`;
|
|
66
66
|
const reason = state.waitingFor === "observer"
|
|
67
67
|
? "waiting for observer backlog"
|
|
68
|
+
: state.waitingFor === "probe"
|
|
69
|
+
? "waiting for queued probe delivery"
|
|
68
70
|
: state.waitingFor === "memories"
|
|
69
71
|
? "waiting for memory threshold"
|
|
70
72
|
: state.waitingFor === "responses"
|
|
@@ -76,7 +78,7 @@ function liveStateLine(state: ContemplatorRunState): string {
|
|
|
76
78
|
: state.waitingFor === "passive"
|
|
77
79
|
? "passive mode"
|
|
78
80
|
: "idle";
|
|
79
|
-
return `LIVE · ${reason} · ${pending} · ${state.responsesSinceRun} primary responses since
|
|
81
|
+
return `LIVE · ${reason} · ${pending} · ${state.responsesSinceRun} primary responses since cooldown anchor\n${timing}${error}`;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export function renderContemplator(entries: Entry[], state?: ContemplatorRunState): string {
|
package/src/commands/status.ts
CHANGED
|
@@ -43,6 +43,7 @@ function formatRunAge(timestamp: number): string {
|
|
|
43
43
|
function contemplatorWaitingLabel(waitingFor: Runtime["contemplatorState"]["waitingFor"]): string {
|
|
44
44
|
switch (waitingFor) {
|
|
45
45
|
case "observer": return "waiting for observer backlog";
|
|
46
|
+
case "probe": return "waiting for queued probe delivery";
|
|
46
47
|
case "memories": return "waiting for memory threshold";
|
|
47
48
|
case "responses": return "waiting for response spacing";
|
|
48
49
|
case "ready": return "ready to launch";
|
package/src/config.ts
CHANGED
|
@@ -61,7 +61,7 @@ export interface Config {
|
|
|
61
61
|
reviewerModel?: ConfiguredModel;
|
|
62
62
|
contemplatorMinNewObservations: number;
|
|
63
63
|
contemplatorMinNewSummaries: number;
|
|
64
|
-
/** Minimum
|
|
64
|
+
/** Minimum primary-model responses after contemplator completion, or after delivery of its probe, before the next run. */
|
|
65
65
|
contemplatorMinTurns: number;
|
|
66
66
|
/** Stateless loss-aware summarizer for the old memory pool. */
|
|
67
67
|
summarizerEnabled: boolean;
|
|
@@ -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
|
@@ -82,9 +82,9 @@ export interface ContemplatorRunState {
|
|
|
82
82
|
pendingObservations: number;
|
|
83
83
|
pendingSummaries: number;
|
|
84
84
|
pendingReviews: number;
|
|
85
|
-
/** Completed primary-model responses since the
|
|
85
|
+
/** Completed primary-model responses since the current completion/probe-delivery spacing anchor. */
|
|
86
86
|
responsesSinceRun: number;
|
|
87
|
-
waitingFor: "disabled" | "passive" | "observer" | "memories" | "responses" | "ready" | "running" | "idle";
|
|
87
|
+
waitingFor: "disabled" | "passive" | "observer" | "probe" | "memories" | "responses" | "ready" | "running" | "idle";
|
|
88
88
|
lastStartedAt?: number;
|
|
89
89
|
lastCompletedAt?: number;
|
|
90
90
|
lastError?: string;
|
|
@@ -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;
|