pi-blackhole 0.3.8 → 0.3.9
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/om/compaction-trigger.ts +117 -56
- package/src/om/runtime.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-blackhole",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"packageManager": "pnpm@11.2.2",
|
|
5
5
|
"description": "Unified compaction + observational memory extension for Pi — compresses conversation context while preserving durable observations and reflections",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,17 @@ function notifySafely(hasUI: boolean, ui: any, message: string, level: "info" |
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function registerCompactionTrigger(pi: ExtensionAPI, runtime: Runtime): void {
|
|
30
|
+
pi.on("agent_start", () => {
|
|
31
|
+
// A new turn is starting — abort any pending auto-compaction wait.
|
|
32
|
+
// The new turn's own agent_end will re-evaluate the threshold and
|
|
33
|
+
// schedule a fresh wait if compaction is still needed.
|
|
34
|
+
if (runtime.autoCompactionController) {
|
|
35
|
+
runtime.autoCompactionController.abort();
|
|
36
|
+
runtime.autoCompactionController = null;
|
|
37
|
+
runtime.compactInFlight = false;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
30
41
|
pi.on("agent_end", (event: any, ctx: any) => {
|
|
31
42
|
try {
|
|
32
43
|
handleAgentEnd(event, ctx, runtime);
|
|
@@ -132,17 +143,51 @@ function handleAgentEnd(event: any, ctx: any, runtime: Runtime): void {
|
|
|
132
143
|
"info",
|
|
133
144
|
);
|
|
134
145
|
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
runtime.compactInFlight = true;
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
runtime.autoCompactionController = controller;
|
|
149
|
+
const signal = controller.signal;
|
|
150
|
+
dbg("compaction_trigger.scheduled", { compactInFlight: runtime.compactInFlight });
|
|
151
|
+
|
|
152
|
+
// Issue #31: keep waiting for the agent to become idle instead of bailing
|
|
153
|
+
// after the first non-idle check. The agent may need a few hundred ms to
|
|
154
|
+
// finish async work from other extension handlers (e.g. pi-rewind's
|
|
155
|
+
// checkpoint I/O) before it is truly idle. The only legitimate cancellation
|
|
156
|
+
// is the agent_start handler above aborting the controller.
|
|
157
|
+
void (async () => {
|
|
158
|
+
try {
|
|
159
|
+
// Yield to the event loop first — matches the historical
|
|
160
|
+
// setTimeout(0) deferral that lets other agent_end listeners run.
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
162
|
+
|
|
163
|
+
// Poll isIdle() every 200ms until it returns true. No max-retries
|
|
164
|
+
// cap: the user can be reading the response for arbitrarily long.
|
|
165
|
+
// ctx.compact() itself aborts any in-flight agent operation, so we
|
|
166
|
+
// must wait until the agent is truly idle.
|
|
167
|
+
let isIdle = false;
|
|
168
|
+
while (!isIdle) {
|
|
169
|
+
if (signal.aborted) {
|
|
170
|
+
dbg("compaction_trigger.microtask.bail", { reason: "aborted_agent_start" });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
137
173
|
|
|
138
|
-
setTimeout(() => {
|
|
139
|
-
dbg("compaction_trigger.microtask.enter", {});
|
|
140
|
-
try {
|
|
141
174
|
// Validate session identity — bail if the session was replaced/reloaded.
|
|
142
|
-
|
|
175
|
+
let currentSessionId: string;
|
|
176
|
+
try {
|
|
177
|
+
currentSessionId = ctx.sessionManager.getSessionId();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (isStaleExtensionContextError(error)) {
|
|
180
|
+
runtime.compactInFlight = false;
|
|
181
|
+
runtime.autoCompactionController = null;
|
|
182
|
+
dbg("compaction_trigger.microtask.bail", { reason: "stale_ctx" });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
143
187
|
dbg("compaction_trigger.microtask.session_check", { currentSessionId, expectedSessionId: sessionId, match: currentSessionId === sessionId });
|
|
144
188
|
if (currentSessionId !== sessionId) {
|
|
145
189
|
runtime.compactInFlight = false;
|
|
190
|
+
runtime.autoCompactionController = null;
|
|
146
191
|
dbg("compaction_trigger.microtask.bail", { reason: "session_changed" });
|
|
147
192
|
notifySafely(
|
|
148
193
|
hasUI,
|
|
@@ -153,60 +198,76 @@ function handleAgentEnd(event: any, ctx: any, runtime: Runtime): void {
|
|
|
153
198
|
return;
|
|
154
199
|
}
|
|
155
200
|
|
|
156
|
-
|
|
201
|
+
isIdle = ctx.isIdle();
|
|
157
202
|
dbg("compaction_trigger.microtask.idle_check", { isIdle });
|
|
158
203
|
if (!isIdle) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
const currentEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
170
|
-
const currentTokens = rawTokensSinceLastCompaction(currentEntries);
|
|
171
|
-
dbg("compaction_trigger.microtask.recheck_tokens", { currentTokens, threshold: runtime.config.compactAfterTokens, ok: currentTokens >= runtime.config.compactAfterTokens });
|
|
172
|
-
if (currentTokens < runtime.config.compactAfterTokens) {
|
|
173
|
-
runtime.compactInFlight = false;
|
|
174
|
-
dbg("compaction_trigger.microtask.bail", { reason: "pressure_relieved", currentTokens, threshold: runtime.config.compactAfterTokens });
|
|
175
|
-
notifySafely(
|
|
176
|
-
hasUI,
|
|
177
|
-
ui,
|
|
178
|
-
"Observational memory: compaction skipped — another compaction already ran before deferred compaction",
|
|
179
|
-
"info",
|
|
180
|
-
);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
dbg("compaction_trigger.microtask.calling_compact", {});
|
|
185
|
-
ctx.compact({
|
|
186
|
-
onComplete: (result: any) => {
|
|
187
|
-
runtime.compactInFlight = false;
|
|
188
|
-
dbg("compaction_trigger.onComplete", { result: !!result });
|
|
189
|
-
notifySafely(hasUI, ui, "Observational memory: compaction complete", "info");
|
|
190
|
-
},
|
|
191
|
-
onError: (error: { message: string }) => {
|
|
192
|
-
runtime.compactInFlight = false;
|
|
193
|
-
dbg("compaction_trigger.onError", { message: error?.message ?? String(error) });
|
|
194
|
-
if (error.message === "Compaction cancelled") {
|
|
195
|
-
// We already notified the user with the real reason before returning { cancel: true }.
|
|
204
|
+
// Sleep in 50ms slices so agent_start aborts are noticed quickly.
|
|
205
|
+
// A single 200ms await would let the loop run for 200ms after
|
|
206
|
+
// the user typed — too long, since we want compaction to wait
|
|
207
|
+
// only for the agent to settle, not for a full tick.
|
|
208
|
+
const sliceMs = 50;
|
|
209
|
+
const end = Date.now() + 200;
|
|
210
|
+
while (Date.now() < end) {
|
|
211
|
+
if (signal.aborted) {
|
|
212
|
+
dbg("compaction_trigger.microtask.bail", { reason: "aborted_agent_start" });
|
|
196
213
|
return;
|
|
197
214
|
}
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
} catch (error) {
|
|
202
|
-
runtime.compactInFlight = false;
|
|
203
|
-
const msg = getErrorMessage(error);
|
|
204
|
-
if (isStaleExtensionContextError(error)) {
|
|
205
|
-
dbg("compaction_trigger.microtask.bail", { reason: "stale_ctx", message: msg });
|
|
206
|
-
return;
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, sliceMs));
|
|
216
|
+
}
|
|
207
217
|
}
|
|
208
|
-
dbg("compaction_trigger.microtask.error", { message: msg });
|
|
209
|
-
notifySafely(hasUI, ui, `Observational memory: compact threw: ${msg}`, "error");
|
|
210
218
|
}
|
|
211
|
-
|
|
219
|
+
|
|
220
|
+
if (signal.aborted) {
|
|
221
|
+
dbg("compaction_trigger.microtask.bail", { reason: "aborted_agent_start" });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const currentEntries = ctx.sessionManager.getBranch() as Entry[];
|
|
226
|
+
const currentTokens = rawTokensSinceLastCompaction(currentEntries);
|
|
227
|
+
dbg("compaction_trigger.microtask.recheck_tokens", { currentTokens, threshold: runtime.config.compactAfterTokens, ok: currentTokens >= runtime.config.compactAfterTokens });
|
|
228
|
+
if (currentTokens < runtime.config.compactAfterTokens) {
|
|
229
|
+
runtime.compactInFlight = false;
|
|
230
|
+
runtime.autoCompactionController = null;
|
|
231
|
+
dbg("compaction_trigger.microtask.bail", { reason: "pressure_relieved", currentTokens, threshold: runtime.config.compactAfterTokens });
|
|
232
|
+
notifySafely(
|
|
233
|
+
hasUI,
|
|
234
|
+
ui,
|
|
235
|
+
"Observational memory: compaction skipped — another compaction already ran before deferred compaction",
|
|
236
|
+
"info",
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
dbg("compaction_trigger.microtask.calling_compact", {});
|
|
242
|
+
// Compaction is now actually starting — clear the controller so
|
|
243
|
+
// agent_start doesn't abort an in-progress compact.
|
|
244
|
+
runtime.autoCompactionController = null;
|
|
245
|
+
ctx.compact({
|
|
246
|
+
onComplete: (result: any) => {
|
|
247
|
+
runtime.compactInFlight = false;
|
|
248
|
+
dbg("compaction_trigger.onComplete", { result: !!result });
|
|
249
|
+
notifySafely(hasUI, ui, "Observational memory: compaction complete", "info");
|
|
250
|
+
},
|
|
251
|
+
onError: (error: { message: string }) => {
|
|
252
|
+
runtime.compactInFlight = false;
|
|
253
|
+
dbg("compaction_trigger.onError", { message: error?.message ?? String(error) });
|
|
254
|
+
if (error.message === "Compaction cancelled") {
|
|
255
|
+
// We already notified the user with the real reason before returning { cancel: true }.
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
notifySafely(hasUI, ui, `Observational memory: ${error.message}`, "error");
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
runtime.compactInFlight = false;
|
|
263
|
+
runtime.autoCompactionController = null;
|
|
264
|
+
const msg = getErrorMessage(error);
|
|
265
|
+
if (isStaleExtensionContextError(error)) {
|
|
266
|
+
dbg("compaction_trigger.microtask.bail", { reason: "stale_ctx", message: msg });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
dbg("compaction_trigger.microtask.error", { message: msg });
|
|
270
|
+
notifySafely(hasUI, ui, `Observational memory: compact threw: ${msg}`, "error");
|
|
271
|
+
}
|
|
272
|
+
})();
|
|
212
273
|
}
|
package/src/om/runtime.ts
CHANGED
|
@@ -69,6 +69,10 @@ export class Runtime {
|
|
|
69
69
|
failedInCycle: Set<string> = new Set();
|
|
70
70
|
compactInFlight = false;
|
|
71
71
|
compactHookInFlight = false;
|
|
72
|
+
/** AbortController for the pending auto-compaction wait loop, or null if none.
|
|
73
|
+
* Set when handleAgentEnd schedules a wait; cleared on abort, success, or terminal bail.
|
|
74
|
+
* agent_start handlers read this to abort the pending wait when a new turn starts. */
|
|
75
|
+
autoCompactionController: AbortController | null = null;
|
|
72
76
|
resolveFailureNotified = false;
|
|
73
77
|
lastObserverError: string | undefined;
|
|
74
78
|
lastReflectorError: string | undefined;
|