@matthewfl/pi-contemplator 0.3.0 → 0.4.0
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/README.md +1 -1
- package/package.json +11 -11
- package/src/agents/contemplator/agent.ts +149 -17
- package/src/commands/settings.ts +3 -1
- package/src/commands/status.ts +1 -1
- package/src/config.ts +4 -0
- package/src/hooks/consolidation-trigger.ts +5 -0
- package/src/runtime.ts +6 -2
- package/src/session-ledger/progress.ts +8 -1
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ The observer and summarizer provide the durable memory substrate. The contemplat
|
|
|
157
157
|
|
|
158
158
|
## Installation
|
|
159
159
|
|
|
160
|
-
`pi-contemplator` 0.
|
|
160
|
+
`pi-contemplator` 0.4.0 requires **Pi 1.0.0** or later.
|
|
161
161
|
|
|
162
162
|
Install the published package through Pi:
|
|
163
163
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -41,20 +41,20 @@
|
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
42
|
"test": "npm run test:unit && npm run test:e2e",
|
|
43
43
|
"test:unit": "vitest run",
|
|
44
|
-
"test:e2e": "node tests/e2e/rpc-contemplator.mjs && node tests/e2e/rpc-contemplator-history.mjs && node tests/e2e/rpc-summarizer.mjs && node tests/e2e/rpc-delivery.mjs && node tests/e2e/rpc-restore-review.mjs && node tests/e2e/rpc-compaction.mjs && node tests/e2e/rpc-compaction-resilience.mjs && node tests/e2e/rpc-memory-edges.mjs && node tests/e2e/rpc-observer-length.mjs && node tests/e2e/rpc-routing-isolation.mjs"
|
|
44
|
+
"test:e2e": "node tests/e2e/rpc-contemplator.mjs && node tests/e2e/rpc-contemplator-active-time.mjs && node tests/e2e/rpc-contemplator-history.mjs && node tests/e2e/rpc-summarizer.mjs && node tests/e2e/rpc-delivery.mjs && node tests/e2e/rpc-restore-review.mjs && node tests/e2e/rpc-compaction.mjs && node tests/e2e/rpc-compaction-resilience.mjs && node tests/e2e/rpc-memory-edges.mjs && node tests/e2e/rpc-observer-length.mjs && node tests/e2e/rpc-routing-isolation.mjs"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
48
|
-
"@earendil-works/pi-ai": "^0.
|
|
49
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
50
|
-
"@earendil-works/pi-tui": "^0.
|
|
47
|
+
"@earendil-works/pi-agent-core": "^1.0.0",
|
|
48
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
49
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
50
|
+
"@earendil-works/pi-tui": "^1.0.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
54
|
-
"@earendil-works/pi-ai": "^0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
56
|
-
"@earendil-works/pi-server": "^0.
|
|
57
|
-
"@earendil-works/pi-tui": "^0.
|
|
53
|
+
"@earendil-works/pi-agent-core": "^1.0.0",
|
|
54
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
56
|
+
"@earendil-works/pi-server": "^1.0.0",
|
|
57
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
58
58
|
"@types/node": "^22.0.0",
|
|
59
59
|
"typebox": "^1.1.38",
|
|
60
60
|
"typescript": "^5.6.0",
|
|
@@ -84,6 +84,8 @@ interface PendingUpdate {
|
|
|
84
84
|
mainAgentOutputTokens: number;
|
|
85
85
|
mainAgentToolCalls: number;
|
|
86
86
|
mainAgentActiveTimeMs: number;
|
|
87
|
+
/** True when cumulative active work, rather than memory volume, forced this update. */
|
|
88
|
+
activeTimeFallback: boolean;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
type Intervention =
|
|
@@ -171,6 +173,8 @@ function coarseAgentTime(durationMs: number): string {
|
|
|
171
173
|
}
|
|
172
174
|
const CONTEMPLATOR_MESSAGE = "om.contemplator.message";
|
|
173
175
|
const CONTEMPLATOR_STATE = "om.contemplator.state";
|
|
176
|
+
/** Durable per-branch baseline for the cumulative active-work fallback trigger. */
|
|
177
|
+
const CONTEMPLATOR_RUN = "om.contemplator.run";
|
|
174
178
|
const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
|
|
175
179
|
const REVIEW_PROPOSAL_MESSAGE = "om.review.proposal";
|
|
176
180
|
const SendProbeSchema = Type.Object({ question: Type.String({ minLength: 1, description: "One concise, memory-grounded probing question, optionally preceded by one short sentence of context. Cite relevant memory identifiers." }) });
|
|
@@ -292,6 +296,8 @@ export class Contemplator {
|
|
|
292
296
|
private queuedProbeIds = new Set<string>();
|
|
293
297
|
/** Probes whose provider-context delivery will establish the next response-spacing anchor. */
|
|
294
298
|
private probeCooldownPendingIds = new Set<string>();
|
|
299
|
+
/** Preserve the configured response gap after a probe actually reaches provider context. */
|
|
300
|
+
private probeDeliverySpacingActive = false;
|
|
295
301
|
private sessionGeneration = 0;
|
|
296
302
|
private latestCtx: MemoryUpdateCtx | undefined;
|
|
297
303
|
/** Completed primary-model responses since the current completion/probe-delivery spacing anchor. */
|
|
@@ -301,6 +307,10 @@ export class Contemplator {
|
|
|
301
307
|
private restoredTipId: string | undefined;
|
|
302
308
|
/** Start of the unpersisted portion of the current main-agent run. */
|
|
303
309
|
private agentActiveSince: number | undefined;
|
|
310
|
+
/** Cumulative active time at the latest contemplator attempt on this branch. */
|
|
311
|
+
private lastContemplatorActiveTimeMs = 0;
|
|
312
|
+
/** Fires while the primary agent remains active, including inside one long tool call. */
|
|
313
|
+
private activeTimeFallbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
304
314
|
|
|
305
315
|
constructor(private readonly pi: ExtensionAPI, private readonly runtime: Runtime) {}
|
|
306
316
|
|
|
@@ -322,12 +332,14 @@ export class Contemplator {
|
|
|
322
332
|
return box;
|
|
323
333
|
});
|
|
324
334
|
this.runtime.setMemoryUpdateListener((ctx) => this.withDebugContext(ctx, () => this.observeTurn(ctx)));
|
|
325
|
-
this.pi.on("agent_start", () => {
|
|
335
|
+
this.pi.on("agent_start", (_event: unknown, ctx: ExtensionContext) => {
|
|
326
336
|
this.agentActiveSince = Date.now();
|
|
337
|
+
this.withDebugContext(ctx, () => this.scheduleActiveTimeFallback(ctx));
|
|
327
338
|
});
|
|
328
339
|
this.pi.on("agent_end", (_event: unknown, ctx: ExtensionContext) => {
|
|
329
340
|
this.persistAgentActivity(ctx);
|
|
330
341
|
this.agentActiveSince = undefined;
|
|
342
|
+
this.clearActiveTimeFallbackTimer();
|
|
331
343
|
});
|
|
332
344
|
this.pi.on("session_start", (event: any, ctx: ExtensionContext) => {
|
|
333
345
|
this.sessionGeneration++;
|
|
@@ -336,11 +348,20 @@ export class Contemplator {
|
|
|
336
348
|
this.consecutiveFlushFailures = 0;
|
|
337
349
|
const generation = this.sessionGeneration;
|
|
338
350
|
this.agentActiveSince = undefined;
|
|
351
|
+
this.clearActiveTimeFallbackTimer();
|
|
339
352
|
// AgentSession preserves its steering queue across extension reloads. An
|
|
340
353
|
// undelivered tracking entry therefore still has a live queued message;
|
|
341
354
|
// restoring it here would enqueue the same probe a second time.
|
|
342
355
|
const reload = event?.reason === "reload";
|
|
343
356
|
this.restore(ctx, true, reload, reload);
|
|
357
|
+
// Reload replaces the extension runtime without restarting an already-active
|
|
358
|
+
// agent run, so no new agent_start event is guaranteed. Recover the clock
|
|
359
|
+
// from Pi's live state; elapsed time before reload may be unavailable, but
|
|
360
|
+
// the current run remains monitored rather than going unarmed indefinitely.
|
|
361
|
+
if (!ctx.isIdle()) {
|
|
362
|
+
this.agentActiveSince = Date.now();
|
|
363
|
+
this.withDebugContext(ctx, () => this.scheduleActiveTimeFallback(ctx));
|
|
364
|
+
}
|
|
344
365
|
// Reconstruct and schedule durable memory immediately. In particular, a
|
|
345
366
|
// reload after a failed run must not silently mark its pending backlog seen.
|
|
346
367
|
queueMicrotask(() => {
|
|
@@ -353,15 +374,22 @@ export class Contemplator {
|
|
|
353
374
|
this.running = false;
|
|
354
375
|
this.consecutiveFlushFailures = 0;
|
|
355
376
|
this.agentActiveSince = undefined;
|
|
377
|
+
this.clearActiveTimeFallbackTimer();
|
|
356
378
|
// Pending steering messages remain queued while navigating the tree.
|
|
357
379
|
this.restore(ctx, true, true);
|
|
358
380
|
});
|
|
359
|
-
this.pi.on("session_shutdown", () => {
|
|
381
|
+
this.pi.on("session_shutdown", (_event: unknown, ctx: ExtensionContext) => {
|
|
382
|
+
// On extension reload Pi shuts down the old runtime before starting the
|
|
383
|
+
// replacement. Persist its final live interval without launching workers,
|
|
384
|
+
// so the replacement can continue from the exact cumulative clock.
|
|
385
|
+
this.persistAgentActivity(ctx, false, false);
|
|
360
386
|
this.sessionGeneration++;
|
|
361
387
|
this.flushEpoch++;
|
|
362
388
|
this.running = false;
|
|
363
389
|
this.consecutiveFlushFailures = 0;
|
|
364
390
|
this.agentActiveSince = undefined;
|
|
391
|
+
this.clearActiveTimeFallbackTimer();
|
|
392
|
+
this.lastContemplatorActiveTimeMs = 0;
|
|
365
393
|
this.history = [];
|
|
366
394
|
this.historyEntryIds = [];
|
|
367
395
|
this.pending = undefined;
|
|
@@ -375,6 +403,7 @@ export class Contemplator {
|
|
|
375
403
|
this.deliveredProbeIds.clear();
|
|
376
404
|
this.queuedProbeIds.clear();
|
|
377
405
|
this.probeCooldownPendingIds.clear();
|
|
406
|
+
this.probeDeliverySpacingActive = false;
|
|
378
407
|
this.latestCtx = undefined;
|
|
379
408
|
this.turnsSinceRun = 0;
|
|
380
409
|
this.assistantResponsesInCurrentTurn = 0;
|
|
@@ -384,6 +413,7 @@ export class Contemplator {
|
|
|
384
413
|
pendingObservations: 0,
|
|
385
414
|
pendingReviews: 0,
|
|
386
415
|
responsesSinceRun: 0,
|
|
416
|
+
activeTimeSinceRunMs: 0,
|
|
387
417
|
waitingFor: "idle",
|
|
388
418
|
};
|
|
389
419
|
});
|
|
@@ -419,6 +449,9 @@ export class Contemplator {
|
|
|
419
449
|
// This records one wall-clock interval regardless of how many tools were
|
|
420
450
|
// running concurrently; persistAgentActivity restarts the shared clock.
|
|
421
451
|
this.persistAgentActivity(ctx);
|
|
452
|
+
// A long autonomous turn can perform many tools without ending a model
|
|
453
|
+
// turn. Recheck the active-work fallback at each completed tool batch.
|
|
454
|
+
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
422
455
|
});
|
|
423
456
|
this.pi.on("context", (event: any, ctx: ExtensionContext) => {
|
|
424
457
|
const deliveredMessages = event.messages?.filter((message: any) => message?.role === "custom" && message.customType === CONTEMPLATOR_SUGGESTION && typeof message.details?.probeId === "string") ?? [];
|
|
@@ -444,6 +477,7 @@ export class Contemplator {
|
|
|
444
477
|
if (cooldownAnchored) {
|
|
445
478
|
// Probe spacing begins only once Pi proves the probe reached an actual
|
|
446
479
|
// provider context. Responses generated before this point do not count.
|
|
480
|
+
this.probeDeliverySpacingActive = true;
|
|
447
481
|
this.turnsSinceRun = 0;
|
|
448
482
|
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
449
483
|
}
|
|
@@ -458,18 +492,46 @@ export class Contemplator {
|
|
|
458
492
|
});
|
|
459
493
|
}
|
|
460
494
|
|
|
461
|
-
private
|
|
495
|
+
private clearActiveTimeFallbackTimer(): void {
|
|
496
|
+
if (this.activeTimeFallbackTimer !== undefined) clearTimeout(this.activeTimeFallbackTimer);
|
|
497
|
+
this.activeTimeFallbackTimer = undefined;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private scheduleActiveTimeFallback(ctx: MemoryUpdateCtx): void {
|
|
501
|
+
this.clearActiveTimeFallbackTimer();
|
|
502
|
+
if (this.agentActiveSince === undefined || !this.runtime.config.contemplatorEnabled || this.runtime.config.passive) return;
|
|
503
|
+
const persistedActiveMs = agentActiveTimeMs(ctx.sessionManager.getBranch() as Entry[]);
|
|
504
|
+
const liveActiveMs = Math.max(0, Date.now() - this.agentActiveSince);
|
|
505
|
+
const elapsedMs = Math.max(0, persistedActiveMs + liveActiveMs - this.lastContemplatorActiveTimeMs);
|
|
506
|
+
const thresholdMs = this.runtime.config.contemplatorMaxActiveMinutes * 60_000;
|
|
507
|
+
const delayMs = Math.max(1, thresholdMs - elapsedMs);
|
|
508
|
+
this.activeTimeFallbackTimer = setTimeout(() => {
|
|
509
|
+
this.activeTimeFallbackTimer = undefined;
|
|
510
|
+
if (this.agentActiveSince === undefined) return;
|
|
511
|
+
this.withDebugContext(ctx, () => {
|
|
512
|
+
// Materialize the live interval before observeTurn reads the ledger. This
|
|
513
|
+
// lets a sleep/long tool trigger contemplation while it is still running.
|
|
514
|
+
this.persistAgentActivity(ctx, false);
|
|
515
|
+
this.observeTurn(ctx);
|
|
516
|
+
});
|
|
517
|
+
}, delayMs);
|
|
518
|
+
(this.activeTimeFallbackTimer as { unref?: () => void }).unref?.();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private persistAgentActivity(ctx: MemoryUpdateCtx, reschedule = true, notify = true): void {
|
|
462
522
|
const startedAt = this.agentActiveSince;
|
|
463
523
|
if (startedAt === undefined) return;
|
|
464
524
|
const endedAt = Date.now();
|
|
465
525
|
this.agentActiveSince = endedAt;
|
|
466
526
|
const durationMs = Math.max(0, endedAt - startedAt);
|
|
467
|
-
if (durationMs
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
527
|
+
if (durationMs > 0) {
|
|
528
|
+
this.pi.appendEntry(OM_AGENT_ACTIVITY, { version: 1, durationMs, endedAt });
|
|
529
|
+
// Notify only after appendEntry so active-time schedulers always observe the
|
|
530
|
+
// checkpoint, regardless of Pi's ordering between independent event handlers.
|
|
531
|
+
if (notify) this.runtime.notifyAgentActivity(ctx);
|
|
532
|
+
debugLog("agent.activity_recorded", { durationMs });
|
|
533
|
+
}
|
|
534
|
+
if (reschedule) this.scheduleActiveTimeFallback(ctx);
|
|
473
535
|
}
|
|
474
536
|
|
|
475
537
|
private withDebugContext<T>(ctx: MemoryUpdateCtx, fn: () => T): T {
|
|
@@ -493,6 +555,7 @@ export class Contemplator {
|
|
|
493
555
|
pendingObservations: this.pending?.observations.length ?? 0,
|
|
494
556
|
pendingReviews: this.pending?.reviews.length ?? 0,
|
|
495
557
|
responsesSinceRun: this.turnsSinceRun,
|
|
558
|
+
activeTimeSinceRunMs: this.runtime.contemplatorState.activeTimeSinceRunMs,
|
|
496
559
|
waitingFor,
|
|
497
560
|
...overrides,
|
|
498
561
|
};
|
|
@@ -509,10 +572,13 @@ export class Contemplator {
|
|
|
509
572
|
const historyMessagesByEntryId = new Map<string, AgentMessage>();
|
|
510
573
|
const checkpointCoveredObservationIds = new Set<string>();
|
|
511
574
|
const checkpointCoveredReviewIds = new Set<string>();
|
|
575
|
+
let latestRunActiveTimeMs: number | undefined;
|
|
576
|
+
let probeDeliveredAfterLatestRun = false;
|
|
512
577
|
let resetProjection: ReturnType<typeof fullProjection> | undefined;
|
|
513
578
|
if (resetTracking) {
|
|
514
579
|
this.deliveredProbeIds.clear();
|
|
515
580
|
this.probeCooldownPendingIds.clear();
|
|
581
|
+
this.probeDeliverySpacingActive = false;
|
|
516
582
|
if (!retainQueuedIds) this.queuedProbeIds.clear();
|
|
517
583
|
this.inFlightReviewIds.clear();
|
|
518
584
|
this.resolvingReviewIds.clear();
|
|
@@ -529,11 +595,19 @@ export class Contemplator {
|
|
|
529
595
|
pendingObservations: 0,
|
|
530
596
|
pendingReviews: 0,
|
|
531
597
|
responsesSinceRun: 0,
|
|
598
|
+
activeTimeSinceRunMs: 0,
|
|
532
599
|
waitingFor: "idle",
|
|
533
600
|
};
|
|
534
601
|
}
|
|
535
602
|
const undeliveredSuggestions = new Map<string, string>();
|
|
536
603
|
for (const entry of entries) {
|
|
604
|
+
if (entry.customType === CONTEMPLATOR_RUN && entry.data && typeof entry.data === "object") {
|
|
605
|
+
const activeTimeMs = (entry.data as { mainAgentActiveTimeMs?: unknown }).mainAgentActiveTimeMs;
|
|
606
|
+
if (typeof activeTimeMs === "number" && Number.isFinite(activeTimeMs) && activeTimeMs >= 0) {
|
|
607
|
+
latestRunActiveTimeMs = activeTimeMs;
|
|
608
|
+
probeDeliveredAfterLatestRun = false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
537
611
|
if (entry.customType === CONTEMPLATOR_STATE && entry.data && typeof entry.data === "object") {
|
|
538
612
|
const state = entry.data as { history?: unknown };
|
|
539
613
|
if (Array.isArray(state.history)) {
|
|
@@ -607,12 +681,19 @@ export class Contemplator {
|
|
|
607
681
|
if (typeof data.probeId !== "string") continue;
|
|
608
682
|
if (data.delivered === true) {
|
|
609
683
|
this.deliveredProbeIds.add(data.probeId);
|
|
684
|
+
probeDeliveredAfterLatestRun = true;
|
|
610
685
|
undeliveredSuggestions.delete(data.probeId);
|
|
611
686
|
} else if (typeof data.suggestion === "string") {
|
|
612
687
|
undeliveredSuggestions.set(data.probeId, data.suggestion);
|
|
613
688
|
}
|
|
614
689
|
}
|
|
615
690
|
}
|
|
691
|
+
// A malformed/foreign marker must not regress a valid live baseline during
|
|
692
|
+
// an ordinary tip refresh. A true session/tree restore with no valid marker
|
|
693
|
+
// intentionally starts this branch's cumulative baseline at zero.
|
|
694
|
+
if (latestRunActiveTimeMs !== undefined) this.lastContemplatorActiveTimeMs = latestRunActiveTimeMs;
|
|
695
|
+
else if (resetTracking) this.lastContemplatorActiveTimeMs = 0;
|
|
696
|
+
this.probeDeliverySpacingActive = probeDeliveredAfterLatestRun;
|
|
616
697
|
if (resetTracking && resetProjection) {
|
|
617
698
|
// Successful update prompts and private-history compaction checkpoints are
|
|
618
699
|
// the durable coverage record. Checkpoints carry ids from prompts replaced
|
|
@@ -681,6 +762,10 @@ export class Contemplator {
|
|
|
681
762
|
});
|
|
682
763
|
return;
|
|
683
764
|
}
|
|
765
|
+
const cumulativeActiveTimeMs = agentActiveTimeMs(branchEntries) + (this.agentActiveSince === undefined ? 0 : Math.max(0, Date.now() - this.agentActiveSince));
|
|
766
|
+
const activeTimeSinceRunMs = Math.max(0, cumulativeActiveTimeMs - this.lastContemplatorActiveTimeMs);
|
|
767
|
+
const activeTimeThresholdMs = this.runtime.config.contemplatorMaxActiveMinutes * 60_000;
|
|
768
|
+
const activeTimeDue = activeTimeSinceRunMs >= activeTimeThresholdMs;
|
|
684
769
|
const projection = fullProjection(branchEntries);
|
|
685
770
|
const observations = projection.observations.map((item) => `[${item.id}] ${item.content}`);
|
|
686
771
|
const reviews = projection.reviews ?? [];
|
|
@@ -704,11 +789,25 @@ export class Contemplator {
|
|
|
704
789
|
reviews: mergeMemoryLines(this.pending?.reviews ?? [], newReviews),
|
|
705
790
|
mainAgentOutputTokens: assistantOutputTokens(branchEntries),
|
|
706
791
|
mainAgentToolCalls: assistantToolCallCount(branchEntries),
|
|
707
|
-
mainAgentActiveTimeMs:
|
|
792
|
+
mainAgentActiveTimeMs: cumulativeActiveTimeMs,
|
|
793
|
+
activeTimeFallback: this.pending?.activeTimeFallback ?? false,
|
|
708
794
|
};
|
|
709
795
|
}
|
|
796
|
+
if (activeTimeDue && !this.pending) {
|
|
797
|
+
this.pending = {
|
|
798
|
+
observations: [],
|
|
799
|
+
reviews: [],
|
|
800
|
+
mainAgentOutputTokens: assistantOutputTokens(branchEntries),
|
|
801
|
+
mainAgentToolCalls: assistantToolCallCount(branchEntries),
|
|
802
|
+
mainAgentActiveTimeMs: cumulativeActiveTimeMs,
|
|
803
|
+
activeTimeFallback: true,
|
|
804
|
+
};
|
|
805
|
+
} else if (activeTimeDue && this.pending) {
|
|
806
|
+
this.pending.activeTimeFallback = true;
|
|
807
|
+
}
|
|
710
808
|
if (!this.pending) {
|
|
711
|
-
this.
|
|
809
|
+
this.scheduleActiveTimeFallback(ctx);
|
|
810
|
+
this.publishState(this.running ? "running" : this.probeCooldownPendingIds.size > 0 ? "probe" : "idle", { activeTimeSinceRunMs });
|
|
712
811
|
return;
|
|
713
812
|
}
|
|
714
813
|
// Activity values are cumulative send-time snapshots, not values frozen when
|
|
@@ -716,15 +815,17 @@ export class Contemplator {
|
|
|
716
815
|
// while that batch waits for its memory/turn thresholds.
|
|
717
816
|
this.pending.mainAgentOutputTokens = assistantOutputTokens(branchEntries);
|
|
718
817
|
this.pending.mainAgentToolCalls = assistantToolCallCount(branchEntries);
|
|
719
|
-
this.pending.mainAgentActiveTimeMs =
|
|
818
|
+
this.pending.mainAgentActiveTimeMs = cumulativeActiveTimeMs;
|
|
720
819
|
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations;
|
|
820
|
+
const forcedByActiveTime = this.pending.activeTimeFallback;
|
|
721
821
|
if (this.probeCooldownPendingIds.size > 0) {
|
|
722
|
-
this.publishState("probe");
|
|
822
|
+
this.publishState("probe", { activeTimeSinceRunMs });
|
|
723
823
|
debugLog("contemplator.waiting", { reason: "probe_delivery", pendingProbeCount: this.probeCooldownPendingIds.size });
|
|
724
824
|
return;
|
|
725
825
|
}
|
|
726
|
-
|
|
727
|
-
|
|
826
|
+
const waitingForDeliveredProbeResponses = this.probeDeliverySpacingActive && this.turnsSinceRun < this.runtime.config.contemplatorMinTurns;
|
|
827
|
+
if (waitingForDeliveredProbeResponses || (!forcedByActiveTime && (!enoughMemories || this.turnsSinceRun < this.runtime.config.contemplatorMinTurns))) {
|
|
828
|
+
this.publishState(waitingForDeliveredProbeResponses || enoughMemories ? "responses" : "memories", { activeTimeSinceRunMs });
|
|
728
829
|
debugLog("contemplator.waiting", {
|
|
729
830
|
enoughMemories,
|
|
730
831
|
turnsSinceRun: this.turnsSinceRun,
|
|
@@ -733,11 +834,13 @@ export class Contemplator {
|
|
|
733
834
|
});
|
|
734
835
|
return;
|
|
735
836
|
}
|
|
736
|
-
this.publishState(this.running ? "running" : "ready");
|
|
837
|
+
this.publishState(this.running ? "running" : "ready", { activeTimeSinceRunMs });
|
|
737
838
|
debugLog("contemplator.triggered", {
|
|
738
839
|
pendingObservationCount: this.pending.observations.length,
|
|
739
840
|
pendingReviewCount: this.pending.reviews.length,
|
|
740
841
|
turnsSinceRun: this.turnsSinceRun,
|
|
842
|
+
forcedByActiveTime,
|
|
843
|
+
activeTimeSinceRunMs,
|
|
741
844
|
});
|
|
742
845
|
void this.flush(ctx);
|
|
743
846
|
}
|
|
@@ -756,6 +859,19 @@ export class Contemplator {
|
|
|
756
859
|
update.mainAgentOutputTokens = assistantOutputTokens(branchEntriesAtStart);
|
|
757
860
|
update.mainAgentToolCalls = assistantToolCallCount(branchEntriesAtStart);
|
|
758
861
|
update.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntriesAtStart);
|
|
862
|
+
const activeTimeSinceLastRunMs = Math.max(0, update.mainAgentActiveTimeMs - this.lastContemplatorActiveTimeMs);
|
|
863
|
+
// Reset the active-work interval at attempt start, including failed attempts,
|
|
864
|
+
// so a broken provider cannot create an immediate background retry loop. The
|
|
865
|
+
// tiny durable marker makes reloads and tree navigation preserve that backoff.
|
|
866
|
+
this.lastContemplatorActiveTimeMs = update.mainAgentActiveTimeMs;
|
|
867
|
+
this.probeDeliverySpacingActive = false;
|
|
868
|
+
this.scheduleActiveTimeFallback(ctx);
|
|
869
|
+
this.pi.appendEntry(CONTEMPLATOR_RUN, {
|
|
870
|
+
version: 1,
|
|
871
|
+
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
872
|
+
startedAt: Date.now(),
|
|
873
|
+
});
|
|
874
|
+
this.markTipPersisted(ctx);
|
|
759
875
|
this.pending = undefined;
|
|
760
876
|
const turnsBeforeRun = this.turnsSinceRun;
|
|
761
877
|
const sessionGeneration = this.sessionGeneration;
|
|
@@ -797,6 +913,7 @@ export class Contemplator {
|
|
|
797
913
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
798
914
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
799
915
|
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
916
|
+
activeTimeFallback: update.activeTimeFallback,
|
|
800
917
|
};
|
|
801
918
|
} else {
|
|
802
919
|
debugLog("contemplator.poisoned_update_released", { reason: resolved.reason, observationCount: update.observations.length, reviewCount: update.reviews.length });
|
|
@@ -827,13 +944,22 @@ export class Contemplator {
|
|
|
827
944
|
if (update.observations.length > 0) updateSections.push(`OBSERVATIONS:\n${update.observations.join("\n")}`);
|
|
828
945
|
if (update.reviews.length > 0) updateSections.push(`REVIEWS:\n${update.reviews.join("\n")}`);
|
|
829
946
|
const updateBody = updateSections.length > 0 ? updateSections.join("\n\n") : "(no new memories)";
|
|
947
|
+
const sparseMemoryDescription = update.observations.length + update.reviews.length === 0
|
|
948
|
+
? "No new memories were created during this interval."
|
|
949
|
+
: `Only ${update.observations.length + update.reviews.length} new memory record${update.observations.length + update.reviews.length === 1 ? " was" : "s were"} created during this interval.`;
|
|
950
|
+
const reviewerHistoryHint = reviewerEnabled
|
|
951
|
+
? " If a recurring concern warrants structural review, remember that the reviewer can inspect the primary agent's recorded chat directly with its chat-history tools; it is not limited to these sparse memories."
|
|
952
|
+
: "";
|
|
953
|
+
const activeTimeNotice = update.activeTimeFallback
|
|
954
|
+
? `\n\nACTIVE-WORK FALLBACK: ${sparseMemoryDescription} The primary agent has been actively working for ${coarseAgentTime(activeTimeSinceLastRunMs)} since the last contemplator run. Check whether it is productively making progress rather than being stuck in an unproductive loop.${reviewerHistoryHint} Prefer no_intervention if there is no grounded, useful action.`
|
|
955
|
+
: "";
|
|
830
956
|
const finalActionNames = reviewerEnabled
|
|
831
957
|
? "send_probe, request_review, or no_intervention"
|
|
832
958
|
: "send_probe or no_intervention";
|
|
833
959
|
const interventionInstruction = reviewerEnabled
|
|
834
960
|
? `You must end this update by calling exactly one final-action tool: ${finalActionNames}. The tool requirement is bookkeeping, not a reason to intervene. Prefer the argument-free no_intervention whenever no specific, grounded, materially useful intervention is clearly warranted or usefulness is uncertain. Use send_probe only for one unusually useful focused question, and request_review only when a deeper workflow or software review is justified. Never send a probe merely to satisfy the final-action requirement. If a tool warns about a bad memory citation, use search_memories and recall, then call a final-action tool again to replace it.`
|
|
835
961
|
: `You must end this update by calling exactly one final-action tool: ${finalActionNames}. The tool requirement is bookkeeping, not a reason to intervene. Prefer the argument-free no_intervention whenever no specific, grounded, materially useful probe is clearly warranted or usefulness is uncertain. Use send_probe only for one unusually useful focused question. Never send a probe merely to satisfy the final-action requirement. If send_probe warns about a bad memory citation, use search_memories and recall, then call a final-action tool again to replace it.`;
|
|
836
|
-
const prompt: Message = { role: "user", content: [{ type: "text", text: `NEW MEMORY UPDATE\n\n${updateBody}\n\nCUMULATIVE ACTIVITY: ${update.mainAgentOutputTokens} generated tokens; ${update.mainAgentToolCalls} tool calls; ${coarseAgentTime(update.mainAgentActiveTimeMs)} active.\n\nConsider these updates in the context of the accumulated memories. Prioritize reasoning gaps, contradictions, user-intent alignment, relevant overlooked alternatives, well-supported loops, and recurring structural patterns. ${interventionInstruction}` }], timestamp: Date.now() };
|
|
962
|
+
const prompt: Message = { role: "user", content: [{ type: "text", text: `NEW MEMORY UPDATE\n\n${updateBody}${activeTimeNotice}\n\nCUMULATIVE ACTIVITY: ${update.mainAgentOutputTokens} generated tokens; ${update.mainAgentToolCalls} tool calls; ${coarseAgentTime(update.mainAgentActiveTimeMs)} active.\n\nConsider these updates in the context of the accumulated memories. Prioritize reasoning gaps, contradictions, user-intent alignment, relevant overlooked alternatives, well-supported loops, and recurring structural patterns. ${interventionInstruction}` }], timestamp: Date.now() };
|
|
837
963
|
let intervention: Intervention | undefined;
|
|
838
964
|
let finalActionWarned = false;
|
|
839
965
|
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
@@ -1000,6 +1126,7 @@ export class Contemplator {
|
|
|
1000
1126
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
1001
1127
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
1002
1128
|
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
1129
|
+
activeTimeFallback: update.activeTimeFallback,
|
|
1003
1130
|
};
|
|
1004
1131
|
} else {
|
|
1005
1132
|
debugLog("contemplator.poisoned_update_released", { reason: failureMessage, observationCount: update.observations.length, reviewCount: update.reviews.length });
|
|
@@ -1031,7 +1158,12 @@ export class Contemplator {
|
|
|
1031
1158
|
: this.turnsSinceRun < this.runtime.config.contemplatorMinTurns
|
|
1032
1159
|
? "responses"
|
|
1033
1160
|
: "ready";
|
|
1161
|
+
const activeTimeSinceRunMs = Math.max(
|
|
1162
|
+
0,
|
|
1163
|
+
agentActiveTimeMs(ctx.sessionManager.getBranch() as Entry[]) - this.lastContemplatorActiveTimeMs,
|
|
1164
|
+
);
|
|
1034
1165
|
this.publishState(waitingFor, {
|
|
1166
|
+
activeTimeSinceRunMs,
|
|
1035
1167
|
lastCompletedAt: Date.now(),
|
|
1036
1168
|
lastError: failureMessage,
|
|
1037
1169
|
});
|
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" | "contemplatorMinTurns" | "summarizerRetriggerTokens" | "summarizerSamplingThresholdTokens";
|
|
13
|
+
type NumberSetting = "observeAfterTokens" | "compactAfterTokens" | "observerChunkMaxTokens" | "newMemoryPoolMaxTokens" | "oldMemoryPoolTargetTokens" | "agentMaxTurns" | "contemplatorMinNewObservations" | "contemplatorMinTurns" | "contemplatorMaxActiveMinutes" | "summarizerRetriggerTokens" | "summarizerSamplingThresholdTokens";
|
|
14
14
|
type BooleanSetting = "contemplatorEnabled" | "showContemplatorMessages" | "reviewerEnabled" | "summarizerEnabled" | "compactionObserverEnabled" | "showWorkerNotifications" | "passive" | "debugLog";
|
|
15
15
|
|
|
16
16
|
function modelLabel(model: ConfiguredModel | undefined): string {
|
|
@@ -239,6 +239,7 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
239
239
|
`Show contemplator messages: ${scalarLabel(runtime, "showContemplatorMessages")}`,
|
|
240
240
|
`Contemplator new-observation trigger (count): ${scalarLabel(runtime, "contemplatorMinNewObservations")}`,
|
|
241
241
|
`Contemplator response spacing (count): ${scalarLabel(runtime, "contemplatorMinTurns")}`,
|
|
242
|
+
`Contemplator maximum active-work interval (minutes): ${scalarLabel(runtime, "contemplatorMaxActiveMinutes")}`,
|
|
242
243
|
`Summarizer enabled: ${scalarLabel(runtime, "summarizerEnabled")}`,
|
|
243
244
|
`Summarizer model: ${memoryWorkerModelSettingLabel(runtime, "summarizer")}`,
|
|
244
245
|
`New memory pool protection budget (tokens): ${scalarLabel(runtime, "newMemoryPoolMaxTokens")}`,
|
|
@@ -298,6 +299,7 @@ export function registerSettingsCommand(pi: ExtensionAPI, runtime: Runtime): voi
|
|
|
298
299
|
["Old memory pool target (tokens, advisory):", "oldMemoryPoolTargetTokens", "Old memory pool target (tokens, advisory)"],
|
|
299
300
|
["Contemplator new-observation trigger (count):", "contemplatorMinNewObservations", "Contemplator new-observation trigger (count)"],
|
|
300
301
|
["Contemplator response spacing (count):", "contemplatorMinTurns", "Contemplator response spacing (count)"],
|
|
302
|
+
["Contemplator maximum active-work interval (minutes):", "contemplatorMaxActiveMinutes", "Contemplator maximum active-work interval (minutes)"],
|
|
301
303
|
["Summarizer old-pool retrigger growth (tokens):", "summarizerRetriggerTokens", "Summarizer old-pool retrigger growth (tokens)"],
|
|
302
304
|
["Summarizer input cap before sampling (tokens):", "summarizerSamplingThresholdTokens", "Summarizer input cap before sampling (tokens)"],
|
|
303
305
|
];
|
package/src/commands/status.ts
CHANGED
|
@@ -129,7 +129,7 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
129
129
|
`Cumulative agent time: ${formatDuration(agentActiveTimeMs(entries))}`,
|
|
130
130
|
`Observe source during compaction: ${runtime.config.compactionObserverEnabled === false ? "disabled" : "enabled"}`,
|
|
131
131
|
`Contemplator: ${runtime.config.contemplatorEnabled ? "enabled" : "disabled"}`,
|
|
132
|
-
`Contemplator trigger: ${runtime.contemplatorState.pendingObservations} observations / ${runtime.contemplatorState.pendingReviews} reviews pending; ${runtime.contemplatorState.responsesSinceRun} / ${runtime.config.contemplatorMinTurns} primary responses; ${contemplatorWaitingLabel(runtime.contemplatorState.waitingFor)}`,
|
|
132
|
+
`Contemplator trigger: ${runtime.contemplatorState.pendingObservations} observations / ${runtime.contemplatorState.pendingReviews} reviews pending; ${runtime.contemplatorState.responsesSinceRun} / ${runtime.config.contemplatorMinTurns} primary responses; ${formatDuration(runtime.contemplatorState.activeTimeSinceRunMs)} / ${formatDuration(runtime.config.contemplatorMaxActiveMinutes * 60_000)} active-work fallback; ${contemplatorWaitingLabel(runtime.contemplatorState.waitingFor)}`,
|
|
133
133
|
`Contemplator model: ${runtime.config.contemplatorModel ? `${runtime.config.contemplatorModel.provider}/${runtime.config.contemplatorModel.id}` : "current session model"}`,
|
|
134
134
|
`Contemplator messages: ${runtime.config.showContemplatorMessages ? "visible" : "hidden"}`,
|
|
135
135
|
`Structural reviewer: ${runtime.config.reviewerEnabled === false ? "disabled" : "enabled"}`,
|
package/src/config.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface Config {
|
|
|
67
67
|
contemplatorMinNewObservations: number;
|
|
68
68
|
/** Minimum primary-model responses after contemplator completion, or after delivery of its probe, before the next run. */
|
|
69
69
|
contemplatorMinTurns: number;
|
|
70
|
+
/** Maximum cumulative main-agent active time between contemplator attempts, excluding time spent waiting for the user. */
|
|
71
|
+
contemplatorMaxActiveMinutes: number;
|
|
70
72
|
/** Stateless loss-aware summarizer for the old memory pool. */
|
|
71
73
|
summarizerEnabled: boolean;
|
|
72
74
|
/** Additional old-pool tokens required before retrying an above-target pool. */
|
|
@@ -92,6 +94,7 @@ export const DEFAULTS: Config = {
|
|
|
92
94
|
reviewerEnabled: true,
|
|
93
95
|
contemplatorMinNewObservations: 8,
|
|
94
96
|
contemplatorMinTurns: 10,
|
|
97
|
+
contemplatorMaxActiveMinutes: 60,
|
|
95
98
|
summarizerEnabled: true,
|
|
96
99
|
summarizerRetriggerTokens: 2_000,
|
|
97
100
|
summarizerSamplingThresholdTokens: 60_000,
|
|
@@ -214,6 +217,7 @@ function normalizeSettingsConfig(value: Record<string, unknown>): Partial<Config
|
|
|
214
217
|
"agentMaxTurns",
|
|
215
218
|
"contemplatorMinNewObservations",
|
|
216
219
|
"contemplatorMinTurns",
|
|
220
|
+
"contemplatorMaxActiveMinutes",
|
|
217
221
|
"summarizerRetriggerTokens",
|
|
218
222
|
"summarizerSamplingThresholdTokens",
|
|
219
223
|
] as const;
|
|
@@ -131,6 +131,11 @@ export function registerConsolidationTrigger(pi: ExtensionAPI, runtime: Runtime)
|
|
|
131
131
|
scheduleSummarizer(pi, runtime, ctx as ConsolidationCtx);
|
|
132
132
|
});
|
|
133
133
|
runtime.setSettingsUpdateListener((ctx, settings) => {
|
|
134
|
+
if (settings.contemplatorMaxActiveMinutes !== undefined) {
|
|
135
|
+
// Re-evaluate immediately when the user lowers the maximum interval;
|
|
136
|
+
// otherwise it would wait for an unrelated primary-agent checkpoint.
|
|
137
|
+
runtime.notifyMemoryUpdate(ctx);
|
|
138
|
+
}
|
|
134
139
|
const affectsScheduling = settings.summarizerEnabled !== undefined ||
|
|
135
140
|
settings.newMemoryPoolMaxTokens !== undefined ||
|
|
136
141
|
settings.oldMemoryPoolTargetTokens !== undefined ||
|
package/src/runtime.ts
CHANGED
|
@@ -32,7 +32,7 @@ export type SessionSettings = Partial<Pick<Config,
|
|
|
32
32
|
| "compactAfterTokensMode" | "compactAfterTokensRatio"
|
|
33
33
|
| "newMemoryPoolMaxTokens" | "oldMemoryPoolTargetTokens" | "agentMaxTurns"
|
|
34
34
|
| "showWorkerNotifications" | "passive" | "compactionObserverEnabled" | "contemplatorEnabled" | "showContemplatorMessages" | "reviewerEnabled"
|
|
35
|
-
| "contemplatorMinNewObservations" | "contemplatorMinTurns"
|
|
35
|
+
| "contemplatorMinNewObservations" | "contemplatorMinTurns" | "contemplatorMaxActiveMinutes"
|
|
36
36
|
| "summarizerEnabled" | "summarizerRetriggerTokens" | "summarizerSamplingThresholdTokens"
|
|
37
37
|
| "debugLog"
|
|
38
38
|
>> & {
|
|
@@ -92,6 +92,8 @@ export interface ContemplatorRunState {
|
|
|
92
92
|
pendingReviews: number;
|
|
93
93
|
/** Completed primary-model responses since the current completion/probe-delivery spacing anchor. */
|
|
94
94
|
responsesSinceRun: number;
|
|
95
|
+
/** Cumulative main-agent active time since the latest contemplator attempt. */
|
|
96
|
+
activeTimeSinceRunMs: number;
|
|
95
97
|
waitingFor: "disabled" | "passive" | "observer" | "probe" | "memories" | "responses" | "ready" | "running" | "idle";
|
|
96
98
|
lastStartedAt?: number;
|
|
97
99
|
lastCompletedAt?: number;
|
|
@@ -148,7 +150,7 @@ export function computeSessionSettings(entries: readonly unknown[]): SessionSett
|
|
|
148
150
|
const numberKeys = [
|
|
149
151
|
"observeAfterTokens", "observerChunkMaxTokens", "compactAfterTokens",
|
|
150
152
|
"newMemoryPoolMaxTokens", "oldMemoryPoolTargetTokens", "agentMaxTurns",
|
|
151
|
-
"contemplatorMinNewObservations", "contemplatorMinTurns",
|
|
153
|
+
"contemplatorMinNewObservations", "contemplatorMinTurns", "contemplatorMaxActiveMinutes",
|
|
152
154
|
"summarizerRetriggerTokens", "summarizerSamplingThresholdTokens",
|
|
153
155
|
] as const;
|
|
154
156
|
for (const key of booleanKeys) if (typeof data[key] === "boolean") restored[key] = data[key];
|
|
@@ -225,6 +227,7 @@ export class Runtime {
|
|
|
225
227
|
pendingObservations: 0,
|
|
226
228
|
pendingReviews: 0,
|
|
227
229
|
responsesSinceRun: 0,
|
|
230
|
+
activeTimeSinceRunMs: 0,
|
|
228
231
|
waitingFor: "idle",
|
|
229
232
|
};
|
|
230
233
|
agentUsage: LlmUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, runs: 0 };
|
|
@@ -325,6 +328,7 @@ export class Runtime {
|
|
|
325
328
|
pendingObservations: 0,
|
|
326
329
|
pendingReviews: 0,
|
|
327
330
|
responsesSinceRun: 0,
|
|
331
|
+
activeTimeSinceRunMs: 0,
|
|
328
332
|
waitingFor: "idle",
|
|
329
333
|
};
|
|
330
334
|
}
|
|
@@ -7,9 +7,16 @@ import {
|
|
|
7
7
|
type MemoryCoverageCustomType,
|
|
8
8
|
} from "./types.js";
|
|
9
9
|
|
|
10
|
-
const SOURCE_ENTRY_TYPES = new Set(["
|
|
10
|
+
const SOURCE_ENTRY_TYPES = new Set(["custom_message", "branch_summary"]);
|
|
11
11
|
|
|
12
12
|
export function isSourceEntry(entry: Entry): boolean {
|
|
13
|
+
if (entry.type === "message") {
|
|
14
|
+
// Pi 0.87 persists its canonical system prompt as a message entry. It is
|
|
15
|
+
// extension/runtime configuration, not new conversation evidence; counting
|
|
16
|
+
// its large sections makes a fresh session immediately cross the observer
|
|
17
|
+
// threshold even though only a few hundred user-visible tokens exist.
|
|
18
|
+
return !isObject(entry.message) || entry.message.role !== "system";
|
|
19
|
+
}
|
|
13
20
|
return SOURCE_ENTRY_TYPES.has(entry.type);
|
|
14
21
|
}
|
|
15
22
|
|