@matthewfl/pi-contemplator 0.0.9 → 0.0.10

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
@@ -190,9 +190,15 @@ Model selection, trigger thresholds, passive mode (which stops all background wo
190
190
  ```bash
191
191
  npm install
192
192
  npm test
193
+ npm run test:unit
194
+ npm run test:e2e
193
195
  npm run typecheck
194
196
  ```
195
197
 
198
+ `npm test` runs both the unit and RPC end-to-end suites. Use `test:unit` or `test:e2e` to run either suite independently.
199
+
200
+ `test:e2e` starts a local OpenAI-compatible model server and launches isolated real Pi CLI processes in RPC mode while mocking only the external model-server boundary. The suites cover observer, reflector, dropper, contemplator, reviewer, primary-agent, memory-tool, and compaction flows. They exercise slow and parallel tools, probe races and feedback, hidden/idle delivery, cumulative activity time, process restore, reviewer transcript resume and budget exhaustion, accepted/rejected reviews, manual/failed compaction continuations and non-restarting proactive compaction, compaction observer sidecars, malformed memory records, huge-source bounding, id collisions, role-specific model routing, feature flags, session-tree forks, and concurrent session isolation.
201
+
196
202
  ## License
197
203
 
198
204
  MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matthewfl/pi-contemplator",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
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",
@@ -39,7 +39,9 @@
39
39
  ],
40
40
  "scripts": {
41
41
  "typecheck": "tsc --noEmit",
42
- "test": "vitest run"
42
+ "test": "npm run test:unit && npm run test:e2e",
43
+ "test:unit": "vitest run",
44
+ "test:e2e": "node tests/e2e/rpc-contemplator.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-routing-isolation.mjs"
43
45
  },
44
46
  "peerDependencies": {
45
47
  "@earendil-works/pi-agent-core": "*",
@@ -48,10 +50,10 @@
48
50
  "@earendil-works/pi-tui": "*"
49
51
  },
50
52
  "devDependencies": {
51
- "@earendil-works/pi-agent-core": "^0.81.0",
52
- "@earendil-works/pi-ai": "^0.81.0",
53
- "@earendil-works/pi-coding-agent": "^0.81.0",
54
- "@earendil-works/pi-tui": "^0.81.0",
53
+ "@earendil-works/pi-agent-core": "^0.84.2",
54
+ "@earendil-works/pi-ai": "^0.84.2",
55
+ "@earendil-works/pi-coding-agent": "^0.84.2",
56
+ "@earendil-works/pi-tui": "^0.84.2",
55
57
  "@types/node": "^22.0.0",
56
58
  "typebox": "^1.1.38",
57
59
  "typescript": "^5.6.0",
@@ -29,10 +29,11 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
29
29
 
30
30
  const initiatedByOm = runtime.compactInFlight && event.reason === "manual";
31
31
  const reason = initiatedByOm ? (runtime.compactOrigin ?? "proactive") : event.reason;
32
+ const omWillResume = initiatedByOm && (runtime.compactOrigin === "agent-requested" || runtime.compactOrigin === "length-stop");
32
33
  if (ctx.hasUI) {
33
34
  let pending = "";
34
35
  if (event.willRetry) pending = ", retry pending";
35
- else if (initiatedByOm) pending = ", resume pending";
36
+ else if (omWillResume) pending = ", resume pending";
36
37
  ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, `OM compaction: running (${reason}${pending})`);
37
38
  if (!initiatedByOm) {
38
39
  const continuation = event.willRetry ? "; the interrupted agent run will resume automatically" : "";
@@ -88,12 +89,13 @@ export function registerCompactionHook(pi: ExtensionAPI, runtime: Runtime): void
88
89
  pi.on("session_compact", (event: any, ctx: any) => {
89
90
  const initiatedByOm = runtime.compactInFlight && event.reason === "manual";
90
91
  const reason = initiatedByOm ? (runtime.compactOrigin ?? "proactive") : event.reason;
92
+ const omWillResume = initiatedByOm && (runtime.compactOrigin === "agent-requested" || runtime.compactOrigin === "length-stop");
91
93
  if (event.willRetry) watchForNativeCompactionResume(pi, runtime, ctx);
92
94
  if (!ctx.hasUI) return;
93
95
  ctx.ui.setStatus?.(COMPACTION_STATUS_KEY, undefined);
94
96
  let continuation = "";
95
97
  if (event.willRetry) continuation = "; resuming the interrupted agent run";
96
- else if (initiatedByOm) continuation = "; resuming the agent run";
98
+ else if (omWillResume) continuation = "; resuming the agent run";
97
99
  ctx.ui.notify(`Observational memory: compaction complete (${reason})${continuation}`, "info");
98
100
  });
99
101
  }
@@ -9,52 +9,22 @@ import {
9
9
  } from "./compaction-resume.js";
10
10
 
11
11
  const COMPACTION_STATUS_KEY = "observational-memory-compaction";
12
+ type CompactionOrigin = "agent-requested" | "length-stop" | "proactive";
13
+
14
+ type TriggerOptions = {
15
+ origin: CompactionOrigin;
16
+ resume: boolean;
17
+ threshold?: number;
18
+ shortContinuationPrompt?: string;
19
+ };
12
20
 
13
21
  export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): void {
14
22
  registerCompactionResumeAcknowledgement(pi, runtime);
15
- pi.on("agent_end", (event: any, ctx: any) => {
16
- runtime.ensureConfig(ctx.cwd);
17
- if (runtime.compactInFlight) return;
18
-
19
- const agentRequested = runtime.compactRequested;
20
- const shortContinuationPrompt = agentRequested ? runtime.compactContinuationPrompt : undefined;
21
- if (agentRequested) runtime.compactRequested = false;
22
- else if (runtime.config.passive === true) return;
23
23
 
24
- const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : 0;
25
- let threshold: number | undefined;
26
- if (!agentRequested) {
27
- // agent_end fires before Pi decides whether to retry or compact-and-retry an
28
- // interrupted request. Starting ctx.compact() here turns it into a manual
29
- // compaction (willRetry=false) and can consume Pi's overflow recovery, leaving
30
- // the agent idle. Let Pi handle every failed/aborted/overflow response; OM's
31
- // session_before_compact hook still supplies the actual memory compaction.
32
- const lastAssistant = [...event.messages].reverse().find(
33
- (m): m is Extract<typeof m, { role: "assistant" }> => m.role === "assistant",
34
- );
35
- if (
36
- lastAssistant
37
- && (
38
- lastAssistant.stopReason === "error"
39
- || lastAssistant.stopReason === "aborted"
40
- || isContextOverflow(lastAssistant, contextWindow)
41
- )
42
- ) return;
43
-
44
- const entries = ctx.sessionManager.getBranch() as Entry[];
45
- const tokens = rawTokensSinceLastCompaction(entries);
46
- // Resolve the proactive-compaction threshold from the active model's context
47
- // window when ratio mode is configured. ctx.model is the current session model
48
- // (Model<any> | undefined per ExtensionContext).
49
- threshold = resolveCompactAfterTokens(runtime.config, contextWindow > 0 ? contextWindow : undefined);
50
- if (tokens < threshold) return;
51
- }
52
-
53
- // Capture ctx properties synchronously — the setTimeout + async work below
54
- // may outlive the extension ctx (stale after session replacement/reload).
24
+ const triggerCompaction = (ctx: any, options: TriggerOptions): void => {
25
+ const { origin, resume, threshold, shortContinuationPrompt } = options;
55
26
  const hasUI = ctx.hasUI;
56
27
  const ui = ctx.ui;
57
- const origin = agentRequested ? "agent-requested" : "proactive";
58
28
 
59
29
  runtime.compactInFlight = true;
60
30
  runtime.compactOrigin = origin;
@@ -63,7 +33,7 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
63
33
  if (!ctx.isIdle()) {
64
34
  runtime.compactInFlight = false;
65
35
  runtime.compactOrigin = undefined;
66
- if (agentRequested) runtime.compactRequested = true;
36
+ if (origin === "agent-requested") runtime.compactRequested = true;
67
37
  if (hasUI) ui?.notify(
68
38
  "Observational memory: compaction deferred — agent became busy before compaction",
69
39
  "info",
@@ -82,22 +52,22 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
82
52
  return;
83
53
  }
84
54
  if (hasUI) {
85
- ui?.setStatus?.(COMPACTION_STATUS_KEY, `OM compaction: running (${origin}, resume pending)`);
86
- const reason = agentRequested ? "agent-requested, " : "";
55
+ const pending = resume ? ", resume pending" : "";
56
+ ui?.setStatus?.(COMPACTION_STATUS_KEY, `OM compaction: running (${origin}${pending})`);
57
+ const reason = origin === "agent-requested" ? "agent-requested, " : origin === "length-stop" ? "length-stop, " : "";
58
+ const continuation = resume ? "; the interrupted agent run will resume automatically" : "";
87
59
  ui?.notify(
88
- `Observational memory: compaction started (${reason}~${currentTokens.toLocaleString()} tokens); the agent will resume automatically`,
60
+ `Observational memory: compaction started (${reason}~${currentTokens.toLocaleString()} tokens)${continuation}`,
89
61
  "info",
90
62
  );
91
63
  }
92
- if (agentRequested) runtime.compactContinuationPrompt = undefined;
64
+ if (origin === "agent-requested") runtime.compactContinuationPrompt = undefined;
93
65
  ctx.compact({
94
66
  onComplete: () => {
95
67
  runtime.compactInFlight = false;
96
68
  runtime.compactOrigin = undefined;
97
- // Both explicit and proactive OM compactions are manual from Pi's
98
- // perspective (willRetry=false), so Pi will not continue either one.
99
- // Always enqueue a hidden continuation after OM finishes compacting.
100
- resumeAfterCompaction(pi, runtime, { hasUI, ui }, false, shortContinuationPrompt);
69
+ if (hasUI && !resume) ui?.setStatus?.(COMPACTION_STATUS_KEY, undefined);
70
+ if (resume) resumeAfterCompaction(pi, runtime, { hasUI, ui }, false, shortContinuationPrompt);
101
71
  },
102
72
  onError: (error: { message: string }) => {
103
73
  runtime.compactInFlight = false;
@@ -106,20 +76,68 @@ export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): v
106
76
  if (error.message !== "Compaction cancelled" && hasUI) {
107
77
  ui?.notify(`Observational memory: ${error.message}`, "error");
108
78
  }
109
- resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
79
+ if (resume) resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
110
80
  },
111
81
  });
112
82
  } catch (error) {
113
83
  runtime.compactInFlight = false;
114
- if (agentRequested) runtime.compactContinuationPrompt = undefined;
84
+ if (origin === "agent-requested") runtime.compactContinuationPrompt = undefined;
115
85
  runtime.compactOrigin = undefined;
116
86
  const msg = error instanceof Error ? error.message : String(error);
117
87
  if (hasUI) {
118
88
  ui?.setStatus?.(COMPACTION_STATUS_KEY, undefined);
119
89
  ui?.notify(`Observational memory: compact threw: ${msg}`, "error");
120
90
  }
121
- resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
91
+ if (resume) resumeAfterCompaction(pi, runtime, { hasUI, ui }, true, shortContinuationPrompt);
122
92
  }
123
93
  }, 0);
94
+ };
95
+
96
+ pi.on("agent_end", (event: any, ctx: any) => {
97
+ runtime.ensureConfig(ctx.cwd);
98
+ if (runtime.compactInFlight) return;
99
+
100
+ const agentRequested = runtime.compactRequested;
101
+ if (agentRequested) {
102
+ const shortContinuationPrompt = runtime.compactContinuationPrompt;
103
+ runtime.compactRequested = false;
104
+ triggerCompaction(ctx, { origin: "agent-requested", resume: true, shortContinuationPrompt });
105
+ return;
106
+ }
107
+ if (runtime.config.passive === true) return;
108
+
109
+ // Pi owns error, abort, and overflow retry policy. OM's session hook still
110
+ // supplies the compaction contents when Pi performs a native retry.
111
+ const lastAssistant = [...event.messages].reverse().find(
112
+ (m): m is Extract<typeof m, { role: "assistant" }> => m.role === "assistant",
113
+ );
114
+ const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : 0;
115
+ if (
116
+ !lastAssistant
117
+ || lastAssistant.stopReason === "error"
118
+ || lastAssistant.stopReason === "aborted"
119
+ || isContextOverflow(lastAssistant, contextWindow)
120
+ ) return;
121
+
122
+ // A non-overflow length stop is interrupted work, not a normal completed
123
+ // turn. Preserve the older compact-and-resume behavior only for this case.
124
+ if (lastAssistant.stopReason !== "length") return;
125
+ const entries = ctx.sessionManager.getBranch() as Entry[];
126
+ const threshold = resolveCompactAfterTokens(runtime.config, contextWindow > 0 ? contextWindow : undefined);
127
+ if (rawTokensSinceLastCompaction(entries) < threshold) return;
128
+ triggerCompaction(ctx, { origin: "length-stop", resume: true, threshold });
129
+ });
130
+
131
+ // Proactive threshold compaction is maintenance after Pi has fully settled.
132
+ // It must not manufacture another agent turn: the previous run completed
133
+ // normally and there is no interrupted work to resume.
134
+ pi.on("agent_settled", (_event: any, ctx: any) => {
135
+ runtime.ensureConfig(ctx.cwd);
136
+ if (runtime.config.passive === true || runtime.compactInFlight || runtime.compactRequested) return;
137
+ const entries = ctx.sessionManager.getBranch() as Entry[];
138
+ const contextWindow = typeof ctx.model?.contextWindow === "number" ? ctx.model.contextWindow : undefined;
139
+ const threshold = resolveCompactAfterTokens(runtime.config, contextWindow);
140
+ if (rawTokensSinceLastCompaction(entries) < threshold) return;
141
+ triggerCompaction(ctx, { origin: "proactive", resume: false, threshold });
124
142
  });
125
143
  }
package/src/runtime.ts CHANGED
@@ -147,7 +147,7 @@ export class Runtime {
147
147
  compactRequested = false;
148
148
  /** Agent-authored instructions to deliver after an explicit compact_context request. */
149
149
  compactContinuationPrompt: string | undefined;
150
- compactOrigin: "proactive" | "agent-requested" | undefined;
150
+ compactOrigin: "proactive" | "agent-requested" | "length-stop" | undefined;
151
151
  compactHookInFlight = false;
152
152
  compactionResumePending = false;
153
153
  compactionResumeGeneration = 0;