@matthewfl/pi-contemplator 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # pi-contemplator
2
2
 
3
- `pi-contemplator` is a [Pi](https://pi.dev/) plugin designed to help long-running, unsupervised agentic sessions stay on track.
3
+ `pi-contemplator` is a [Pi](https://pi.dev/) plugin for long-running, largely unsupervised agentic sessions. It does two things:
4
4
 
5
- It adds a background **contemplator** agent that examines accumulated session memories for logical fallacies, unsupported assumptions, contradictions, and incorrect conclusions. When it finds a meaningful reasoning problem, it can interject with a focused question that helps the primary agent reconsider its reasoning before a mistaken premise poisons the rest of the session.
5
+ 1. **Keeps memory alive across compaction.** A background **observer**, **reflector**, and **dropper** distill the conversation into a durable, branch-local memory ledger. When Pi compacts the context window, the summary is rendered deterministically from that ledger fast, model-free, and lossless enough that important facts survive.
6
+ 2. **Gives the primary agent a second set of eyes.** A background **contemplator** reads the accumulated memories and watches for reasoning that is going wrong. When it finds a genuine problem, it can inject a focused, memory-cited question. For deeper, recurring structural issues, it can commission a short-lived **reviewer** that produces a durable, advisory design proposal.
7
+
8
+ The result: a long session is less likely to drift off course, get stuck in an unproductive loop, or silently compound a wrong conclusion — and when it does, it can often catch itself before the mistake poisons the rest of the work.
6
9
 
7
10
  > [!WARNING]
8
11
  > **Alpha-quality software:** `pi-contemplator` is under active development. Expect bugs, behavioral changes, and breaking configuration or memory-format changes.
@@ -15,7 +18,11 @@ It adds a background **contemplator** agent that examines accumulated session me
15
18
 
16
19
  ## Why use it?
17
20
 
18
- A long-running agent can draw the wrong conclusion from valid evidence and then repeatedly reinforce that conclusion. As the mistake accumulates in context, the agent becomes less likely to question it and less able to discover the real problem.
21
+ A long-running agent faces three compounding risks:
22
+
23
+ - **Compaction amnesia.** Pi compacts older messages to fit the context window. Details that weren't captured deliberately are simply gone.
24
+ - **Reasoning entrenchment.** An agent can draw the wrong conclusion from valid evidence and then repeatedly reinforce it. As the mistake accumulates in context, the agent becomes *less* likely to question it and *less* able to discover the real problem.
25
+ - **Unproductive loops.** The agent keeps bumping into the same obstacle through slightly different approaches, burning tokens without gaining information.
19
26
 
20
27
  Without a contemplator, a session can look like this:
21
28
 
@@ -66,17 +73,86 @@ A or C is true
66
73
 
67
74
  The contemplator does not take over the primary agent's work. It provides occasional, evidence-grounded challenges when reconsidering the current reasoning is more valuable than allowing the session to continue along the same path.
68
75
 
76
+ ## What it does for you
77
+
78
+ ### It catches reasoning problems before they settle
79
+
80
+ The contemplator is a "System 2" thinker: slower and more deliberative, focused on the larger shape of the problem while the primary agent does the hands-on work. It watches the memory ledger (not the live transcript) and looks for:
81
+
82
+ - unsupported assumptions — a conclusion that depends on something never established;
83
+ - contradictions — a recent claim that conflicts with earlier recorded evidence;
84
+ - drift from the user's intent, priorities, or constraints;
85
+ - overlooked alternatives — treating one explanation as settled when others are still plausible;
86
+ - unproductive loops — several memories showing the agent returning to the same obstacle;
87
+ - connections — relevant facts from separate memories that the primary agent never joined up.
88
+
89
+ When it finds something worth challenging, it sends a concise, memory-cited question as an asynchronous probe. Probes appear as purple cards in the chat and are delivered to the agent's next turn — they never interrupt mid-work or force a new turn:
90
+
91
+ ```text
92
+ ◆ CONTEMPLATOR PROBE
93
+
94
+ You concluded that fix Y resolved the failure, citing memory [a1b2c3d4e5f6].
95
+ Earlier, memory [d4e5f6a1b2c3] recorded that A, B, and C were also
96
+ possible explanations. What observation would distinguish Y from A, B, and C
97
+ before you build on that conclusion?
98
+ ```
99
+
100
+ ### It commissions deeper structural reviews
101
+
102
+ Some patterns are bigger than one question. When several memories point at a *recurring* structural problem, the contemplator can request a review instead of a probe. The reviewer is a separate, short-lived, strictly-scoped agent that independently investigates the concern and, if the evidence supports it, records a durable conceptual proposal.
103
+
104
+ Two scopes are handled separately:
105
+
106
+ - **Workflow** — problems in *how* the work is performed: repeatedly reconstructing the same information, reasoning at length about something that could just be observed or computed, failing to preserve a reusable result, weak feedback loops.
107
+ - **Software** — problems in *the software being produced*: repeated special cases, several fixes touching the same missing invariant, duplicated concepts, unclear responsibility boundaries, local workarounds that hint at a missing abstraction.
108
+
109
+ The reviewer isn't just fed the concern and asked to design a fix. It independently searches the memory ledger *and* the primary agent's recorded chat history (including regex search over the transcript), looks for supporting and contrary evidence, and reaches exactly one terminal outcome: either a durable proposal with a memory id, or a conclusion that no proposal is currently justified. The primary agent receives only a compact notice pointing at the proposal:
110
+
111
+ ```text
112
+ ◆ CONTEMPLATOR REVIEW
113
+
114
+ BACKGROUND WORKFLOW REVIEW PROPOSAL [f7a8b9c0d1e2]
115
+
116
+ Several memories ([1111], [2222], [3333]) show the same multi-step lookup being
117
+ rebuilt from scratch each time. The proposal suggests a reusable, indexable
118
+ trace that would make the relationship reproducible in seconds instead of minutes.
119
+
120
+ Recall memory [f7a8b9c0d1e2] to read the full conceptual proposal when it is
121
+ relevant. This is advisory.
122
+ ```
123
+
124
+ Proposals are deliberately conceptual — the reviewer cannot write code, specify exact APIs, name files, or hand over an implementation plan. The primary agent remains the only agent grounded in the actual codebase and environment, and it decides whether, when, and how to use a proposal.
125
+
126
+ ### It keeps memory useful and accountable
127
+
128
+ The memory system is built to be trustworthy:
129
+
130
+ - **Every memory has provenance.** Observations cite the exact source entries that support them; reflections cite the observations they're based on. Everything gets a deterministic 12-character id computed in code, not guessed by a model.
131
+ - **Nothing important is silently lost.** The `recall` tool recovers the exact source evidence behind any memory id. The `search_memories` tool finds candidate memories by topic. Even observations the dropper later prunes remain recallable from ledger history.
132
+ - **Compaction is deterministic and model-free.** The summary the agent sees is folded from the ledger by code, not rewritten by a model. That makes compaction fast and cheap, and it means the same session state always produces the same summary.
133
+
134
+ ## Why you can trust it in the background
135
+
136
+ Running extra agents in the background can be worrying — what if they take over, spam the user, or go off the rails? `pi-contemplator` is explicitly designed against that:
137
+
138
+ - **Advisory only.** The contemplator and reviewer never modify the primary agent's work, never implement anything, and never inject instructions into the user's session. They produce questions and proposals; the primary agent decides what to do.
139
+ - **One intervention per update.** The contemplator can send at most one probe *or* one review request per turn — never a barrage. Most turns it sends nothing at all.
140
+ - **Grounded in evidence, not noise.** Loops must be supported by a clear pattern across *multiple* memories. The contemplator is explicitly forbidden from inferring a stall from silence, elapsed time, or a missing recent result. It may consult activity signals (cumulative tokens, tool-call count, active time) as *advisory* context, but never as proof.
141
+ - **Sees memory, not secrets.** The contemplator reads only the memory ledger, not the live transcript, so it can't react to or interfere with in-flight work.
142
+ - **Epistemic humility.** The contemplator is instructed not to behave as though it already knows the answer, not to praise or criticize, and not to manage the primary agent step by step. It asks questions that help *both* agents discover what's missing.
143
+ - **Bounded and de-duplicated.** Reviews are serialized (one at a time), coalesced (no duplicate review for the same evidence and concern), and capped by a lifetime output-token budget — a review that runs out of budget records an honest "no proposal" rather than a half-baked design.
144
+
69
145
  ## Background agents
70
146
 
71
- `pi-contemplator` uses five specialized background agents:
147
+ `pi-contemplator` uses five specialized background agents, all enabled by default:
72
148
 
73
- | Agent | Purpose | Compared with `pi-observational-memory` |
149
+ | Agent | What it does | When it runs |
74
150
  |---|---|---|
75
- | **Observer** | Extracts concrete, durable observations from the primary session. | Same as `pi-observational-memory` |
76
- | **Reflector** | Finds higher-level patterns and relationships across observations. | Same as `pi-observational-memory` |
77
- | **Dropper** | Removes observations that are obsolete, redundant, or safely represented elsewhere. | Same as `pi-observational-memory` |
78
- | **Contemplator** | Watches accumulated memories for reasoning gaps, contradictions, overlooked alternatives, and recurring structural concerns; it can send a focused probe to the primary agent. | Added by `pi-contemplator` |
79
- | **Reviewer** | Performs a deeper, scoped workflow or software-design investigation when the contemplator identifies a well-supported recurring structural issue. | Added by `pi-contemplator` |
151
+ | **Observer** | Extracts concrete, timestamped observations from the primary session, citing source entries. | In the background after turns, once enough new source text accumulates. |
152
+ | **Reflector** | Distills durable conclusions (user intent, decisions, constraints) from observations, citing supporting observations. | Periodically, after the observer is up to date. |
153
+ | **Dropper** | Prunes observations that are obsolete, redundant, or safely represented elsewhere, keeping the active memory pool bounded. | Only after a successful reflection, when the memory pool is over target. |
154
+ | **Contemplator** | Watches accumulated memories for reasoning gaps, contradictions, overlooked alternatives, and recurring structural concerns; can send a focused probe or request a review. | Asynchronously after enough new memories accumulate. |
155
+ | **Reviewer** | Performs a deep, scoped (workflow or software) structural investigation and records a durable proposal or a no-proposal conclusion. | Only when the contemplator commissions a review, and only one at a time. |
80
156
 
81
157
  The observer, reflector, and dropper provide the durable memory substrate. The contemplator reasons over that substrate, while the reviewer is launched only when a concern warrants a deeper structural investigation.
82
158
 
@@ -94,18 +170,20 @@ Or run it directly from a local checkout:
94
170
  pi -e ./src/index.ts
95
171
  ```
96
172
 
97
- ## Configuration and commands
173
+ ## Commands and configuration
98
174
 
99
- The plugin works with its defaults, including the contemplator and reviewer. Contemplator probes and review notices appear as purple cards in the chat by default; use `/om:settings messages off` to hide newly sent cards without stopping their delivery to the agent. Model selection, trigger thresholds, passive mode, compaction behavior, and other settings are documented in [docs/configuration.md](docs/configuration.md).
175
+ The plugin works with its defaults, including the contemplator and reviewer. Contemplator probes and review notices appear as purple cards in the chat by default; use `/om:settings messages off` to hide newly sent cards without stopping their delivery to the agent.
100
176
 
101
- Useful commands include:
177
+ Useful commands:
102
178
 
103
- - `/om:status` — show memory and background-agent status.
104
- - `/om:view contemplator` — inspect the contemplator's persisted private transcript.
179
+ - `/om:status` — show memory and background-agent status, including how much recorded memory is still visible vs. pending.
180
+ - `/om:view` — inspect visible memory (and attempt to copy it).
181
+ - `/om:view full` — inspect the full memory ledger, including everything not yet folded into a compaction.
182
+ - `/om:view contemplator` — inspect the contemplator's private transcript and probes.
105
183
  - `/om:view reviewer` — inspect structural reviewer transcripts and outcomes.
106
- - `/om:settings` — inspect or change session-level settings.
184
+ - `/om:settings` — inspect or change session-level settings (including `messages on|off`, `reviewer on|off`, `compaction on|off`).
107
185
 
108
- See [docs/how-it-works.md](docs/how-it-works.md) for the memory lifecycle and detailed behavior.
186
+ Model selection, trigger thresholds, passive mode (which stops all background work), compaction behavior, and other tuning are documented in [docs/configuration.md](docs/configuration.md). See [docs/how-it-works.md](docs/how-it-works.md) for the memory lifecycle and [docs/concepts.md](docs/concepts.md) for the mental model.
109
187
 
110
188
  ## Development
111
189
 
@@ -117,4 +195,4 @@ npm run typecheck
117
195
 
118
196
  ## License
119
197
 
120
- MIT. See [LICENSE](LICENSE).
198
+ MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthewfl/pi-contemplator",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "A Pi extension that keeps long-running agentic sessions on track with background memory, contemplation, and structural review.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,7 +5,7 @@ import { streamSimple } from "@earendil-works/pi-ai/compat";
5
5
  import { generateSummaryWithUsage } from "@earendil-works/pi-coding-agent";
6
6
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
7
  import { Box, Text } from "@earendil-works/pi-tui";
8
- import { assistantOutputTokens, assistantToolCallCount, fullProjection, isReviewRequestEntry, isReviewResultEntry, OM_REVIEWER_MESSAGE, OM_REVIEWER_NOTICE, OM_REVIEWER_STATE, OM_REVIEW_REQUEST, OM_REVIEW_RESULT, type Entry, type ReviewResult, type StructuralReviewRequest } from "../../session-ledger/index.js";
8
+ import { agentActiveTimeMs, assistantOutputTokens, assistantToolCallCount, fullProjection, isReviewRequestEntry, isReviewResultEntry, OM_AGENT_ACTIVITY, OM_REVIEWER_MESSAGE, OM_REVIEWER_NOTICE, OM_REVIEWER_STATE, OM_REVIEW_REQUEST, OM_REVIEW_RESULT, type Entry, type ReviewResult, type StructuralReviewRequest } from "../../session-ledger/index.js";
9
9
  import { hashId } from "../../ids.js";
10
10
  import { createSearchMemoriesAgentTool } from "../../tools/search-memories.js";
11
11
  import { createRecallAgentTool } from "../../tools/recall-observation.js";
@@ -22,6 +22,7 @@ interface PendingUpdate {
22
22
  reviews: string[];
23
23
  mainAgentOutputTokens: number;
24
24
  mainAgentToolCalls: number;
25
+ mainAgentActiveTimeMs: number;
25
26
  }
26
27
 
27
28
  type Intervention =
@@ -90,6 +91,19 @@ function customMessageText(content: unknown): string {
90
91
  .filter(Boolean)
91
92
  .join("\n");
92
93
  }
94
+
95
+ const AGENT_TIME_BUCKET_MINUTES = 5;
96
+
97
+ function coarseAgentTime(durationMs: number): string {
98
+ const totalMinutes = Math.floor(durationMs / 60_000);
99
+ const bucketMinutes = Math.floor(totalMinutes / AGENT_TIME_BUCKET_MINUTES) * AGENT_TIME_BUCKET_MINUTES;
100
+ if (bucketMinutes < AGENT_TIME_BUCKET_MINUTES) return `less than ${AGENT_TIME_BUCKET_MINUTES} minutes`;
101
+ const hours = Math.floor(bucketMinutes / 60);
102
+ const minutes = bucketMinutes % 60;
103
+ if (hours === 0) return `about ${minutes} minutes`;
104
+ const hourLabel = `${hours} hour${hours === 1 ? "" : "s"}`;
105
+ return minutes === 0 ? `about ${hourLabel}` : `about ${hourLabel} ${minutes} minutes`;
106
+ }
93
107
  const CONTEMPLATOR_MESSAGE = "om.contemplator.message";
94
108
  const CONTEMPLATOR_STATE = "om.contemplator.state";
95
109
  const CONTEMPLATOR_SUGGESTION = "om.contemplator.suggestion";
@@ -160,6 +174,8 @@ export class Contemplator {
160
174
  private latestCtx: MemoryUpdateCtx | undefined;
161
175
  private turnsSinceRun = 0;
162
176
  private restoredTipId: string | undefined;
177
+ /** Start of the unpersisted portion of the current main-agent run. */
178
+ private agentActiveSince: number | undefined;
163
179
 
164
180
  constructor(private readonly pi: ExtensionAPI, private readonly runtime: Runtime) {}
165
181
 
@@ -176,8 +192,16 @@ export class Contemplator {
176
192
  return box;
177
193
  });
178
194
  this.runtime.setMemoryUpdateListener((ctx) => this.withDebugContext(ctx, () => this.observeTurn(ctx)));
195
+ this.pi.on("agent_start", () => {
196
+ this.agentActiveSince = Date.now();
197
+ });
198
+ this.pi.on("agent_end", (_event: unknown, ctx: ExtensionContext) => {
199
+ this.persistAgentActivity(ctx);
200
+ this.agentActiveSince = undefined;
201
+ });
179
202
  this.pi.on("session_start", (event: any, ctx: ExtensionContext) => {
180
203
  this.sessionGeneration++;
204
+ this.agentActiveSince = undefined;
181
205
  // AgentSession preserves its steering queue across extension reloads. An
182
206
  // undelivered tracking entry therefore still has a live queued message;
183
207
  // restoring it here would enqueue the same probe a second time.
@@ -186,11 +210,13 @@ export class Contemplator {
186
210
  });
187
211
  this.pi.on("session_tree", (_event: any, ctx: ExtensionContext) => {
188
212
  this.sessionGeneration++;
213
+ this.agentActiveSince = undefined;
189
214
  // Pending steering messages remain queued while navigating the tree.
190
215
  this.restore(ctx, true, true);
191
216
  });
192
217
  this.pi.on("session_shutdown", () => {
193
218
  this.sessionGeneration++;
219
+ this.agentActiveSince = undefined;
194
220
  this.history = [];
195
221
  this.pending = undefined;
196
222
  this.seenObservationIds.clear();
@@ -234,11 +260,23 @@ export class Contemplator {
234
260
  }
235
261
  });
236
262
  this.pi.on("turn_end", (_event: any, ctx: ExtensionContext) => {
263
+ this.persistAgentActivity(ctx);
237
264
  this.turnsSinceRun++;
238
265
  this.withDebugContext(ctx, () => this.observeTurn(ctx));
239
266
  });
240
267
  }
241
268
 
269
+ private persistAgentActivity(ctx: MemoryUpdateCtx): void {
270
+ const startedAt = this.agentActiveSince;
271
+ if (startedAt === undefined) return;
272
+ const endedAt = Date.now();
273
+ this.agentActiveSince = endedAt;
274
+ const durationMs = Math.max(0, endedAt - startedAt);
275
+ if (durationMs === 0) return;
276
+ this.pi.appendEntry(OM_AGENT_ACTIVITY, { version: 1, durationMs, endedAt });
277
+ debugLog("agent.activity_recorded", { durationMs });
278
+ }
279
+
242
280
  private withDebugContext<T>(ctx: MemoryUpdateCtx, fn: () => T): T {
243
281
  this.runtime.ensureConfig(ctx.cwd);
244
282
  const sessionManager = ctx.sessionManager as { getSessionId?: () => string; getSessionFile?: () => string };
@@ -359,7 +397,8 @@ export class Contemplator {
359
397
  debugLog("contemplator.skipped", { reason: "passive" });
360
398
  return;
361
399
  }
362
- const projection = fullProjection(ctx.sessionManager.getBranch() as Entry[]);
400
+ const branchEntries = ctx.sessionManager.getBranch() as Entry[];
401
+ const projection = fullProjection(branchEntries);
363
402
  const observations = projection.observations.map((item) => `[${item.id}] ${item.content}`);
364
403
  const reflections = projection.reflections.map((item) => `[${item.id}] ${item.content}`);
365
404
  const reviews = projection.reviews ?? [];
@@ -387,11 +426,18 @@ export class Contemplator {
387
426
  observations: mergeMemoryLines(this.pending?.observations ?? [], newObservations),
388
427
  reflections: mergeMemoryLines(this.pending?.reflections ?? [], newReflections),
389
428
  reviews: mergeMemoryLines(this.pending?.reviews ?? [], newReviews),
390
- mainAgentOutputTokens: assistantOutputTokens(ctx.sessionManager.getBranch() as Entry[]),
391
- mainAgentToolCalls: assistantToolCallCount(ctx.sessionManager.getBranch() as Entry[]),
429
+ mainAgentOutputTokens: assistantOutputTokens(branchEntries),
430
+ mainAgentToolCalls: assistantToolCallCount(branchEntries),
431
+ mainAgentActiveTimeMs: agentActiveTimeMs(branchEntries),
392
432
  };
393
433
  }
394
434
  if (!this.pending) return;
435
+ // Activity values are cumulative send-time snapshots, not values frozen when
436
+ // the first memory entered a pending batch. This includes work performed
437
+ // while that batch waits for its memory/turn thresholds.
438
+ this.pending.mainAgentOutputTokens = assistantOutputTokens(branchEntries);
439
+ this.pending.mainAgentToolCalls = assistantToolCallCount(branchEntries);
440
+ this.pending.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntries);
395
441
  const enoughMemories = this.pending.reviews.length > 0 || this.pending.observations.length >= this.runtime.config.contemplatorMinNewObservations || this.pending.reflections.length >= this.runtime.config.contemplatorMinNewReflections;
396
442
  if (!enoughMemories || this.turnsSinceRun < this.runtime.config.contemplatorMinTurns) {
397
443
  debugLog("contemplator.waiting", {
@@ -417,7 +463,15 @@ export class Contemplator {
417
463
  debugLog("contemplator.flush_skipped", { reason: this.running ? "already_running" : "no_pending_update" });
418
464
  return;
419
465
  }
466
+ // Checkpoint a live main-agent interval before taking the activity snapshot.
467
+ // persistAgentActivity restarts the running clock at Date.now(), so a later
468
+ // turn/agent checkpoint records only the remainder.
469
+ this.persistAgentActivity(ctx);
420
470
  const update = this.pending;
471
+ const branchEntriesAtStart = ctx.sessionManager.getBranch() as Entry[];
472
+ update.mainAgentOutputTokens = assistantOutputTokens(branchEntriesAtStart);
473
+ update.mainAgentToolCalls = assistantToolCallCount(branchEntriesAtStart);
474
+ update.mainAgentActiveTimeMs = agentActiveTimeMs(branchEntriesAtStart);
421
475
  this.pending = undefined;
422
476
  const turnsBeforeRun = this.turnsSinceRun;
423
477
  const sessionGeneration = this.sessionGeneration;
@@ -452,6 +506,7 @@ export class Contemplator {
452
506
  reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
453
507
  mainAgentOutputTokens: update.mainAgentOutputTokens,
454
508
  mainAgentToolCalls: update.mainAgentToolCalls,
509
+ mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
455
510
  };
456
511
  this.turnsSinceRun = turnsBeforeRun;
457
512
  }
@@ -476,7 +531,7 @@ export class Contemplator {
476
531
  const interventionInstruction = reviewerEnabled
477
532
  ? "Use send_probe for one focused question, or request_review only when a deeper workflow or software review is justified. Use no more than one intervention."
478
533
  : "Use send_probe only when one focused question is materially useful. Use no more than one intervention.";
479
- const prompt: Message = { role: "user", content: [{ type: "text", text: `NEW MEMORY UPDATE\n\n${updateBody}\n\nACTIVITY SIGNAL cumulative primary-agent generated tokens: ${update.mainAgentOutputTokens}; cumulative primary-agent tool calls: ${update.mainAgentToolCalls}\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() };
534
+ 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() };
480
535
  promptMessage = prompt;
481
536
  this.history.push(prompt);
482
537
  let intervention: Intervention | undefined;
@@ -578,6 +633,7 @@ export class Contemplator {
578
633
  reviews: mergeMemoryLines(pending?.reviews ?? [], update.reviews),
579
634
  mainAgentOutputTokens: update.mainAgentOutputTokens,
580
635
  mainAgentToolCalls: update.mainAgentToolCalls,
636
+ mainAgentActiveTimeMs: update.mainAgentActiveTimeMs,
581
637
  };
582
638
  this.turnsSinceRun = turnsBeforeRun;
583
639
  }
@@ -3,6 +3,7 @@ import { observationPoolMetrics } from "../agents/dropper/pool.js";
3
3
  import { resolveCompactAfterTokens } from "../config.js";
4
4
  import type { Runtime } from "../runtime.js";
5
5
  import {
6
+ agentActiveTimeMs,
6
7
  diffProjection,
7
8
  foldLedger,
8
9
  fullProjection,
@@ -26,6 +27,16 @@ function formatTokens(n: number): string {
26
27
  return String(n);
27
28
  }
28
29
 
30
+ function formatDuration(durationMs: number): string {
31
+ const totalSeconds = Math.floor(durationMs / 1_000);
32
+ const hours = Math.floor(totalSeconds / 3_600);
33
+ const minutes = Math.floor((totalSeconds % 3_600) / 60);
34
+ const seconds = totalSeconds % 60;
35
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
36
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
37
+ return `${seconds}s`;
38
+ }
39
+
29
40
  function truncateStatusText(value: string, limit = 1_000): string {
30
41
  return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
31
42
  }
@@ -99,6 +110,7 @@ export function registerStatusCommand(pi: ExtensionAPI, runtime: Runtime): void
99
110
  `Visible observation pool: ~${visibleObservationTokens.toLocaleString()} / ${runtime.config.observationsPoolMaxTokens.toLocaleString()} tokens (${pct(visibleObservationTokens, runtime.config.observationsPoolMaxTokens)}%)`,
100
111
  `Active observation pool: ~${activeObservationPool.observationTokens.toLocaleString()} / ${runtime.config.observationsPoolTargetTokens.toLocaleString()} target tokens (${pct(activeObservationPool.observationTokens, runtime.config.observationsPoolTargetTokens)}%)`,
101
112
  `Reflection pool: ~${visibleReflectionTokens.toLocaleString()} tokens`,
113
+ `Cumulative agent time: ${formatDuration(agentActiveTimeMs(entries))}`,
102
114
  `Compaction observer: ${runtime.config.compactionObserverEnabled === false ? "disabled" : "enabled"}`,
103
115
  `Contemplator: ${runtime.config.contemplatorEnabled ? "enabled" : "disabled"}`,
104
116
  `Contemplator model: ${runtime.config.contemplatorModel ? `${runtime.config.contemplatorModel.provider}/${runtime.config.contemplatorModel.id}` : "current session model"}`,
@@ -1,5 +1,6 @@
1
1
  import { estimateEntryTokens } from "../tokens.js";
2
2
  import {
3
+ OM_AGENT_ACTIVITY,
3
4
  OM_OBSERVATIONS_DROPPED,
4
5
  OM_OBSERVATIONS_RECORDED,
5
6
  OM_REFLECTIONS_RECORDED,
@@ -52,6 +53,17 @@ export function assistantToolCallCount(entries: Entry[]): number {
52
53
  return total;
53
54
  }
54
55
 
56
+ /** Sum persisted main-agent active wall-clock time on the current branch. */
57
+ export function agentActiveTimeMs(entries: Entry[]): number {
58
+ let total = 0;
59
+ for (const entry of entries) {
60
+ if (entry.type !== "custom" || entry.customType !== OM_AGENT_ACTIVITY || !isObject(entry.data)) continue;
61
+ const durationMs = entry.data.durationMs;
62
+ if (typeof durationMs === "number" && Number.isFinite(durationMs) && durationMs >= 0) total += durationMs;
63
+ }
64
+ return total;
65
+ }
66
+
55
67
  function isObject(value: unknown): value is Record<string, unknown> {
56
68
  return typeof value === "object" && value !== null;
57
69
  }
@@ -9,6 +9,8 @@ export const OM_REVIEWER_MESSAGE = "om.reviewer.message";
9
9
  export const OM_REVIEWER_STATE = "om.reviewer.state";
10
10
  /** Compact proposal notice queued for the primary agent. */
11
11
  export const OM_REVIEWER_NOTICE = "om.reviewer.notice";
12
+ /** Main-agent active wall-clock time, excluding idle waits for user input. */
13
+ export const OM_AGENT_ACTIVITY = "om.agent.activity";
12
14
  export const OM_FOLDED = "om.folded";
13
15
 
14
16
  export const RELEVANCE_VALUES = ["low", "medium", "high", "critical"] as const;