@matthewfl/pi-contemplator 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agents/contemplator/agent.ts +38 -6
- package/src/agents/observer/agent.ts +18 -1
- package/src/commands/contemplator-view.ts +3 -1
- package/src/commands/observer-view.ts +59 -0
- package/src/commands/status.ts +3 -2
- package/src/commands/view.ts +15 -2
- package/src/config.ts +1 -1
- package/src/hooks/consolidation-trigger.ts +45 -3
- package/src/runtime.ts +17 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A Pi extension that keeps long-running agentic sessions on track with background memory, contemplation, and structural review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -229,9 +229,11 @@ export class Contemplator {
|
|
|
229
229
|
private deliveredProbeIds = new Set<string>();
|
|
230
230
|
/** Probe ids passed to pi.sendMessage by this live extension runtime. */
|
|
231
231
|
private queuedProbeIds = new Set<string>();
|
|
232
|
+
/** Probes whose provider-context delivery will establish the next response-spacing anchor. */
|
|
233
|
+
private probeCooldownPendingIds = new Set<string>();
|
|
232
234
|
private sessionGeneration = 0;
|
|
233
235
|
private latestCtx: MemoryUpdateCtx | undefined;
|
|
234
|
-
/** Completed primary-model responses since the
|
|
236
|
+
/** Completed primary-model responses since the current completion/probe-delivery spacing anchor. */
|
|
235
237
|
private turnsSinceRun = 0;
|
|
236
238
|
/** Used to avoid counting the final turn_end after its assistant message_end. */
|
|
237
239
|
private assistantResponsesInCurrentTurn = 0;
|
|
@@ -311,6 +313,7 @@ export class Contemplator {
|
|
|
311
313
|
this.reviewerSessions.clear();
|
|
312
314
|
this.deliveredProbeIds.clear();
|
|
313
315
|
this.queuedProbeIds.clear();
|
|
316
|
+
this.probeCooldownPendingIds.clear();
|
|
314
317
|
this.latestCtx = undefined;
|
|
315
318
|
this.turnsSinceRun = 0;
|
|
316
319
|
this.assistantResponsesInCurrentTurn = 0;
|
|
@@ -359,9 +362,12 @@ export class Contemplator {
|
|
|
359
362
|
});
|
|
360
363
|
this.pi.on("context", (event: any, ctx: ExtensionContext) => {
|
|
361
364
|
const deliveredMessages = event.messages?.filter((message: any) => message?.role === "custom" && message.customType === CONTEMPLATOR_SUGGESTION && typeof message.details?.probeId === "string") ?? [];
|
|
365
|
+
let cooldownAnchored = false;
|
|
362
366
|
for (const delivered of deliveredMessages) {
|
|
363
367
|
if (this.deliveredProbeIds.has(delivered.details.probeId)) continue;
|
|
364
368
|
this.deliveredProbeIds.add(delivered.details.probeId);
|
|
369
|
+
this.probeCooldownPendingIds.delete(delivered.details.probeId);
|
|
370
|
+
cooldownAnchored = true;
|
|
365
371
|
// Once Pi includes the probe in a provider context it is no longer in
|
|
366
372
|
// either in-memory delivery queue. Keeping this id indefinitely caused
|
|
367
373
|
// later tree restores to suppress a genuinely needed requeue.
|
|
@@ -375,6 +381,12 @@ export class Contemplator {
|
|
|
375
381
|
this.markTipPersisted(ctx);
|
|
376
382
|
debugLog("contemplator.suggestion_delivered", { probeId: delivered.details.probeId });
|
|
377
383
|
}
|
|
384
|
+
if (cooldownAnchored) {
|
|
385
|
+
// Probe spacing begins only once Pi proves the probe reached an actual
|
|
386
|
+
// provider context. Responses generated before this point do not count.
|
|
387
|
+
this.turnsSinceRun = 0;
|
|
388
|
+
this.withDebugContext(ctx, () => this.observeTurn(ctx));
|
|
389
|
+
}
|
|
378
390
|
});
|
|
379
391
|
this.pi.on("turn_end", (_event: any, ctx: ExtensionContext) => {
|
|
380
392
|
this.persistAgentActivity(ctx);
|
|
@@ -437,6 +449,7 @@ export class Contemplator {
|
|
|
437
449
|
let resetProjection: ReturnType<typeof fullProjection> | undefined;
|
|
438
450
|
if (resetTracking) {
|
|
439
451
|
this.deliveredProbeIds.clear();
|
|
452
|
+
this.probeCooldownPendingIds.clear();
|
|
440
453
|
if (!retainQueuedIds) this.queuedProbeIds.clear();
|
|
441
454
|
this.inFlightReviewIds.clear();
|
|
442
455
|
this.resolvingReviewIds.clear();
|
|
@@ -545,6 +558,9 @@ export class Contemplator {
|
|
|
545
558
|
}
|
|
546
559
|
this.restoredTipId = tipId;
|
|
547
560
|
for (const [probeId, question] of undeliveredSuggestions) {
|
|
561
|
+
// An undelivered durable probe remains the cooldown anchor even when Pi's
|
|
562
|
+
// live queue survived an extension reload and must not be duplicated.
|
|
563
|
+
this.probeCooldownPendingIds.add(probeId);
|
|
548
564
|
// A durable custom_message proves only that Pi inserted the probe at some
|
|
549
565
|
// point; it does not prove an in-memory queue still owns it, and compaction
|
|
550
566
|
// may have removed it from active model context. Suppress requeue only for
|
|
@@ -620,7 +636,7 @@ export class Contemplator {
|
|
|
620
636
|
};
|
|
621
637
|
}
|
|
622
638
|
if (!this.pending) {
|
|
623
|
-
this.publishState(this.running ? "running" : "idle");
|
|
639
|
+
this.publishState(this.running ? "running" : this.probeCooldownPendingIds.size > 0 ? "probe" : "idle");
|
|
624
640
|
return;
|
|
625
641
|
}
|
|
626
642
|
// Activity values are cumulative send-time snapshots, not values frozen when
|
|
@@ -630,6 +646,11 @@ export class Contemplator {
|
|
|
630
646
|
this.pending.mainAgentToolCalls = assistantToolCallCount(branchEntries);
|
|
631
647
|
this.pending.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntries);
|
|
632
648
|
const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations || this.pending.summaries.length >= this.runtime.config.contemplatorMinNewSummaries;
|
|
649
|
+
if (this.probeCooldownPendingIds.size > 0) {
|
|
650
|
+
this.publishState("probe");
|
|
651
|
+
debugLog("contemplator.waiting", { reason: "probe_delivery", pendingProbeCount: this.probeCooldownPendingIds.size });
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
633
654
|
if (!enoughMemories || this.turnsSinceRun < this.runtime.config.contemplatorMinTurns) {
|
|
634
655
|
this.publishState(!enoughMemories ? "memories" : "responses");
|
|
635
656
|
debugLog("contemplator.waiting", {
|
|
@@ -676,6 +697,7 @@ export class Contemplator {
|
|
|
676
697
|
let failureMessage: string | undefined;
|
|
677
698
|
let workerNotified = false;
|
|
678
699
|
let promptPersisted = false;
|
|
700
|
+
let emittedProbeId: string | undefined;
|
|
679
701
|
let workerWatchdog: ReturnType<typeof createWorkerStallWatchdog> | undefined;
|
|
680
702
|
this.publishState("running", { lastStartedAt: startedAt, lastError: undefined });
|
|
681
703
|
debugLog("contemplator.start", {
|
|
@@ -863,7 +885,7 @@ export class Contemplator {
|
|
|
863
885
|
this.markTipPersisted(ctx);
|
|
864
886
|
}
|
|
865
887
|
}
|
|
866
|
-
if (intervention?.kind === "probe" && sessionGeneration === this.sessionGeneration) this.queueProbe(ctx, intervention.question, "send_probe");
|
|
888
|
+
if (intervention?.kind === "probe" && sessionGeneration === this.sessionGeneration) emittedProbeId = this.queueProbe(ctx, intervention.question, "send_probe");
|
|
867
889
|
if (intervention?.kind === "review" && this.runtime.config.reviewerEnabled && sessionGeneration === this.sessionGeneration) {
|
|
868
890
|
const reviewerModel = await this.runtime.resolveModel({
|
|
869
891
|
model: ctx.model,
|
|
@@ -917,13 +939,21 @@ export class Contemplator {
|
|
|
917
939
|
if (flushEpoch !== this.flushEpoch) return;
|
|
918
940
|
this.running = false;
|
|
919
941
|
if (!failed) this.consecutiveFlushFailures = 0;
|
|
942
|
+
// Normal runs establish their spacing anchor at completion. A probe run
|
|
943
|
+
// instead anchors at provider-context delivery: if delivery already occurred
|
|
944
|
+
// during this run, retain responses counted since it; otherwise the pending
|
|
945
|
+
// probe gate blocks launches until the context event resets the counter.
|
|
946
|
+
if (emittedProbeId === undefined) this.turnsSinceRun = 0;
|
|
947
|
+
const waitingForProbe = this.probeCooldownPendingIds.size > 0;
|
|
920
948
|
const pendingHasEnoughMemories = this.pending !== undefined && (
|
|
921
949
|
this.pending.reviews.length > 0 ||
|
|
922
950
|
this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations ||
|
|
923
951
|
this.pending.summaries.length >= this.runtime.config.contemplatorMinNewSummaries
|
|
924
952
|
);
|
|
925
|
-
const waitingFor =
|
|
926
|
-
? "
|
|
953
|
+
const waitingFor = waitingForProbe
|
|
954
|
+
? "probe"
|
|
955
|
+
: !this.pending
|
|
956
|
+
? "idle"
|
|
927
957
|
: !pendingHasEnoughMemories
|
|
928
958
|
? "memories"
|
|
929
959
|
: this.turnsSinceRun < this.runtime.config.contemplatorMinTurns
|
|
@@ -948,7 +978,7 @@ export class Contemplator {
|
|
|
948
978
|
}
|
|
949
979
|
}
|
|
950
980
|
|
|
951
|
-
private queueProbe(ctx: MemoryUpdateCtx, question: string, source: "send_probe" | "restore", existingProbeId?: string):
|
|
981
|
+
private queueProbe(ctx: MemoryUpdateCtx, question: string, source: "send_probe" | "restore", existingProbeId?: string): string {
|
|
952
982
|
const probeId = existingProbeId ?? `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
953
983
|
// Persist intent before touching Pi's in-memory queue. A crash in between
|
|
954
984
|
// leaves a recoverable pending probe rather than an invisible lost one.
|
|
@@ -967,6 +997,7 @@ export class Contemplator {
|
|
|
967
997
|
// unrelated observer update or compaction callback cannot restore and enqueue
|
|
968
998
|
// a duplicate while the original idle steer is still pending.
|
|
969
999
|
this.queuedProbeIds.add(probeId);
|
|
1000
|
+
this.probeCooldownPendingIds.add(probeId);
|
|
970
1001
|
this.pi.sendMessage({
|
|
971
1002
|
customType: CONTEMPLATOR_SUGGESTION,
|
|
972
1003
|
content: `Background contemplator probe (advisory):\n${question}\n\nReferenced memories can be reviewed using the recall tool.`,
|
|
@@ -981,6 +1012,7 @@ export class Contemplator {
|
|
|
981
1012
|
triggerTurn: "omitted",
|
|
982
1013
|
source,
|
|
983
1014
|
});
|
|
1015
|
+
return probeId;
|
|
984
1016
|
}
|
|
985
1017
|
|
|
986
1018
|
private queueStructuralReview(options: QueueStructuralReviewOptions): void {
|
|
@@ -26,6 +26,7 @@ interface RunObserverArgs {
|
|
|
26
26
|
thinkingLevel?: ModelThinkingLevel;
|
|
27
27
|
recordUsage?: (usage: LlmUsageInput) => void;
|
|
28
28
|
onProgress?: () => void;
|
|
29
|
+
onMessages?: (messages: readonly AgentMessage[]) => void;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
const RelevanceSchema = Type.Union([
|
|
@@ -223,6 +224,10 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
223
224
|
messages: history.slice(),
|
|
224
225
|
tools: [recordObservations as AgentTool<any>, doneTool],
|
|
225
226
|
};
|
|
227
|
+
// Publish a live launch-local transcript for /om:view observer. Keep an
|
|
228
|
+
// invocation-local list because agentLoop owns its internal context copy.
|
|
229
|
+
let liveMessages: AgentMessage[] = [...history, prompt];
|
|
230
|
+
args.onMessages?.(liveMessages.slice());
|
|
226
231
|
const invocationConfig: AgentLoopConfig = afterLength && reasoning
|
|
227
232
|
? { ...baseConfig, reasoning: "minimal" }
|
|
228
233
|
: baseConfig;
|
|
@@ -230,7 +235,17 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
230
235
|
for await (const event of stream) {
|
|
231
236
|
args.onProgress?.();
|
|
232
237
|
logAgentStreamError("observer", event);
|
|
233
|
-
const
|
|
238
|
+
const typedEvent = event as { type?: string; message?: AgentMessage & { role?: string; stopReason?: string; errorMessage?: string } };
|
|
239
|
+
const message = typedEvent.message;
|
|
240
|
+
if (message && message !== prompt && message.role !== "user") {
|
|
241
|
+
if (typedEvent.type === "message_start") liveMessages.push(message);
|
|
242
|
+
else if (typedEvent.type === "message_update" || typedEvent.type === "message_end") {
|
|
243
|
+
const index = liveMessages.map((item) => item.role).lastIndexOf(message.role);
|
|
244
|
+
if (index >= 0) liveMessages[index] = message;
|
|
245
|
+
else liveMessages.push(message);
|
|
246
|
+
}
|
|
247
|
+
args.onMessages?.(liveMessages.slice());
|
|
248
|
+
}
|
|
234
249
|
if (message?.role === "assistant" && ["error", "aborted", "length"].includes(message.stopReason ?? "")) {
|
|
235
250
|
terminalFailure = { stopReason: message.stopReason!, errorMessage: message.errorMessage };
|
|
236
251
|
}
|
|
@@ -238,6 +253,8 @@ IMPORTANT: Now call record_observations to record the useful new observations fr
|
|
|
238
253
|
const result = await stream.result();
|
|
239
254
|
if (!Array.isArray(result)) return;
|
|
240
255
|
history.push(...result);
|
|
256
|
+
liveMessages = history.slice();
|
|
257
|
+
args.onMessages?.(liveMessages);
|
|
241
258
|
for (const message of result) {
|
|
242
259
|
if (message.role === "assistant" && ["error", "aborted", "length"].includes(message.stopReason ?? "")) {
|
|
243
260
|
terminalFailure = { stopReason: message.stopReason, errorMessage: message.errorMessage };
|
|
@@ -65,6 +65,8 @@ function liveStateLine(state: ContemplatorRunState): string {
|
|
|
65
65
|
if (state.running) return `LIVE · running for ${Math.max(0, Math.floor((Date.now() - (state.lastStartedAt ?? Date.now())) / 60_000))}m · ${pending}\n${timing}${error}`;
|
|
66
66
|
const reason = state.waitingFor === "observer"
|
|
67
67
|
? "waiting for observer backlog"
|
|
68
|
+
: state.waitingFor === "probe"
|
|
69
|
+
? "waiting for queued probe delivery"
|
|
68
70
|
: state.waitingFor === "memories"
|
|
69
71
|
? "waiting for memory threshold"
|
|
70
72
|
: state.waitingFor === "responses"
|
|
@@ -76,7 +78,7 @@ function liveStateLine(state: ContemplatorRunState): string {
|
|
|
76
78
|
: state.waitingFor === "passive"
|
|
77
79
|
? "passive mode"
|
|
78
80
|
: "idle";
|
|
79
|
-
return `LIVE · ${reason} · ${pending} · ${state.responsesSinceRun} primary responses since
|
|
81
|
+
return `LIVE · ${reason} · ${pending} · ${state.responsesSinceRun} primary responses since cooldown anchor\n${timing}${error}`;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export function renderContemplator(entries: Entry[], state?: ContemplatorRunState): string {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ObserverRunView } from "../runtime.js";
|
|
2
|
+
|
|
3
|
+
const DIM = "\x1b[2m";
|
|
4
|
+
const RESET = "\x1b[0m";
|
|
5
|
+
|
|
6
|
+
type StoredMessage = { role?: unknown; content?: unknown };
|
|
7
|
+
type ContentPart = { type?: unknown; text?: unknown; thinking?: unknown; name?: unknown; arguments?: unknown; content?: unknown };
|
|
8
|
+
|
|
9
|
+
function renderValue(value: unknown): string {
|
|
10
|
+
if (typeof value === "string") return value;
|
|
11
|
+
if (value === undefined || value === null) return "";
|
|
12
|
+
return JSON.stringify(value, null, 2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderContent(content: unknown): string {
|
|
16
|
+
if (typeof content === "string") return content;
|
|
17
|
+
if (!Array.isArray(content)) return renderValue(content);
|
|
18
|
+
return content.map((part: ContentPart) => {
|
|
19
|
+
if (part.type === "text") return typeof part.text === "string" ? part.text : "";
|
|
20
|
+
if (part.type === "thinking") {
|
|
21
|
+
const thinking = typeof part.thinking === "string" ? part.thinking : typeof part.text === "string" ? part.text : renderValue(part);
|
|
22
|
+
return `[thinking]\n${thinking}`;
|
|
23
|
+
}
|
|
24
|
+
if (part.type === "toolCall" || part.type === "tool_use" || part.type === "toolUse") {
|
|
25
|
+
const name = typeof part.name === "string" ? part.name : "unknown tool";
|
|
26
|
+
return `[tool call: ${name}${part.arguments === undefined ? "" : ` ${renderValue(part.arguments)}`}]`;
|
|
27
|
+
}
|
|
28
|
+
if (part.type === "toolResult" || part.type === "tool_result") return `[tool result]\n${renderValue(part.content)}`;
|
|
29
|
+
return `[${String(part.type ?? "content")}] ${renderValue(part)}`;
|
|
30
|
+
}).filter(Boolean).join("\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function estimateTokens(value: unknown): number {
|
|
34
|
+
return Math.max(1, Math.ceil(JSON.stringify(value).length / 4));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Render the currently active observer chunk, or the most recently completed chunk. */
|
|
38
|
+
export function renderObserver(run: ObserverRunView | undefined, now = Date.now()): string {
|
|
39
|
+
if (!run) return `${DIM}OBSERVER${RESET}\n\n${DIM}Observer has not run yet during this launch.${RESET}`;
|
|
40
|
+
const messages = run.messages.filter((message): message is StoredMessage => !!message && typeof message === "object");
|
|
41
|
+
const totalTokens = messages.reduce((sum, message) => sum + estimateTokens(message), 0);
|
|
42
|
+
const elapsedMs = Math.max(0, (run.completedAt ?? now) - run.startedAt);
|
|
43
|
+
const lines = [
|
|
44
|
+
`${DIM}OBSERVER · ${run.status} · ${messages.length} messages · ~${totalTokens.toLocaleString()} estimated transcript tokens${RESET}`,
|
|
45
|
+
`${DIM}Chunk ~${run.chunkTokens.toLocaleString()} tokens · backlog at start ~${run.backlogTokens.toLocaleString()} tokens · ${run.sourceEntryIds.length} source entr${run.sourceEntryIds.length === 1 ? "y" : "ies"}${RESET}`,
|
|
46
|
+
`${DIM}Started ${new Date(run.startedAt).toISOString()} · ${run.completedAt === undefined ? `running for ${Math.floor(elapsedMs / 1000)}s` : `ended ${new Date(run.completedAt).toISOString()} after ${Math.floor(elapsedMs / 1000)}s`}${RESET}`,
|
|
47
|
+
"",
|
|
48
|
+
];
|
|
49
|
+
if (messages.length === 0) lines.push(`${DIM}(no observer messages captured yet)${RESET}`);
|
|
50
|
+
for (const [index, message] of messages.entries()) {
|
|
51
|
+
if (index > 0) lines.push("");
|
|
52
|
+
const role = typeof message.role === "string" ? message.role : "unknown";
|
|
53
|
+
lines.push(`${DIM}── ${role} · ~${estimateTokens(message).toLocaleString()} tokens ──${RESET}`);
|
|
54
|
+
lines.push(renderContent(message.content) || `${DIM}(empty message)${RESET}`);
|
|
55
|
+
}
|
|
56
|
+
if (run.summary) lines.push("", `${DIM}── Completion summary ──${RESET}`, run.summary);
|
|
57
|
+
if (run.error) lines.push("", `${DIM}── Failure ──${RESET}`, run.error);
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -43,6 +43,7 @@ function formatRunAge(timestamp: number): string {
|
|
|
43
43
|
function contemplatorWaitingLabel(waitingFor: Runtime["contemplatorState"]["waitingFor"]): string {
|
|
44
44
|
switch (waitingFor) {
|
|
45
45
|
case "observer": return "waiting for observer backlog";
|
|
46
|
+
case "probe": return "waiting for queued probe delivery";
|
|
46
47
|
case "memories": return "waiting for memory threshold";
|
|
47
48
|
case "responses": return "waiting for response spacing";
|
|
48
49
|
case "ready": return "ready to launch";
|
|
@@ -142,8 +143,8 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
lines.push("", "── Last worker runs ──");
|
|
145
|
-
lines.push(`
|
|
146
|
-
lines.push(`
|
|
146
|
+
lines.push(`Observer chunk start: ${runtime.lastObserverStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.lastObserverStartedAt)}`);
|
|
147
|
+
lines.push(`Observer chunk end: ${runtime.lastObserverCompletedAt === undefined ? runtime.lastObserverRun?.status === "running" ? "running" : "not completed this launch" : formatRunAge(runtime.lastObserverCompletedAt)}`);
|
|
147
148
|
lines.push(`Last summarizer start: ${runtime.lastSummarizerStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.lastSummarizerStartedAt)}`);
|
|
148
149
|
lines.push(`Last summarizer end: ${runtime.lastSummarizerCompletedAt === undefined ? "not completed this launch" : formatRunAge(runtime.lastSummarizerCompletedAt)}`);
|
|
149
150
|
lines.push(`Last contemplator start: ${runtime.contemplatorState.lastStartedAt === undefined ? "not run this launch" : formatRunAge(runtime.contemplatorState.lastStartedAt)}`);
|
package/src/commands/view.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { copyTextToClipboard } from "../clipboard.js";
|
|
|
4
4
|
import { renderContemplator, stripAnsi } from "./contemplator-view.js";
|
|
5
5
|
import { renderReviewer } from "./reviewer-view.js";
|
|
6
6
|
import { renderSummarizer } from "./summarizer-view.js";
|
|
7
|
+
import { renderObserver } from "./observer-view.js";
|
|
7
8
|
import { executeRecall, formatRecallResultForTui } from "../tools/recall-observation.js";
|
|
8
9
|
import {
|
|
9
10
|
chronologicalMemories,
|
|
@@ -76,7 +77,7 @@ export function registerViewCommand(
|
|
|
76
77
|
|
|
77
78
|
pi.registerCommand("om:view", {
|
|
78
79
|
description:
|
|
79
|
-
"Print and copy pi-contemplator memory content (visible, full, memory, contemplator, summarizer, reviewer, or reviews)",
|
|
80
|
+
"Print and copy pi-contemplator memory content (visible, full, memory, contemplator, observer, summarizer, reviewer, or reviews)",
|
|
80
81
|
handler: async (args, ctx) => {
|
|
81
82
|
runtime.ensureConfig(ctx.cwd);
|
|
82
83
|
const entries = ctx.sessionManager.getBranch() as Entry[];
|
|
@@ -116,6 +117,18 @@ export function registerViewCommand(
|
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
if (mode === "observer") {
|
|
121
|
+
const output = renderObserver(runtime.lastObserverRun);
|
|
122
|
+
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
123
|
+
ctx.ui.notify(
|
|
124
|
+
`${output}
|
|
125
|
+
|
|
126
|
+
${copied ? "Copied /om:view observer output to clipboard." : "Warning: failed to copy /om:view observer output to clipboard."}`,
|
|
127
|
+
"info",
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
119
132
|
if (mode === "summarizer") {
|
|
120
133
|
const output = renderSummarizer(runtime.lastSummarizerRun);
|
|
121
134
|
const copied = await copyToClipboard(stripAnsi(output)).catch(() => false);
|
|
@@ -154,7 +167,7 @@ export function registerViewCommand(
|
|
|
154
167
|
}
|
|
155
168
|
|
|
156
169
|
if (mode && mode !== "visible") {
|
|
157
|
-
ctx.ui.notify("Usage: /om:view [visible|full|memory <id>|contemplator|summarizer|reviewer|reviews]", "info");
|
|
170
|
+
ctx.ui.notify("Usage: /om:view [visible|full|memory <id>|contemplator|observer|summarizer|reviewer|reviews]", "info");
|
|
158
171
|
return;
|
|
159
172
|
}
|
|
160
173
|
|
package/src/config.ts
CHANGED
|
@@ -61,7 +61,7 @@ export interface Config {
|
|
|
61
61
|
reviewerModel?: ConfiguredModel;
|
|
62
62
|
contemplatorMinNewObservations: number;
|
|
63
63
|
contemplatorMinNewSummaries: number;
|
|
64
|
-
/** Minimum
|
|
64
|
+
/** Minimum primary-model responses after contemplator completion, or after delivery of its probe, before the next run. */
|
|
65
65
|
contemplatorMinTurns: number;
|
|
66
66
|
/** Stateless loss-aware summarizer for the old memory pool. */
|
|
67
67
|
summarizerEnabled: boolean;
|
|
@@ -227,7 +227,6 @@ export async function runConsolidationPipeline(
|
|
|
227
227
|
|
|
228
228
|
const beforeFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
229
229
|
runtime.consolidationPhase = "observer";
|
|
230
|
-
runtime.lastObserverStartedAt = Date.now();
|
|
231
230
|
try {
|
|
232
231
|
// A large backlog is drained in bounded, oldest-first chunks. The normal
|
|
233
232
|
// trigger threshold controls when the batch stops; a static compaction
|
|
@@ -253,8 +252,6 @@ export async function runConsolidationPipeline(
|
|
|
253
252
|
} catch (error) {
|
|
254
253
|
debugLog("observer.error", { errorMessage: runtime.recordConsolidationStageError(ctx, "observer", error) });
|
|
255
254
|
return;
|
|
256
|
-
} finally {
|
|
257
|
-
runtime.lastObserverCompletedAt = Date.now();
|
|
258
255
|
}
|
|
259
256
|
const afterFold = foldLedger(ctx.sessionManager.getBranch() as Entry[]);
|
|
260
257
|
const beforeIds = new Set(beforeFold.observations.map((item) => item.id));
|
|
@@ -477,6 +474,20 @@ async function runObserverStage(
|
|
|
477
474
|
priorObservations: priorObservations.length,
|
|
478
475
|
});
|
|
479
476
|
|
|
477
|
+
const observerStartedAt = Date.now();
|
|
478
|
+
runtime.lastObserverStartedAt = observerStartedAt;
|
|
479
|
+
// Clear the previous end when a new chunk starts so /om:status never pairs
|
|
480
|
+
// this chunk's start with the preceding chunk's completion.
|
|
481
|
+
runtime.lastObserverCompletedAt = undefined;
|
|
482
|
+
runtime.lastObserverRun = {
|
|
483
|
+
startedAt: observerStartedAt,
|
|
484
|
+
status: "running",
|
|
485
|
+
messages: [],
|
|
486
|
+
chunkTokens,
|
|
487
|
+
backlogTokens: tokens,
|
|
488
|
+
sourceEntryIds: sourceEntryIds.slice(),
|
|
489
|
+
};
|
|
490
|
+
let acceptsObserverMessages = true;
|
|
480
491
|
let observations;
|
|
481
492
|
let failedMessage: string | undefined;
|
|
482
493
|
const observerWatchdog = createWorkerStallWatchdog("observer");
|
|
@@ -493,6 +504,17 @@ async function runObserverStage(
|
|
|
493
504
|
thinkingLevel: runtime.config.model?.thinking ?? "low",
|
|
494
505
|
recordUsage: (usage) => runtime.recordAgentUsage(usage),
|
|
495
506
|
onProgress: observerWatchdog.progress,
|
|
507
|
+
onMessages: (messages) => {
|
|
508
|
+
if (!acceptsObserverMessages || options.contextGeneration !== undefined && options.contextGeneration !== runtime.getContextGeneration()) return;
|
|
509
|
+
runtime.lastObserverRun = {
|
|
510
|
+
startedAt: observerStartedAt,
|
|
511
|
+
status: "running",
|
|
512
|
+
messages: messages.slice(),
|
|
513
|
+
chunkTokens,
|
|
514
|
+
backlogTokens: tokens,
|
|
515
|
+
sourceEntryIds: sourceEntryIds.slice(),
|
|
516
|
+
};
|
|
517
|
+
},
|
|
496
518
|
signal: observerWatchdog.signal,
|
|
497
519
|
}));
|
|
498
520
|
} catch (error) {
|
|
@@ -509,8 +531,19 @@ async function runObserverStage(
|
|
|
509
531
|
chunkTokens,
|
|
510
532
|
});
|
|
511
533
|
observations = undefined;
|
|
534
|
+
if (runtime.lastObserverRun?.startedAt === observerStartedAt) {
|
|
535
|
+
runtime.lastObserverRun = { ...runtime.lastObserverRun, status: "failed", error: failedMessage };
|
|
536
|
+
}
|
|
512
537
|
} finally {
|
|
538
|
+
acceptsObserverMessages = false;
|
|
513
539
|
observerWatchdog.dispose();
|
|
540
|
+
if (options.contextGeneration === undefined || options.contextGeneration === runtime.getContextGeneration()) {
|
|
541
|
+
const completedAt = Date.now();
|
|
542
|
+
runtime.lastObserverCompletedAt = completedAt;
|
|
543
|
+
if (runtime.lastObserverRun?.startedAt === observerStartedAt) {
|
|
544
|
+
runtime.lastObserverRun = { ...runtime.lastObserverRun, completedAt };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
514
547
|
}
|
|
515
548
|
if (options.contextGeneration !== undefined && options.contextGeneration !== runtime.getContextGeneration()) {
|
|
516
549
|
debugLog("observer.stale", { reason: "session_or_branch_changed" });
|
|
@@ -531,6 +564,15 @@ async function runObserverStage(
|
|
|
531
564
|
});
|
|
532
565
|
}
|
|
533
566
|
const accepted = observations ?? [];
|
|
567
|
+
if (!failedMessage && runtime.lastObserverRun?.startedAt === observerStartedAt) {
|
|
568
|
+
runtime.lastObserverRun = {
|
|
569
|
+
...runtime.lastObserverRun,
|
|
570
|
+
status: "completed",
|
|
571
|
+
summary: accepted.length > 0
|
|
572
|
+
? `${accepted.length} observation${accepted.length === 1 ? "" : "s"} recorded; chunk covered through ${effectiveCoversUpToId}.`
|
|
573
|
+
: `No observations recorded; chunk covered through ${effectiveCoversUpToId}.`,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
534
576
|
const data = buildObservationsRecordedData(accepted, effectiveCoversUpToId);
|
|
535
577
|
if (!data) return "continue";
|
|
536
578
|
debugLog(failedMessage ? "observer.failed_coverage" : accepted.length > 0 ? "observer.records" : "observer.coverage_only", {
|
package/src/runtime.ts
CHANGED
|
@@ -56,6 +56,18 @@ export interface MemoryUpdateCtx extends LaunchCtx {
|
|
|
56
56
|
|
|
57
57
|
export type SettingsUpdate = Partial<SessionSettings>;
|
|
58
58
|
|
|
59
|
+
export interface ObserverRunView {
|
|
60
|
+
startedAt: number;
|
|
61
|
+
completedAt?: number;
|
|
62
|
+
status: "running" | "completed" | "failed";
|
|
63
|
+
messages: readonly unknown[];
|
|
64
|
+
chunkTokens: number;
|
|
65
|
+
backlogTokens: number;
|
|
66
|
+
sourceEntryIds: readonly string[];
|
|
67
|
+
summary?: string;
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
export interface SummarizerRunView {
|
|
60
72
|
startedAt: number;
|
|
61
73
|
completedAt?: number;
|
|
@@ -70,9 +82,9 @@ export interface ContemplatorRunState {
|
|
|
70
82
|
pendingObservations: number;
|
|
71
83
|
pendingSummaries: number;
|
|
72
84
|
pendingReviews: number;
|
|
73
|
-
/** Completed primary-model responses since the
|
|
85
|
+
/** Completed primary-model responses since the current completion/probe-delivery spacing anchor. */
|
|
74
86
|
responsesSinceRun: number;
|
|
75
|
-
waitingFor: "disabled" | "passive" | "observer" | "memories" | "responses" | "ready" | "running" | "idle";
|
|
87
|
+
waitingFor: "disabled" | "passive" | "observer" | "probe" | "memories" | "responses" | "ready" | "running" | "idle";
|
|
76
88
|
lastStartedAt?: number;
|
|
77
89
|
lastCompletedAt?: number;
|
|
78
90
|
lastError?: string;
|
|
@@ -187,6 +199,8 @@ export class Runtime {
|
|
|
187
199
|
lastObserverCompletedAt: number | undefined;
|
|
188
200
|
lastSummarizerStartedAt: number | undefined;
|
|
189
201
|
lastSummarizerCompletedAt: number | undefined;
|
|
202
|
+
/** Current or most recent observer chunk transcript in this launch/session context. */
|
|
203
|
+
lastObserverRun: ObserverRunView | undefined;
|
|
190
204
|
/** Most recent summarizer transcript in this extension launch/session context. */
|
|
191
205
|
lastSummarizerRun: SummarizerRunView | undefined;
|
|
192
206
|
/** Launch-local liveness and trigger diagnostics published by the contemplator. */
|
|
@@ -277,6 +291,7 @@ export class Runtime {
|
|
|
277
291
|
this.lastObserverCompletedAt = undefined;
|
|
278
292
|
this.lastSummarizerStartedAt = undefined;
|
|
279
293
|
this.lastSummarizerCompletedAt = undefined;
|
|
294
|
+
this.lastObserverRun = undefined;
|
|
280
295
|
this.lastSummarizerRun = undefined;
|
|
281
296
|
this.contemplatorState = {
|
|
282
297
|
running: false,
|