@matthewfl/pi-contemplator 0.0.7 → 0.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthewfl/pi-contemplator",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
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",
@@ -244,11 +244,24 @@ export class Contemplator {
244
244
  }
245
245
  this.persistReviewerStates(ctx);
246
246
  });
247
+ this.pi.on("message_end", (event: any) => {
248
+ const message = event?.message;
249
+ if (message?.role !== "custom" || message.customType !== CONTEMPLATOR_SUGGESTION) return;
250
+ if (typeof message.details?.probeId !== "string") return;
251
+ // message_end means Pi has drained the steer into the conversation
252
+ // stream. It is no longer protected by an in-memory queue, so a later
253
+ // tree restore must be allowed to requeue it until context acknowledges it.
254
+ this.queuedProbeIds.delete(message.details.probeId);
255
+ });
247
256
  this.pi.on("context", (event: any, ctx: ExtensionContext) => {
248
257
  const deliveredMessages = event.messages?.filter((message: any) => message?.role === "custom" && message.customType === CONTEMPLATOR_SUGGESTION && typeof message.details?.probeId === "string") ?? [];
249
258
  for (const delivered of deliveredMessages) {
250
259
  if (this.deliveredProbeIds.has(delivered.details.probeId)) continue;
251
260
  this.deliveredProbeIds.add(delivered.details.probeId);
261
+ // Once Pi includes the probe in a provider context it is no longer in
262
+ // either in-memory delivery queue. Keeping this id indefinitely caused
263
+ // later tree restores to suppress a genuinely needed requeue.
264
+ this.queuedProbeIds.delete(delivered.details.probeId);
252
265
  this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, {
253
266
  version: 1,
254
267
  suggestion: typeof delivered.details.question === "string" ? delivered.details.question : String(delivered.content ?? ""),
@@ -310,12 +323,7 @@ export class Contemplator {
310
323
  this.turnsSinceRun = 0;
311
324
  }
312
325
  const undeliveredSuggestions = new Map<string, string>();
313
- const queuedProbeIds = new Set<string>();
314
326
  for (const entry of entries) {
315
- if (entry.customType === CONTEMPLATOR_SUGGESTION && entry.type === "custom_message") {
316
- const details = entry.details as { probeId?: unknown } | undefined;
317
- if (typeof details?.probeId === "string") queuedProbeIds.add(details.probeId);
318
- }
319
327
  if (entry.customType === CONTEMPLATOR_STATE && entry.data && typeof entry.data === "object") {
320
328
  const state = entry.data as { history?: unknown };
321
329
  if (Array.isArray(state.history)) this.history = state.history.filter((message): message is AgentMessage => !!message && typeof message === "object");
@@ -380,7 +388,11 @@ export class Contemplator {
380
388
  }
381
389
  this.restoredTipId = tipId;
382
390
  for (const [probeId, question] of undeliveredSuggestions) {
383
- if (skipUndeliveredRestore || queuedProbeIds.has(probeId) || this.queuedProbeIds.has(probeId)) continue;
391
+ // A durable custom_message proves only that Pi inserted the probe at some
392
+ // point; it does not prove an in-memory queue still owns it, and compaction
393
+ // may have removed it from active model context. Suppress requeue only for
394
+ // ids this live extension instance still knows are queued.
395
+ if (skipUndeliveredRestore || this.queuedProbeIds.has(probeId)) continue;
384
396
  this.queueProbe(ctx, question, "restore", probeId);
385
397
  }
386
398
  if (resetTracking) void this.resumePendingReviews(ctx);
@@ -650,18 +662,22 @@ export class Contemplator {
650
662
 
651
663
  private queueProbe(ctx: MemoryUpdateCtx, question: string, source: "send_probe" | "restore", existingProbeId?: string): void {
652
664
  const probeId = existingProbeId ?? `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
665
+ // Persist intent before touching Pi's in-memory queue. A crash in between
666
+ // leaves a recoverable pending probe rather than an invisible lost one.
667
+ this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, { version: 1, suggestion: question, delivered: false, source, probeId });
668
+ this.markTipPersisted(ctx);
669
+ // DELIVERY INVARIANT: probes must always use steer. The contemplator is
670
+ // designed for agents that run for hours; a probe must be injected after
671
+ // the current tool-call batch, before the very next model request. Never
672
+ // change this to nextTurn: that can postpone delivery until a user prompt.
673
+ // triggerTurn stays false so a probe never starts an extra agent turn.
674
+ if (this.agentActiveSince !== undefined) this.queuedProbeIds.add(probeId);
653
675
  this.pi.sendMessage({
654
676
  customType: CONTEMPLATOR_SUGGESTION,
655
677
  content: `Background contemplator probe (advisory):\n${question}`,
656
678
  display: this.runtime.config.showContemplatorMessages,
657
679
  details: { version: 1, question, source, probeId },
658
680
  }, { deliverAs: "steer", triggerTurn: false });
659
- // sendMessage queues synchronously. Mark every source (not only restore)
660
- // before a later turn_end can rebuild state from the still-undelivered
661
- // tracking entry and enqueue this probe again.
662
- this.queuedProbeIds.add(probeId);
663
- this.pi.appendEntry(CONTEMPLATOR_SUGGESTION, { version: 1, suggestion: question, delivered: false, source, probeId });
664
- this.markTipPersisted(ctx);
665
681
  debugLog("contemplator.suggestion_queued", {
666
682
  probeId,
667
683
  suggestionLength: question.length,