@matthewfl/pi-contemplator 0.3.1 → 0.4.1
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 +269 -32
- package/src/commands/contemplator-view.ts +1 -1
- package/src/commands/settings.ts +3 -1
- package/src/commands/status.ts +3 -2
- package/src/config.ts +4 -0
- package/src/hooks/consolidation-trigger.ts +5 -0
- package/src/runtime.ts +9 -2
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.1",
|
|
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",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { agentLoop, type AgentContext, type AgentLoopConfig, type AgentMessage, type AgentTool, type StreamFn } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { Type, type Message, type Model } from "@earendil-works/pi-ai";
|
|
3
|
+
import { isRetryableAssistantError } from "@earendil-works/pi-ai/utils/retry";
|
|
3
4
|
import type { Static } from "typebox";
|
|
4
5
|
import { estimateTokens as estimateAgentMessageTokens, generateSummaryWithUsage } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -23,6 +24,8 @@ const CONTEMPLATOR_HISTORY_MAX_TRIGGER_TOKENS = 40_000;
|
|
|
23
24
|
const CONTEMPLATOR_HISTORY_CONTEXT_FRACTION = 0.25;
|
|
24
25
|
const CONTEMPLATOR_HISTORY_KEEP_RECENT_TOKENS = 12_000;
|
|
25
26
|
const CONTEMPLATOR_HISTORY_SUMMARY_RESERVE_TOKENS = 16_000;
|
|
27
|
+
const CONTEMPLATOR_UPDATE_MAX_TOKENS = 20_000;
|
|
28
|
+
const CONTEMPLATOR_UPDATE_CONTEXT_FRACTION = 0.2;
|
|
26
29
|
const CONTEMPLATOR_HISTORY_SUMMARY_INSTRUCTIONS = "This is an older prefix of a private contemplator transcript. Be concise. Preserve durable user intent, decisions, evidence, unresolved reasoning gaps, and review/probe outcomes. Newer transcript messages are retained verbatim after this checkpoint.";
|
|
27
30
|
|
|
28
31
|
function contemplatorMessageTokens(message: AgentMessage): number {
|
|
@@ -84,6 +87,8 @@ interface PendingUpdate {
|
|
|
84
87
|
mainAgentOutputTokens: number;
|
|
85
88
|
mainAgentToolCalls: number;
|
|
86
89
|
mainAgentActiveTimeMs: number;
|
|
90
|
+
/** True when cumulative active work, rather than memory volume, forced this update. */
|
|
91
|
+
activeTimeFallback: boolean;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
type Intervention =
|
|
@@ -143,6 +148,72 @@ function reviewSummaryLine(review: ReviewResult): string {
|
|
|
143
148
|
: `[${review.id}] ${review.scope} review concluded with no proposal — ${review.reason}`;
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
function memoryLineId(line: string): string | undefined {
|
|
152
|
+
return line.match(/^\[([0-9a-f]{12})\]/)?.[1];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function contemplatorUpdateBudgetTokens(model: Model<any>): number {
|
|
156
|
+
const contextWindow = Number(model.contextWindow);
|
|
157
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) return CONTEMPLATOR_UPDATE_MAX_TOKENS;
|
|
158
|
+
return Math.max(1_024, Math.min(CONTEMPLATOR_UPDATE_MAX_TOKENS, Math.floor(contextWindow * CONTEMPLATOR_UPDATE_CONTEXT_FRACTION)));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Keep the newest pending records within a bounded request budget. The complete
|
|
163
|
+
* ids are persisted as coverage separately, so intentionally omitted old
|
|
164
|
+
* records do not reappear after reload even though their bodies are not copied
|
|
165
|
+
* into the private contemplator transcript.
|
|
166
|
+
*/
|
|
167
|
+
function boundedContemplatorUpdate(observations: string[], reviews: string[], budgetTokens: number): {
|
|
168
|
+
observations: string[];
|
|
169
|
+
reviews: string[];
|
|
170
|
+
omitted: number;
|
|
171
|
+
} {
|
|
172
|
+
type Candidate = { kind: "observation" | "review"; index: number; line: string };
|
|
173
|
+
// Reviews are rare terminal outcomes and get first claim on the budget. Within
|
|
174
|
+
// each section, newest records are selected first and rendered chronologically.
|
|
175
|
+
const candidates: Candidate[] = [
|
|
176
|
+
...reviews.map((line, index) => ({ kind: "review" as const, index, line })).reverse(),
|
|
177
|
+
...observations.map((line, index) => ({ kind: "observation" as const, index, line })).reverse(),
|
|
178
|
+
];
|
|
179
|
+
const selected = new Map<Candidate, string>();
|
|
180
|
+
let remaining = budgetTokens;
|
|
181
|
+
for (const candidate of candidates) {
|
|
182
|
+
const tokens = Math.max(1, Math.ceil(candidate.line.length / 4));
|
|
183
|
+
if (tokens <= remaining) {
|
|
184
|
+
selected.set(candidate, candidate.line);
|
|
185
|
+
remaining -= tokens;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Always expose at least the newest record. An individually enormous memory
|
|
189
|
+
// is excerpted rather than defeating the safety cap; recall retains the full
|
|
190
|
+
// source under the id shown at the beginning of the excerpt.
|
|
191
|
+
if (selected.size === 0 && remaining > 0) {
|
|
192
|
+
const maxChars = Math.max(256, remaining * 4);
|
|
193
|
+
selected.set(candidate, `${candidate.line.slice(0, Math.max(1, maxChars - 96))}\n… [memory excerpt capped; use recall for the full record]`);
|
|
194
|
+
remaining = 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const selectedObservations = candidates
|
|
198
|
+
.filter((candidate) => candidate.kind === "observation" && selected.has(candidate))
|
|
199
|
+
.sort((left, right) => left.index - right.index)
|
|
200
|
+
.map((candidate) => selected.get(candidate)!);
|
|
201
|
+
const selectedReviews = candidates
|
|
202
|
+
.filter((candidate) => candidate.kind === "review" && selected.has(candidate))
|
|
203
|
+
.sort((left, right) => left.index - right.index)
|
|
204
|
+
.map((candidate) => selected.get(candidate)!);
|
|
205
|
+
return {
|
|
206
|
+
observations: selectedObservations,
|
|
207
|
+
reviews: selectedReviews,
|
|
208
|
+
omitted: observations.length + reviews.length - selected.size,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function retryableContemplatorFailure(message: string): boolean {
|
|
213
|
+
return isRetryableAssistantError({ stopReason: "error", errorMessage: message } as any)
|
|
214
|
+
|| /produced no progress|runtime ceiling/i.test(message);
|
|
215
|
+
}
|
|
216
|
+
|
|
146
217
|
function reviewRequestKey(request: RequestReviewArgs): string {
|
|
147
218
|
return `${request.scope}:${hashId(`${request.evidence}\n${request.concern}`)}`;
|
|
148
219
|
}
|
|
@@ -171,6 +242,8 @@ function coarseAgentTime(durationMs: number): string {
|
|
|
171
242
|
}
|
|
172
243
|
const CONTEMPLATOR_MESSAGE = "om.contemplator.message";
|
|
173
244
|
const CONTEMPLATOR_STATE = "om.contemplator.state";
|
|
245
|
+
/** Durable per-branch baseline for the cumulative active-work fallback trigger. */
|
|
246
|
+
const CONTEMPLATOR_RUN = "om.contemplator.run";
|
|
174
247
|
const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
|
|
175
248
|
const REVIEW_PROPOSAL_MESSAGE = "om.review.proposal";
|
|
176
249
|
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 +365,8 @@ export class Contemplator {
|
|
|
292
365
|
private queuedProbeIds = new Set<string>();
|
|
293
366
|
/** Probes whose provider-context delivery will establish the next response-spacing anchor. */
|
|
294
367
|
private probeCooldownPendingIds = new Set<string>();
|
|
368
|
+
/** Preserve the configured response gap after a probe actually reaches provider context. */
|
|
369
|
+
private probeDeliverySpacingActive = false;
|
|
295
370
|
private sessionGeneration = 0;
|
|
296
371
|
private latestCtx: MemoryUpdateCtx | undefined;
|
|
297
372
|
/** Completed primary-model responses since the current completion/probe-delivery spacing anchor. */
|
|
@@ -301,6 +376,10 @@ export class Contemplator {
|
|
|
301
376
|
private restoredTipId: string | undefined;
|
|
302
377
|
/** Start of the unpersisted portion of the current main-agent run. */
|
|
303
378
|
private agentActiveSince: number | undefined;
|
|
379
|
+
/** Cumulative active time at the latest contemplator attempt on this branch. */
|
|
380
|
+
private lastContemplatorActiveTimeMs = 0;
|
|
381
|
+
/** Fires while the primary agent remains active, including inside one long tool call. */
|
|
382
|
+
private activeTimeFallbackTimer: ReturnType<typeof setTimeout> | undefined;
|
|
304
383
|
|
|
305
384
|
constructor(private readonly pi: ExtensionAPI, private readonly runtime: Runtime) {}
|
|
306
385
|
|
|
@@ -322,12 +401,14 @@ export class Contemplator {
|
|
|
322
401
|
return box;
|
|
323
402
|
});
|
|
324
403
|
this.runtime.setMemoryUpdateListener((ctx) => this.withDebugContext(ctx, () => this.observeTurn(ctx)));
|
|
325
|
-
this.pi.on("agent_start", () => {
|
|
404
|
+
this.pi.on("agent_start", (_event: unknown, ctx: ExtensionContext) => {
|
|
326
405
|
this.agentActiveSince = Date.now();
|
|
406
|
+
this.withDebugContext(ctx, () => this.scheduleActiveTimeFallback(ctx));
|
|
327
407
|
});
|
|
328
408
|
this.pi.on("agent_end", (_event: unknown, ctx: ExtensionContext) => {
|
|
329
409
|
this.persistAgentActivity(ctx);
|
|
330
410
|
this.agentActiveSince = undefined;
|
|
411
|
+
this.clearActiveTimeFallbackTimer();
|
|
331
412
|
});
|
|
332
413
|
this.pi.on("session_start", (event: any, ctx: ExtensionContext) => {
|
|
333
414
|
this.sessionGeneration++;
|
|
@@ -336,11 +417,20 @@ export class Contemplator {
|
|
|
336
417
|
this.consecutiveFlushFailures = 0;
|
|
337
418
|
const generation = this.sessionGeneration;
|
|
338
419
|
this.agentActiveSince = undefined;
|
|
420
|
+
this.clearActiveTimeFallbackTimer();
|
|
339
421
|
// AgentSession preserves its steering queue across extension reloads. An
|
|
340
422
|
// undelivered tracking entry therefore still has a live queued message;
|
|
341
423
|
// restoring it here would enqueue the same probe a second time.
|
|
342
424
|
const reload = event?.reason === "reload";
|
|
343
425
|
this.restore(ctx, true, reload, reload);
|
|
426
|
+
// Reload replaces the extension runtime without restarting an already-active
|
|
427
|
+
// agent run, so no new agent_start event is guaranteed. Recover the clock
|
|
428
|
+
// from Pi's live state; elapsed time before reload may be unavailable, but
|
|
429
|
+
// the current run remains monitored rather than going unarmed indefinitely.
|
|
430
|
+
if (!ctx.isIdle()) {
|
|
431
|
+
this.agentActiveSince = Date.now();
|
|
432
|
+
this.withDebugContext(ctx, () => this.scheduleActiveTimeFallback(ctx));
|
|
433
|
+
}
|
|
344
434
|
// Reconstruct and schedule durable memory immediately. In particular, a
|
|
345
435
|
// reload after a failed run must not silently mark its pending backlog seen.
|
|
346
436
|
queueMicrotask(() => {
|
|
@@ -353,15 +443,22 @@ export class Contemplator {
|
|
|
353
443
|
this.running = false;
|
|
354
444
|
this.consecutiveFlushFailures = 0;
|
|
355
445
|
this.agentActiveSince = undefined;
|
|
446
|
+
this.clearActiveTimeFallbackTimer();
|
|
356
447
|
// Pending steering messages remain queued while navigating the tree.
|
|
357
448
|
this.restore(ctx, true, true);
|
|
358
449
|
});
|
|
359
|
-
this.pi.on("session_shutdown", () => {
|
|
450
|
+
this.pi.on("session_shutdown", (_event: unknown, ctx: ExtensionContext) => {
|
|
451
|
+
// On extension reload Pi shuts down the old runtime before starting the
|
|
452
|
+
// replacement. Persist its final live interval without launching workers,
|
|
453
|
+
// so the replacement can continue from the exact cumulative clock.
|
|
454
|
+
this.persistAgentActivity(ctx, false, false);
|
|
360
455
|
this.sessionGeneration++;
|
|
361
456
|
this.flushEpoch++;
|
|
362
457
|
this.running = false;
|
|
363
458
|
this.consecutiveFlushFailures = 0;
|
|
364
459
|
this.agentActiveSince = undefined;
|
|
460
|
+
this.clearActiveTimeFallbackTimer();
|
|
461
|
+
this.lastContemplatorActiveTimeMs = 0;
|
|
365
462
|
this.history = [];
|
|
366
463
|
this.historyEntryIds = [];
|
|
367
464
|
this.pending = undefined;
|
|
@@ -375,6 +472,7 @@ export class Contemplator {
|
|
|
375
472
|
this.deliveredProbeIds.clear();
|
|
376
473
|
this.queuedProbeIds.clear();
|
|
377
474
|
this.probeCooldownPendingIds.clear();
|
|
475
|
+
this.probeDeliverySpacingActive = false;
|
|
378
476
|
this.latestCtx = undefined;
|
|
379
477
|
this.turnsSinceRun = 0;
|
|
380
478
|
this.assistantResponsesInCurrentTurn = 0;
|
|
@@ -384,6 +482,7 @@ export class Contemplator {
|
|
|
384
482
|
pendingObservations: 0,
|
|
385
483
|
pendingReviews: 0,
|
|
386
484
|
responsesSinceRun: 0,
|
|
485
|
+
activeTimeSinceRunMs: 0,
|
|
387
486
|
waitingFor: "idle",
|
|
388
487
|
};
|
|
389
488
|
});
|
|
@@ -419,6 +518,9 @@ export class Contemplator {
|
|
|
419
518
|
// This records one wall-clock interval regardless of how many tools were
|
|
420
519
|
// running concurrently; persistAgentActivity restarts the shared clock.
|
|
421
520
|
this.persistAgentActivity(ctx);
|
|
521
|
+
// A long autonomous turn can perform many tools without ending a model
|
|
522
|
+
// turn. Recheck the active-work fallback at each completed tool batch.
|
|
523
|
+
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
422
524
|
});
|
|
423
525
|
this.pi.on("context", (event: any, ctx: ExtensionContext) => {
|
|
424
526
|
const deliveredMessages = event.messages?.filter((message: any) => message?.role === "custom" && message.customType === CONTEMPLATOR_SUGGESTION && typeof message.details?.probeId === "string") ?? [];
|
|
@@ -444,6 +546,7 @@ export class Contemplator {
|
|
|
444
546
|
if (cooldownAnchored) {
|
|
445
547
|
// Probe spacing begins only once Pi proves the probe reached an actual
|
|
446
548
|
// provider context. Responses generated before this point do not count.
|
|
549
|
+
this.probeDeliverySpacingActive = true;
|
|
447
550
|
this.turnsSinceRun = 0;
|
|
448
551
|
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
449
552
|
}
|
|
@@ -458,18 +561,46 @@ export class Contemplator {
|
|
|
458
561
|
});
|
|
459
562
|
}
|
|
460
563
|
|
|
461
|
-
private
|
|
564
|
+
private clearActiveTimeFallbackTimer(): void {
|
|
565
|
+
if (this.activeTimeFallbackTimer !== undefined) clearTimeout(this.activeTimeFallbackTimer);
|
|
566
|
+
this.activeTimeFallbackTimer = undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private scheduleActiveTimeFallback(ctx: MemoryUpdateCtx): void {
|
|
570
|
+
this.clearActiveTimeFallbackTimer();
|
|
571
|
+
if (this.agentActiveSince === undefined || !this.runtime.config.contemplatorEnabled || this.runtime.config.passive) return;
|
|
572
|
+
const persistedActiveMs = agentActiveTimeMs(ctx.sessionManager.getBranch() as Entry[]);
|
|
573
|
+
const liveActiveMs = Math.max(0, Date.now() - this.agentActiveSince);
|
|
574
|
+
const elapsedMs = Math.max(0, persistedActiveMs + liveActiveMs - this.lastContemplatorActiveTimeMs);
|
|
575
|
+
const thresholdMs = this.runtime.config.contemplatorMaxActiveMinutes * 60_000;
|
|
576
|
+
const delayMs = Math.max(1, thresholdMs - elapsedMs);
|
|
577
|
+
this.activeTimeFallbackTimer = setTimeout(() => {
|
|
578
|
+
this.activeTimeFallbackTimer = undefined;
|
|
579
|
+
if (this.agentActiveSince === undefined) return;
|
|
580
|
+
this.withDebugContext(ctx, () => {
|
|
581
|
+
// Materialize the live interval before observeTurn reads the ledger. This
|
|
582
|
+
// lets a sleep/long tool trigger contemplation while it is still running.
|
|
583
|
+
this.persistAgentActivity(ctx, false);
|
|
584
|
+
this.observeTurn(ctx);
|
|
585
|
+
});
|
|
586
|
+
}, delayMs);
|
|
587
|
+
(this.activeTimeFallbackTimer as { unref?: () => void }).unref?.();
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private persistAgentActivity(ctx: MemoryUpdateCtx, reschedule = true, notify = true): void {
|
|
462
591
|
const startedAt = this.agentActiveSince;
|
|
463
592
|
if (startedAt === undefined) return;
|
|
464
593
|
const endedAt = Date.now();
|
|
465
594
|
this.agentActiveSince = endedAt;
|
|
466
595
|
const durationMs = Math.max(0, endedAt - startedAt);
|
|
467
|
-
if (durationMs
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
596
|
+
if (durationMs > 0) {
|
|
597
|
+
this.pi.appendEntry(OM_AGENT_ACTIVITY, { version: 1, durationMs, endedAt });
|
|
598
|
+
// Notify only after appendEntry so active-time schedulers always observe the
|
|
599
|
+
// checkpoint, regardless of Pi's ordering between independent event handlers.
|
|
600
|
+
if (notify) this.runtime.notifyAgentActivity(ctx);
|
|
601
|
+
debugLog("agent.activity_recorded", { durationMs });
|
|
602
|
+
}
|
|
603
|
+
if (reschedule) this.scheduleActiveTimeFallback(ctx);
|
|
473
604
|
}
|
|
474
605
|
|
|
475
606
|
private withDebugContext<T>(ctx: MemoryUpdateCtx, fn: () => T): T {
|
|
@@ -493,6 +624,7 @@ export class Contemplator {
|
|
|
493
624
|
pendingObservations: this.pending?.observations.length ?? 0,
|
|
494
625
|
pendingReviews: this.pending?.reviews.length ?? 0,
|
|
495
626
|
responsesSinceRun: this.turnsSinceRun,
|
|
627
|
+
activeTimeSinceRunMs: this.runtime.contemplatorState.activeTimeSinceRunMs,
|
|
496
628
|
waitingFor,
|
|
497
629
|
...overrides,
|
|
498
630
|
};
|
|
@@ -509,10 +641,13 @@ export class Contemplator {
|
|
|
509
641
|
const historyMessagesByEntryId = new Map<string, AgentMessage>();
|
|
510
642
|
const checkpointCoveredObservationIds = new Set<string>();
|
|
511
643
|
const checkpointCoveredReviewIds = new Set<string>();
|
|
644
|
+
let latestRunActiveTimeMs: number | undefined;
|
|
645
|
+
let probeDeliveredAfterLatestRun = false;
|
|
512
646
|
let resetProjection: ReturnType<typeof fullProjection> | undefined;
|
|
513
647
|
if (resetTracking) {
|
|
514
648
|
this.deliveredProbeIds.clear();
|
|
515
649
|
this.probeCooldownPendingIds.clear();
|
|
650
|
+
this.probeDeliverySpacingActive = false;
|
|
516
651
|
if (!retainQueuedIds) this.queuedProbeIds.clear();
|
|
517
652
|
this.inFlightReviewIds.clear();
|
|
518
653
|
this.resolvingReviewIds.clear();
|
|
@@ -529,11 +664,19 @@ export class Contemplator {
|
|
|
529
664
|
pendingObservations: 0,
|
|
530
665
|
pendingReviews: 0,
|
|
531
666
|
responsesSinceRun: 0,
|
|
667
|
+
activeTimeSinceRunMs: 0,
|
|
532
668
|
waitingFor: "idle",
|
|
533
669
|
};
|
|
534
670
|
}
|
|
535
671
|
const undeliveredSuggestions = new Map<string, string>();
|
|
536
672
|
for (const entry of entries) {
|
|
673
|
+
if (entry.customType === CONTEMPLATOR_RUN && entry.data && typeof entry.data === "object") {
|
|
674
|
+
const activeTimeMs = (entry.data as { mainAgentActiveTimeMs?: unknown }).mainAgentActiveTimeMs;
|
|
675
|
+
if (typeof activeTimeMs === "number" && Number.isFinite(activeTimeMs) && activeTimeMs >= 0) {
|
|
676
|
+
latestRunActiveTimeMs = activeTimeMs;
|
|
677
|
+
probeDeliveredAfterLatestRun = false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
537
680
|
if (entry.customType === CONTEMPLATOR_STATE && entry.data && typeof entry.data === "object") {
|
|
538
681
|
const state = entry.data as { history?: unknown };
|
|
539
682
|
if (Array.isArray(state.history)) {
|
|
@@ -543,6 +686,11 @@ export class Contemplator {
|
|
|
543
686
|
}
|
|
544
687
|
if (entry.customType === CONTEMPLATOR_MESSAGE && entry.data && typeof entry.data === "object") {
|
|
545
688
|
const data = entry.data as { message?: unknown; compacted?: unknown; retainedMessageEntryIds?: unknown; coveredObservationIds?: unknown; coveredReviewIds?: unknown };
|
|
689
|
+
// Coverage may be broader than the rendered prompt when a long-dead
|
|
690
|
+
// contemplator returns to a large backlog. Persisting compact id lists
|
|
691
|
+
// avoids copying every old memory body while keeping restore stable.
|
|
692
|
+
if (Array.isArray(data.coveredObservationIds)) for (const id of data.coveredObservationIds) if (typeof id === "string") checkpointCoveredObservationIds.add(id);
|
|
693
|
+
if (Array.isArray(data.coveredReviewIds)) for (const id of data.coveredReviewIds) if (typeof id === "string") checkpointCoveredReviewIds.add(id);
|
|
546
694
|
const message = data.message;
|
|
547
695
|
if (message && typeof message === "object") {
|
|
548
696
|
const typedMessage = message as AgentMessage;
|
|
@@ -553,8 +701,6 @@ export class Contemplator {
|
|
|
553
701
|
: [];
|
|
554
702
|
this.history = [typedMessage, ...retainedIds.map((id) => historyMessagesByEntryId.get(id)!)];
|
|
555
703
|
this.historyEntryIds = [entry.id, ...retainedIds];
|
|
556
|
-
if (Array.isArray(data.coveredObservationIds)) for (const id of data.coveredObservationIds) if (typeof id === "string") checkpointCoveredObservationIds.add(id);
|
|
557
|
-
if (Array.isArray(data.coveredReviewIds)) for (const id of data.coveredReviewIds) if (typeof id === "string") checkpointCoveredReviewIds.add(id);
|
|
558
704
|
} else {
|
|
559
705
|
this.history.push(typedMessage);
|
|
560
706
|
this.historyEntryIds.push(entry.id);
|
|
@@ -607,12 +753,19 @@ export class Contemplator {
|
|
|
607
753
|
if (typeof data.probeId !== "string") continue;
|
|
608
754
|
if (data.delivered === true) {
|
|
609
755
|
this.deliveredProbeIds.add(data.probeId);
|
|
756
|
+
probeDeliveredAfterLatestRun = true;
|
|
610
757
|
undeliveredSuggestions.delete(data.probeId);
|
|
611
758
|
} else if (typeof data.suggestion === "string") {
|
|
612
759
|
undeliveredSuggestions.set(data.probeId, data.suggestion);
|
|
613
760
|
}
|
|
614
761
|
}
|
|
615
762
|
}
|
|
763
|
+
// A malformed/foreign marker must not regress a valid live baseline during
|
|
764
|
+
// an ordinary tip refresh. A true session/tree restore with no valid marker
|
|
765
|
+
// intentionally starts this branch's cumulative baseline at zero.
|
|
766
|
+
if (latestRunActiveTimeMs !== undefined) this.lastContemplatorActiveTimeMs = latestRunActiveTimeMs;
|
|
767
|
+
else if (resetTracking) this.lastContemplatorActiveTimeMs = 0;
|
|
768
|
+
this.probeDeliverySpacingActive = probeDeliveredAfterLatestRun;
|
|
616
769
|
if (resetTracking && resetProjection) {
|
|
617
770
|
// Successful update prompts and private-history compaction checkpoints are
|
|
618
771
|
// the durable coverage record. Checkpoints carry ids from prompts replaced
|
|
@@ -681,6 +834,10 @@ export class Contemplator {
|
|
|
681
834
|
});
|
|
682
835
|
return;
|
|
683
836
|
}
|
|
837
|
+
const cumulativeActiveTimeMs = agentActiveTimeMs(branchEntries) + (this.agentActiveSince === undefined ? 0 : Math.max(0, Date.now() - this.agentActiveSince));
|
|
838
|
+
const activeTimeSinceRunMs = Math.max(0, cumulativeActiveTimeMs - this.lastContemplatorActiveTimeMs);
|
|
839
|
+
const activeTimeThresholdMs = this.runtime.config.contemplatorMaxActiveMinutes * 60_000;
|
|
840
|
+
const activeTimeDue = activeTimeSinceRunMs >= activeTimeThresholdMs;
|
|
684
841
|
const projection = fullProjection(branchEntries);
|
|
685
842
|
const observations = projection.observations.map((item) => `[${item.id}] ${item.content}`);
|
|
686
843
|
const reviews = projection.reviews ?? [];
|
|
@@ -704,11 +861,25 @@ export class Contemplator {
|
|
|
704
861
|
reviews: mergeMemoryLines(this.pending?.reviews ?? [], newReviews),
|
|
705
862
|
mainAgentOutputTokens: assistantOutputTokens(branchEntries),
|
|
706
863
|
mainAgentToolCalls: assistantToolCallCount(branchEntries),
|
|
707
|
-
mainAgentActiveTimeMs:
|
|
864
|
+
mainAgentActiveTimeMs: cumulativeActiveTimeMs,
|
|
865
|
+
activeTimeFallback: this.pending?.activeTimeFallback ?? false,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
if (activeTimeDue && !this.pending) {
|
|
869
|
+
this.pending = {
|
|
870
|
+
observations: [],
|
|
871
|
+
reviews: [],
|
|
872
|
+
mainAgentOutputTokens: assistantOutputTokens(branchEntries),
|
|
873
|
+
mainAgentToolCalls: assistantToolCallCount(branchEntries),
|
|
874
|
+
mainAgentActiveTimeMs: cumulativeActiveTimeMs,
|
|
875
|
+
activeTimeFallback: true,
|
|
708
876
|
};
|
|
877
|
+
} else if (activeTimeDue && this.pending) {
|
|
878
|
+
this.pending.activeTimeFallback = true;
|
|
709
879
|
}
|
|
710
880
|
if (!this.pending) {
|
|
711
|
-
this.
|
|
881
|
+
this.scheduleActiveTimeFallback(ctx);
|
|
882
|
+
this.publishState(this.running ? "running" : this.probeCooldownPendingIds.size > 0 ? "probe" : "idle", { activeTimeSinceRunMs });
|
|
712
883
|
return;
|
|
713
884
|
}
|
|
714
885
|
// Activity values are cumulative send-time snapshots, not values frozen when
|
|
@@ -716,15 +887,17 @@ export class Contemplator {
|
|
|
716
887
|
// while that batch waits for its memory/turn thresholds.
|
|
717
888
|
this.pending.mainAgentOutputTokens = assistantOutputTokens(branchEntries);
|
|
718
889
|
this.pending.mainAgentToolCalls = assistantToolCallCount(branchEntries);
|
|
719
|
-
this.pending.mainAgentActiveTimeMs =
|
|
890
|
+
this.pending.mainAgentActiveTimeMs = cumulativeActiveTimeMs;
|
|
720
891
|
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations;
|
|
892
|
+
const forcedByActiveTime = this.pending.activeTimeFallback;
|
|
721
893
|
if (this.probeCooldownPendingIds.size > 0) {
|
|
722
|
-
this.publishState("probe");
|
|
894
|
+
this.publishState("probe", { activeTimeSinceRunMs });
|
|
723
895
|
debugLog("contemplator.waiting", { reason: "probe_delivery", pendingProbeCount: this.probeCooldownPendingIds.size });
|
|
724
896
|
return;
|
|
725
897
|
}
|
|
726
|
-
|
|
727
|
-
|
|
898
|
+
const waitingForDeliveredProbeResponses = this.probeDeliverySpacingActive && this.turnsSinceRun < this.runtime.config.contemplatorMinTurns;
|
|
899
|
+
if (waitingForDeliveredProbeResponses || (!forcedByActiveTime && (!enoughMemories || this.turnsSinceRun < this.runtime.config.contemplatorMinTurns))) {
|
|
900
|
+
this.publishState(waitingForDeliveredProbeResponses || enoughMemories ? "responses" : "memories", { activeTimeSinceRunMs });
|
|
728
901
|
debugLog("contemplator.waiting", {
|
|
729
902
|
enoughMemories,
|
|
730
903
|
turnsSinceRun: this.turnsSinceRun,
|
|
@@ -733,11 +906,13 @@ export class Contemplator {
|
|
|
733
906
|
});
|
|
734
907
|
return;
|
|
735
908
|
}
|
|
736
|
-
this.publishState(this.running ? "running" : "ready");
|
|
909
|
+
this.publishState(this.running ? "running" : "ready", { activeTimeSinceRunMs });
|
|
737
910
|
debugLog("contemplator.triggered", {
|
|
738
911
|
pendingObservationCount: this.pending.observations.length,
|
|
739
912
|
pendingReviewCount: this.pending.reviews.length,
|
|
740
913
|
turnsSinceRun: this.turnsSinceRun,
|
|
914
|
+
forcedByActiveTime,
|
|
915
|
+
activeTimeSinceRunMs,
|
|
741
916
|
});
|
|
742
917
|
void this.flush(ctx);
|
|
743
918
|
}
|
|
@@ -756,6 +931,19 @@ export class Contemplator {
|
|
|
756
931
|
update.mainAgentOutputTokens = assistantOutputTokens(branchEntriesAtStart);
|
|
757
932
|
update.mainAgentToolCalls = assistantToolCallCount(branchEntriesAtStart);
|
|
758
933
|
update.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntriesAtStart);
|
|
934
|
+
const activeTimeSinceLastRunMs = Math.max(0, update.mainAgentActiveTimeMs - this.lastContemplatorActiveTimeMs);
|
|
935
|
+
// Reset the active-work interval at attempt start, including failed attempts,
|
|
936
|
+
// so a broken provider cannot create an immediate background retry loop. The
|
|
937
|
+
// tiny durable marker makes reloads and tree navigation preserve that backoff.
|
|
938
|
+
this.lastContemplatorActiveTimeMs = update.mainAgentActiveTimeMs;
|
|
939
|
+
this.probeDeliverySpacingActive = false;
|
|
940
|
+
this.scheduleActiveTimeFallback(ctx);
|
|
941
|
+
this.pi.appendEntry(CONTEMPLATOR_RUN, {
|
|
942
|
+
version: 1,
|
|
943
|
+
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
944
|
+
startedAt: Date.now(),
|
|
945
|
+
});
|
|
946
|
+
this.markTipPersisted(ctx);
|
|
759
947
|
this.pending = undefined;
|
|
760
948
|
const turnsBeforeRun = this.turnsSinceRun;
|
|
761
949
|
const sessionGeneration = this.sessionGeneration;
|
|
@@ -764,6 +952,7 @@ export class Contemplator {
|
|
|
764
952
|
this.turnsSinceRun = 0;
|
|
765
953
|
const startedAt = Date.now();
|
|
766
954
|
let failed = false;
|
|
955
|
+
let transientFailure = false;
|
|
767
956
|
let failureMessage: string | undefined;
|
|
768
957
|
let workerNotified = false;
|
|
769
958
|
let promptPersisted = false;
|
|
@@ -797,6 +986,7 @@ export class Contemplator {
|
|
|
797
986
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
798
987
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
799
988
|
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
989
|
+
activeTimeFallback: update.activeTimeFallback,
|
|
800
990
|
};
|
|
801
991
|
} else {
|
|
802
992
|
debugLog("contemplator.poisoned_update_released", { reason: resolved.reason, observationCount: update.observations.length, reviewCount: update.reviews.length });
|
|
@@ -823,17 +1013,41 @@ export class Contemplator {
|
|
|
823
1013
|
workerNotified = true;
|
|
824
1014
|
}
|
|
825
1015
|
const reviewerEnabled = this.runtime.config.reviewerEnabled;
|
|
1016
|
+
const boundedUpdate = boundedContemplatorUpdate(
|
|
1017
|
+
update.observations,
|
|
1018
|
+
update.reviews,
|
|
1019
|
+
contemplatorUpdateBudgetTokens(resolved.model as Model<any>),
|
|
1020
|
+
);
|
|
826
1021
|
const updateSections: string[] = [];
|
|
827
|
-
if (
|
|
828
|
-
if (
|
|
1022
|
+
if (boundedUpdate.observations.length > 0) updateSections.push(`OBSERVATIONS:\n${boundedUpdate.observations.join("\n")}`);
|
|
1023
|
+
if (boundedUpdate.reviews.length > 0) updateSections.push(`REVIEWS:\n${boundedUpdate.reviews.join("\n")}`);
|
|
1024
|
+
if (boundedUpdate.omitted > 0) updateSections.push(`BACKLOG SAFETY CAP: ${boundedUpdate.omitted} older pending memory record${boundedUpdate.omitted === 1 ? " was" : "s were"} omitted from this update. They remain searchable and recallable; focus on the newest records shown here.`);
|
|
829
1025
|
const updateBody = updateSections.length > 0 ? updateSections.join("\n\n") : "(no new memories)";
|
|
1026
|
+
const coveredObservationIds = update.observations.map(memoryLineId).filter((id): id is string => id !== undefined);
|
|
1027
|
+
const coveredReviewIds = update.reviews.map(memoryLineId).filter((id): id is string => id !== undefined);
|
|
1028
|
+
debugLog("contemplator.update_bounded", {
|
|
1029
|
+
observationCount: update.observations.length,
|
|
1030
|
+
reviewCount: update.reviews.length,
|
|
1031
|
+
renderedObservationCount: boundedUpdate.observations.length,
|
|
1032
|
+
renderedReviewCount: boundedUpdate.reviews.length,
|
|
1033
|
+
omittedCount: boundedUpdate.omitted,
|
|
1034
|
+
});
|
|
1035
|
+
const sparseMemoryDescription = update.observations.length + update.reviews.length === 0
|
|
1036
|
+
? "No new memories were created during this interval."
|
|
1037
|
+
: `Only ${update.observations.length + update.reviews.length} new memory record${update.observations.length + update.reviews.length === 1 ? " was" : "s were"} created during this interval.`;
|
|
1038
|
+
const reviewerHistoryHint = reviewerEnabled
|
|
1039
|
+
? " 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."
|
|
1040
|
+
: "";
|
|
1041
|
+
const activeTimeNotice = update.activeTimeFallback
|
|
1042
|
+
? `\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.`
|
|
1043
|
+
: "";
|
|
830
1044
|
const finalActionNames = reviewerEnabled
|
|
831
1045
|
? "send_probe, request_review, or no_intervention"
|
|
832
1046
|
: "send_probe or no_intervention";
|
|
833
1047
|
const interventionInstruction = reviewerEnabled
|
|
834
1048
|
? `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
1049
|
: `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() };
|
|
1050
|
+
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
1051
|
let intervention: Intervention | undefined;
|
|
838
1052
|
let finalActionWarned = false;
|
|
839
1053
|
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
@@ -935,6 +1149,7 @@ export class Contemplator {
|
|
|
935
1149
|
const errorMessage = "errorMessage" in assistant && typeof assistant.errorMessage === "string"
|
|
936
1150
|
? assistant.errorMessage
|
|
937
1151
|
: `Contemplator model ${assistant.stopReason}`;
|
|
1152
|
+
transientFailure = assistant.stopReason === "error" && retryableContemplatorFailure(errorMessage);
|
|
938
1153
|
throw new Error(errorMessage);
|
|
939
1154
|
}
|
|
940
1155
|
if (!intervention && invocation < CONTEMPLATOR_MAX_INVOCATIONS) {
|
|
@@ -949,7 +1164,9 @@ export class Contemplator {
|
|
|
949
1164
|
for (const message of runMessages) {
|
|
950
1165
|
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
951
1166
|
this.history.push(message);
|
|
952
|
-
this.historyEntryIds.push(this.appendContemplatorHistoryEntry(ctx,
|
|
1167
|
+
this.historyEntryIds.push(this.appendContemplatorHistoryEntry(ctx, message === prompt
|
|
1168
|
+
? { version: 2, message, coveredObservationIds, coveredReviewIds }
|
|
1169
|
+
: { version: 1, message }));
|
|
953
1170
|
promptPersisted = true;
|
|
954
1171
|
this.markTipPersisted(ctx);
|
|
955
1172
|
}
|
|
@@ -989,10 +1206,12 @@ export class Contemplator {
|
|
|
989
1206
|
} catch (error) {
|
|
990
1207
|
failed = true;
|
|
991
1208
|
failureMessage = error instanceof Error ? error.message : String(error);
|
|
992
|
-
|
|
1209
|
+
transientFailure ||= retryableContemplatorFailure(failureMessage);
|
|
1210
|
+
debugLog("contemplator.error", { errorMessage: failureMessage, transientFailure });
|
|
993
1211
|
if (sessionGeneration === this.sessionGeneration && !promptPersisted) {
|
|
994
|
-
this.consecutiveFlushFailures
|
|
995
|
-
|
|
1212
|
+
if (transientFailure) this.consecutiveFlushFailures = 0;
|
|
1213
|
+
else this.consecutiveFlushFailures++;
|
|
1214
|
+
if (transientFailure || this.consecutiveFlushFailures < 2) {
|
|
996
1215
|
const pending = this.pending as PendingUpdate | undefined;
|
|
997
1216
|
this.pending = {
|
|
998
1217
|
observations: mergeMemoryLines(pending?.observations ?? [], update.observations),
|
|
@@ -1000,6 +1219,7 @@ export class Contemplator {
|
|
|
1000
1219
|
mainAgentOutputTokens: update.mainAgentOutputTokens,
|
|
1001
1220
|
mainAgentToolCalls: update.mainAgentToolCalls,
|
|
1002
1221
|
mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
|
|
1222
|
+
activeTimeFallback: update.activeTimeFallback,
|
|
1003
1223
|
};
|
|
1004
1224
|
} else {
|
|
1005
1225
|
debugLog("contemplator.poisoned_update_released", { reason: failureMessage, observationCount: update.observations.length, reviewCount: update.reviews.length });
|
|
@@ -1031,8 +1251,14 @@ export class Contemplator {
|
|
|
1031
1251
|
: this.turnsSinceRun < this.runtime.config.contemplatorMinTurns
|
|
1032
1252
|
? "responses"
|
|
1033
1253
|
: "ready";
|
|
1254
|
+
const activeTimeSinceRunMs = Math.max(
|
|
1255
|
+
0,
|
|
1256
|
+
agentActiveTimeMs(ctx.sessionManager.getBranch() as Entry[]) - this.lastContemplatorActiveTimeMs,
|
|
1257
|
+
);
|
|
1258
|
+
const finishedAt = Date.now();
|
|
1034
1259
|
this.publishState(waitingFor, {
|
|
1035
|
-
|
|
1260
|
+
activeTimeSinceRunMs,
|
|
1261
|
+
...(failed ? { lastFailedAt: finishedAt } : { lastCompletedAt: finishedAt, lastFailedAt: undefined }),
|
|
1036
1262
|
lastError: failureMessage,
|
|
1037
1263
|
});
|
|
1038
1264
|
debugLog("contemplator.complete", {
|
|
@@ -1378,13 +1604,24 @@ export class Contemplator {
|
|
|
1378
1604
|
// per checkpoint, and restore unions checkpoints, avoiding cumulative O(n²)
|
|
1379
1605
|
// id duplication across a very long session.
|
|
1380
1606
|
const prefixCoveredIds = new Set<string>();
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1607
|
+
const branchEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
1608
|
+
const branchById = new Map(branchEntries.map((entry) => [entry.id, entry]));
|
|
1609
|
+
for (let index = 0; index < prefixEnd; index++) {
|
|
1610
|
+
const message = history[index];
|
|
1611
|
+
if (message.role === "user") {
|
|
1612
|
+
const text = customMessageText(message.content);
|
|
1613
|
+
if (text.includes("NEW MEMORY UPDATE")) for (const id of memoryReferenceIds(text)) prefixCoveredIds.add(id);
|
|
1614
|
+
}
|
|
1615
|
+
// A safety-capped prompt intentionally covers records whose bodies were
|
|
1616
|
+
// omitted. Carry those compact id references into the replacement
|
|
1617
|
+
// checkpoint before its original message entry becomes unreachable.
|
|
1618
|
+
const entryId = historyEntryIds[index];
|
|
1619
|
+
const data = entryId === undefined ? undefined : branchById.get(entryId)?.data as { coveredObservationIds?: unknown; coveredReviewIds?: unknown } | undefined;
|
|
1620
|
+
for (const ids of [data?.coveredObservationIds, data?.coveredReviewIds]) {
|
|
1621
|
+
if (Array.isArray(ids)) for (const id of ids) if (typeof id === "string") prefixCoveredIds.add(id);
|
|
1622
|
+
}
|
|
1386
1623
|
}
|
|
1387
|
-
const currentProjection = fullProjection(
|
|
1624
|
+
const currentProjection = fullProjection(branchEntries);
|
|
1388
1625
|
const checkpoint = {
|
|
1389
1626
|
version: 2,
|
|
1390
1627
|
compacted: true,
|
|
@@ -60,7 +60,7 @@ function renderMessage(message: StoredMessage, compacted: boolean): string {
|
|
|
60
60
|
|
|
61
61
|
function liveStateLine(state: ContemplatorRunState): string {
|
|
62
62
|
const pending = `${state.pendingObservations} observations / ${state.pendingReviews} reviews pending`;
|
|
63
|
-
const timing = `Last start: ${state.lastStartedAt === undefined ? "not run this launch" : new Date(state.lastStartedAt).toISOString()} · Last
|
|
63
|
+
const timing = `Last start: ${state.lastStartedAt === undefined ? "not run this launch" : new Date(state.lastStartedAt).toISOString()} · Last success: ${state.lastCompletedAt === undefined ? "none this launch" : new Date(state.lastCompletedAt).toISOString()} · Last failure: ${state.lastFailedAt === undefined ? "none this launch" : new Date(state.lastFailedAt).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}`;
|
|
66
66
|
const reason = state.waitingFor === "observer"
|
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"}`,
|
|
@@ -147,7 +147,8 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
147
147
|
lines.push(`Last summarizer start: ${runtime.lastSummarizerStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.lastSummarizerStartedAt)}`);
|
|
148
148
|
lines.push(`Last summarizer end: ${runtime.lastSummarizerCompletedAt === undefined ? "not completed this launch" : formatRunAge(runtime.lastSummarizerCompletedAt)}`);
|
|
149
149
|
lines.push(`Last contemplator start: ${runtime.contemplatorState.lastStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.contemplatorState.lastStartedAt)}`);
|
|
150
|
-
lines.push(`Last contemplator
|
|
150
|
+
lines.push(`Last contemplator success: ${runtime.contemplatorState.lastCompletedAt === undefined ? "not completed this launch" : formatRunAge(runtime.contemplatorState.lastCompletedAt)}`);
|
|
151
|
+
lines.push(`Last contemplator failure: ${runtime.contemplatorState.lastFailedAt === undefined ? "none this launch" : formatRunAge(runtime.contemplatorState.lastFailedAt)}`);
|
|
151
152
|
|
|
152
153
|
lines.push("", "── Interventions ──");
|
|
153
154
|
// Probe stats come from the branch ledger (like /om:view contemplator):
|
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,9 +92,14 @@ 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;
|
|
99
|
+
/** Most recent successful final-action completion. */
|
|
97
100
|
lastCompletedAt?: number;
|
|
101
|
+
/** Most recent failed attempt; distinct so a timeout is never shown as success. */
|
|
102
|
+
lastFailedAt?: number;
|
|
98
103
|
lastError?: string;
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -148,7 +153,7 @@ export function computeSessionSettings(entries: readonly unknown[]): SessionSett
|
|
|
148
153
|
const numberKeys = [
|
|
149
154
|
"observeAfterTokens", "observerChunkMaxTokens", "compactAfterTokens",
|
|
150
155
|
"newMemoryPoolMaxTokens", "oldMemoryPoolTargetTokens", "agentMaxTurns",
|
|
151
|
-
"contemplatorMinNewObservations", "contemplatorMinTurns",
|
|
156
|
+
"contemplatorMinNewObservations", "contemplatorMinTurns", "contemplatorMaxActiveMinutes",
|
|
152
157
|
"summarizerRetriggerTokens", "summarizerSamplingThresholdTokens",
|
|
153
158
|
] as const;
|
|
154
159
|
for (const key of booleanKeys) if (typeof data[key] === "boolean") restored[key] = data[key];
|
|
@@ -225,6 +230,7 @@ export class Runtime {
|
|
|
225
230
|
pendingObservations: 0,
|
|
226
231
|
pendingReviews: 0,
|
|
227
232
|
responsesSinceRun: 0,
|
|
233
|
+
activeTimeSinceRunMs: 0,
|
|
228
234
|
waitingFor: "idle",
|
|
229
235
|
};
|
|
230
236
|
agentUsage: LlmUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, runs: 0 };
|
|
@@ -325,6 +331,7 @@ export class Runtime {
|
|
|
325
331
|
pendingObservations: 0,
|
|
326
332
|
pendingReviews: 0,
|
|
327
333
|
responsesSinceRun: 0,
|
|
334
|
+
activeTimeSinceRunMs: 0,
|
|
328
335
|
waitingFor: "idle",
|
|
329
336
|
};
|
|
330
337
|
}
|