pi-mega-compact 0.20.65 → 0.20.66

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.
@@ -131,7 +131,8 @@ export function harness(opts = {}) {
131
131
  registerCommand: () => { }, registerTool: () => { }, registerShortcut: () => { },
132
132
  registerFlag: () => { }, getFlag: () => undefined,
133
133
  registerMessageRenderer: () => { }, registerEntryRenderer: () => { },
134
- sendMessage: () => { }, sendUserMessage: (m) => { sendUserMessages.push(m); },
134
+ sendMessage: (m) => { if (typeof m?.content === "string")
135
+ sendUserMessages.push(m.content); }, sendUserMessage: (m) => { sendUserMessages.push(m); },
135
136
  appendEntry: (t, d) => appended.push({ t, d }),
136
137
  setSessionName: () => { }, getSessionName: () => undefined, setLabel: () => { },
137
138
  exec: async () => ({ stdout: "", stderr: "", code: 0 }),
@@ -186,7 +186,10 @@ export function harness(opts = {}) {
186
186
  getFlag: () => undefined,
187
187
  registerMessageRenderer: () => { },
188
188
  registerEntryRenderer: () => { },
189
- sendMessage: (_m) => { },
189
+ sendMessage: (m) => {
190
+ if (typeof m?.content === "string")
191
+ sendUserMessages.push(m.content);
192
+ },
190
193
  sendUserMessage: (m) => {
191
194
  sendUserMessages.push(m);
192
195
  },
@@ -1,5 +1,5 @@
1
1
  import { piCompactWouldNoop } from "../../mega-pipeline.js";
2
- import { safeSendUserMessage } from "../send-safe.js";
2
+ import { safeSendInvisibleMessage } from "../send-safe.js";
3
3
  /** Handle the `agent_end` pi event. Non-fatal end-to-end. */
4
4
  export async function handleAgentEnd(ctx, pi, runtime, config) {
5
5
  runtime.activeAgents = Math.max(0, runtime.activeAgents - 1);
@@ -200,7 +200,7 @@ export async function handleAgentEnd(ctx, pi, runtime, config) {
200
200
  const nudgeMsg = lengthStop && !didDurableTrim
201
201
  ? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
202
202
  : "[mega-compact] continue from the compacted context above.";
203
- await safeSendUserMessage(pi, nudgeMsg);
203
+ await safeSendInvisibleMessage(pi, nudgeMsg);
204
204
  }
205
205
  }
206
206
  catch {
@@ -0,0 +1,131 @@
1
+ import { errorRetryBackoffMs } from "../../error-classifier.js";
2
+ import { safeSendInvisibleMessage } from "../../send-safe.js";
3
+ import { maybeSendProviderOutageAdvisory } from "../../outage-advisor.js";
4
+ /** Handle the transient or permanent error-retry path. Non-fatal. */
5
+ export async function transientRetry(event, _ctx, pi, runtime, config, tc) {
6
+ const { effectiveCategory, detail, errSig } = tc;
7
+ // S38.7: hard-stop switch — bypass ALL retry logic when set.
8
+ if (config.errorRetryHardStop) {
9
+ runtime.rt.errorRetryCount = 0;
10
+ runtime.dashboard.event("error_retry_disabled", {
11
+ category: effectiveCategory,
12
+ turnIndex: event.turnIndex,
13
+ reason: "hard-stop",
14
+ });
15
+ return; // early exit — no retry
16
+ }
17
+ // S38.6: circuit-breaker — stop retrying after too many consecutive errors.
18
+ runtime.rt.consecutiveErrors++;
19
+ // R10: send calm "provider outage" advisory once per episode.
20
+ await maybeSendProviderOutageAdvisory(effectiveCategory, runtime, pi, config, { signal: detail.signal, rawText: (errSig || '').slice(0, 500) });
21
+ if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
22
+ runtime.dashboard.event("error_retry_circuit_open", {
23
+ consecutive: runtime.rt.consecutiveErrors,
24
+ max: config.maxConsecutiveErrors,
25
+ turnIndex: event.turnIndex,
26
+ });
27
+ runtime.logger.warn("error-retry-circuit-open", {
28
+ sessionId: runtime.rt.sessionId,
29
+ consecutive: runtime.rt.consecutiveErrors,
30
+ max: config.maxConsecutiveErrors,
31
+ });
32
+ return; // early exit — circuit breaker tripped
33
+ }
34
+ const max = effectiveCategory === "transient"
35
+ ? config.autoRetryTransientMax
36
+ : config.autoRetryPermanentMax;
37
+ // max === 0 disables the category entirely (revert to S28-only).
38
+ if (max <= 0) {
39
+ runtime.rt.errorRetryCount = 0;
40
+ return;
41
+ }
42
+ runtime.rt.errorRetryCount++;
43
+ if (runtime.rt.errorRetryCount > max) {
44
+ // Exhausted — surface the error, reset for the next burst.
45
+ runtime.dashboard.event("error_retry_exhausted", {
46
+ category: effectiveCategory,
47
+ count: runtime.rt.errorRetryCount,
48
+ max,
49
+ turnIndex: event.turnIndex,
50
+ });
51
+ runtime.logger.info("error-retry-exhausted", {
52
+ sessionId: runtime.rt.sessionId,
53
+ category: effectiveCategory,
54
+ count: runtime.rt.errorRetryCount,
55
+ max,
56
+ });
57
+ runtime.rt.errorRetryCount = 0;
58
+ return;
59
+ }
60
+ // R2: session-global cap — total S38 nudges per session across ALL bursts.
61
+ // Independent of the per-burst max and the circuit breaker. Hitting it is
62
+ // terminal for the session: log + dashboard event, stop nudging. `0` disables.
63
+ if (config.errorRetrySessionMax > 0 &&
64
+ runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax) {
65
+ runtime.dashboard.event("error_retry_session_exhausted", {
66
+ count: runtime.rt.errorRetrySessionCount,
67
+ max: config.errorRetrySessionMax,
68
+ category: effectiveCategory,
69
+ turnIndex: event.turnIndex,
70
+ });
71
+ runtime.logger.warn("error-retry-session-exhausted", {
72
+ sessionId: runtime.rt.sessionId,
73
+ count: runtime.rt.errorRetrySessionCount,
74
+ max: config.errorRetrySessionMax,
75
+ category: effectiveCategory,
76
+ });
77
+ runtime.rt.errorRetryCount = 0;
78
+ return; // terminal for the session — no nudge
79
+ }
80
+ // R1: in-flight nudge dedup — a nudge queued via deliverAs:'followUp' must
81
+ // not be re-sent until consumed by an actual new agent turn (turn_start
82
+ // resets retryNudgePending). Without this, a fast-erroring provider + a
83
+ // per-turn nudge → N nudges queue up and pi dispatches N retry turns, each
84
+ // re-submitting the same failing prompt (the 2026-07-28 incident).
85
+ if (runtime.rt.retryNudgePending) {
86
+ runtime.dashboard.event("error_retry_dedup_skip", {
87
+ category: effectiveCategory,
88
+ count: runtime.rt.errorRetryCount,
89
+ max,
90
+ turnIndex: event.turnIndex,
91
+ });
92
+ return; // pending nudge not yet consumed — skip
93
+ }
94
+ // R1: gating backoff — errorRetryUntil is GATING. A nudge cannot fire before
95
+ // the previous backoff elapses. Paces retries (5s/10s/20s/30s default).
96
+ const now = Date.now();
97
+ if (now < runtime.rt.errorRetryUntil) {
98
+ runtime.dashboard.event("error_retry_backoff_skip", {
99
+ category: effectiveCategory,
100
+ count: runtime.rt.errorRetryCount,
101
+ max,
102
+ turnIndex: event.turnIndex,
103
+ });
104
+ return; // backoff not elapsed — skip
105
+ }
106
+ // Fire the retry nudge. Set pending (R1 dedup) + backoff (R1 pacing) +
107
+ // session count (R2 cap) BEFORE the await so a re-entrant turn_end during
108
+ // the send can't double-fire.
109
+ runtime.rt.retryNudgePending = true;
110
+ runtime.rt.lastErrorRetryAt = now;
111
+ runtime.rt.errorRetryUntil =
112
+ now +
113
+ errorRetryBackoffMs(runtime.rt.errorRetryCount, config.errorRetryBackoffMs);
114
+ runtime.rt.errorRetrySessionCount++;
115
+ runtime.dashboard.event("error_retry", {
116
+ category: effectiveCategory,
117
+ count: runtime.rt.errorRetryCount,
118
+ max,
119
+ turnIndex: event.turnIndex,
120
+ });
121
+ runtime.logger.info("error-retry", {
122
+ sessionId: runtime.rt.sessionId,
123
+ category: effectiveCategory,
124
+ count: runtime.rt.errorRetryCount,
125
+ max,
126
+ });
127
+ // PREVENT-PI-003: user-role only (queued + catch-guarded).
128
+ // Invisible: display:false so the retry trigger fires without
129
+ // rendering "Follow-up:" spam in the conversation UI.
130
+ await safeSendInvisibleMessage(pi, "[mega-compact] the last turn ended with an error; please retry.");
131
+ }
@@ -1,7 +1,7 @@
1
1
  import { piCompactWouldNoop } from "../../../mega-pipeline.js";
2
- import { classifyError, classifyErrorDetailed, errorRetryBackoffMs, extractErrorSignature, isKnownRetryableTransient, } from "../../error-classifier.js";
3
- import { safeSendUserMessage } from "../../send-safe.js";
4
- import { maybeSendProviderOutageAdvisory } from "../../outage-advisor.js";
2
+ import { classifyError, classifyErrorDetailed, extractErrorSignature, isKnownRetryableTransient, } from "../../error-classifier.js";
3
+ import { safeSendInvisibleMessage } from "../../send-safe.js";
4
+ import { transientRetry } from "./errorRetry-transient.js";
5
5
  /** S38: broader error-retry safety net. Non-fatal end-to-end. */
6
6
  export async function errorRetry(event, ctx, pi, runtime, config) {
7
7
  // S38: broader error-retry safety net. S28 only catches stopReason==='length';
@@ -209,7 +209,10 @@ export async function errorRetry(event, ctx, pi, runtime, config) {
209
209
  // Default ON (advisoryChannel=true): dashboard-only, no user message.
210
210
  if (!config.advisoryChannel) {
211
211
  // PREVENT-PI-003: user-role sendUserMessage only.
212
- await safeSendUserMessage(pi, "[mega-compact] this session's context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.");
212
+ // Invisible: display:false the dashboard event + log already
213
+ // capture the poisoned-context signal; the user does not need a
214
+ // visible "Follow-up:" message.
215
+ await safeSendInvisibleMessage(pi, "[mega-compact] this session's context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.");
213
216
  }
214
217
  // (c) one guarded compact per error signature (attempt to remove
215
218
  // the poisoned region). Race-guarded + deferred, mirroring the
@@ -245,139 +248,12 @@ export async function errorRetry(event, ctx, pi, runtime, config) {
245
248
  }
246
249
  else {
247
250
  // (5) transient or permanent — retry with exponential backoff.
248
- // S38.7: hard-stop switch — bypass ALL retry logic when set.
249
- if (config.errorRetryHardStop) {
250
- runtime.rt.errorRetryCount = 0;
251
- runtime.dashboard.event("error_retry_disabled", {
252
- category: effectiveCategory,
253
- turnIndex: event.turnIndex,
254
- reason: "hard-stop",
255
- });
256
- return; // early exit — no retry
257
- }
258
- // S38.6: circuit-breaker — stop retrying after too many consecutive errors.
259
- runtime.rt.consecutiveErrors++;
260
- // R10: send calm "provider outage" advisory once per episode.
261
- await maybeSendProviderOutageAdvisory(effectiveCategory, runtime, pi, config, { signal: detail.signal, rawText: (errSig || '').slice(0, 500) });
262
- if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
263
- runtime.dashboard.event("error_retry_circuit_open", {
264
- consecutive: runtime.rt.consecutiveErrors,
265
- max: config.maxConsecutiveErrors,
266
- turnIndex: event.turnIndex,
267
- });
268
- runtime.logger.warn("error-retry-circuit-open", {
269
- sessionId: runtime.rt.sessionId,
270
- consecutive: runtime.rt.consecutiveErrors,
271
- max: config.maxConsecutiveErrors,
272
- });
273
- return; // early exit — circuit breaker tripped
274
- }
275
- const max = effectiveCategory === "transient"
276
- ? config.autoRetryTransientMax
277
- : config.autoRetryPermanentMax;
278
- // max === 0 disables the category entirely (revert to S28-only).
279
- if (max <= 0) {
280
- runtime.rt.errorRetryCount = 0;
281
- }
282
- else {
283
- runtime.rt.errorRetryCount++;
284
- if (runtime.rt.errorRetryCount > max) {
285
- // Exhausted — surface the error, reset for the next burst.
286
- runtime.dashboard.event("error_retry_exhausted", {
287
- category: effectiveCategory,
288
- count: runtime.rt.errorRetryCount,
289
- max,
290
- turnIndex: event.turnIndex,
291
- });
292
- runtime.logger.info("error-retry-exhausted", {
293
- sessionId: runtime.rt.sessionId,
294
- category: effectiveCategory,
295
- count: runtime.rt.errorRetryCount,
296
- max,
297
- });
298
- runtime.rt.errorRetryCount = 0;
299
- }
300
- else {
301
- // R2: session-global cap — total S38 nudges per session across
302
- // ALL bursts. Independent of the per-burst max and the circuit
303
- // breaker. Hitting it is terminal for the session: log +
304
- // dashboard event, stop nudging. `0` disables (reverts to
305
- // per-burst + circuit-breaker only).
306
- if (config.errorRetrySessionMax > 0 &&
307
- runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax) {
308
- runtime.dashboard.event("error_retry_session_exhausted", {
309
- count: runtime.rt.errorRetrySessionCount,
310
- max: config.errorRetrySessionMax,
311
- category: effectiveCategory,
312
- turnIndex: event.turnIndex,
313
- });
314
- runtime.logger.warn("error-retry-session-exhausted", {
315
- sessionId: runtime.rt.sessionId,
316
- count: runtime.rt.errorRetrySessionCount,
317
- max: config.errorRetrySessionMax,
318
- category: effectiveCategory,
319
- });
320
- runtime.rt.errorRetryCount = 0;
321
- return; // terminal for the session — no nudge
322
- }
323
- // R1: in-flight nudge dedup — a nudge queued via
324
- // deliverAs:'followUp' must not be re-sent until it has been
325
- // consumed by an actual new agent turn (turn_start resets
326
- // retryNudgePending). Without this, a fast-erroring provider +
327
- // a per-turn nudge → N nudges queue up and pi dispatches N
328
- // retry turns, each re-submitting the same failing prompt
329
- // (the 2026-07-28 incident). errorRetryCount still advances
330
- // (this IS an error turn), so the per-burst max + circuit
331
- // breaker still bound the burst.
332
- if (runtime.rt.retryNudgePending) {
333
- runtime.dashboard.event("error_retry_dedup_skip", {
334
- category: effectiveCategory,
335
- count: runtime.rt.errorRetryCount,
336
- max,
337
- turnIndex: event.turnIndex,
338
- });
339
- return; // pending nudge not yet consumed — skip
340
- }
341
- // R1: gating backoff — errorRetryUntil is now GATING
342
- // (previously documented as non-gating). A nudge cannot fire
343
- // before the previous backoff elapses. This paces retries
344
- // (5s/10s/20s/30s by default) so a fast-erroring provider
345
- // doesn't slam N nudges in <1s.
346
- const now = Date.now();
347
- if (now < runtime.rt.errorRetryUntil) {
348
- runtime.dashboard.event("error_retry_backoff_skip", {
349
- category: effectiveCategory,
350
- count: runtime.rt.errorRetryCount,
351
- max,
352
- turnIndex: event.turnIndex,
353
- });
354
- return; // backoff not elapsed — skip
355
- }
356
- // Fire the retry nudge. Set pending (R1 dedup) + backoff
357
- // (R1 pacing) + session count (R2 cap) BEFORE the await so a
358
- // re-entrant turn_end during the send can't double-fire.
359
- runtime.rt.retryNudgePending = true;
360
- runtime.rt.lastErrorRetryAt = now;
361
- runtime.rt.errorRetryUntil =
362
- now +
363
- errorRetryBackoffMs(runtime.rt.errorRetryCount, config.errorRetryBackoffMs);
364
- runtime.rt.errorRetrySessionCount++;
365
- runtime.dashboard.event("error_retry", {
366
- category: effectiveCategory,
367
- count: runtime.rt.errorRetryCount,
368
- max,
369
- turnIndex: event.turnIndex,
370
- });
371
- runtime.logger.info("error-retry", {
372
- sessionId: runtime.rt.sessionId,
373
- category: effectiveCategory,
374
- count: runtime.rt.errorRetryCount,
375
- max,
376
- });
377
- // PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
378
- await safeSendUserMessage(pi, "[mega-compact] the last turn ended with an error; please retry.");
379
- }
380
- }
251
+ // Delegated to errorRetry-transient.ts (delegate-shell split).
252
+ await transientRetry(event, ctx, pi, runtime, config, {
253
+ effectiveCategory,
254
+ detail,
255
+ errSig,
256
+ });
381
257
  }
382
258
  }
383
259
  }
@@ -3,7 +3,7 @@ import { estimateBlockTokens } from "../../src/tokens.js";
3
3
  import { recordScore, getDedupStats } from "../../src/store/sqlite.js";
4
4
  import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achievements.js";
5
5
  import { resolveRepoRoot } from "../mega-config.js";
6
- import { safeSendUserMessage } from "./send-safe.js";
6
+ import { safeSendInvisibleMessage } from "./send-safe.js";
7
7
  /**
8
8
  * Build a minimal fallback compaction so pi never runs its throwing compact().
9
9
  *
@@ -36,7 +36,7 @@ function fallbackCompaction(event) {
36
36
  * Debounced resume-nudge: restart the agent loop after a compaction (which
37
37
  * may have stopped it). Idempotent — one nudge per 30s, never blocks.
38
38
  *
39
- * Uses safeSendUserMessage ({ deliverAs: 'followUp' } + catch-guard) so that a
39
+ * Uses safeSendInvisibleMessage ({ deliverAs: 'followUp' } + catch-guard) so that a
40
40
  * nudge fired during session_before_compact (which is mid-prompt-submission,
41
41
  * so the agent can be busy) QUEUES instead of throwing
42
42
  * "Agent is already processing. Specify streamingBehavior (steer or followUp)".
@@ -47,7 +47,7 @@ async function nudgeResume(pi, runtime) {
47
47
  if (now >= runtime.resumeNudgeUntil) {
48
48
  runtime.resumeNudgeUntil = now + 30_000;
49
49
  runtime.rt.extensionInitiatedTurn = true; // R13: suppress self-classification
50
- await safeSendUserMessage(pi, "[mega-compact] continue from the compacted context above.");
50
+ await safeSendInvisibleMessage(pi, "[mega-compact] continue from the compacted context above.");
51
51
  }
52
52
  }
53
53
  catch {
@@ -9,10 +9,10 @@
9
9
  * Distinct from the poisoned-context /clear advise: the outage advisory is
10
10
  * for transient errors where the user's context is fine.
11
11
  *
12
- * PREVENT-PI-003: sends via safeSendUserMessage (user-role only).
12
+ * PREVENT-PI-003: sends via safeSendInvisibleMessage (user-role only).
13
13
  * PREVENT-PI-004: local ctx call, no network.
14
14
  */
15
- import { safeSendUserMessage } from "./send-safe.js";
15
+ import { safeSendInvisibleMessage } from "./send-safe.js";
16
16
  /**
17
17
  * Check the provider-outage advisory condition and fire once per episode.
18
18
  *
@@ -21,7 +21,7 @@ import { safeSendUserMessage } from "./send-safe.js";
21
21
  *
22
22
  * @param effectiveCategory — must be "transient" for the advisory to fire.
23
23
  * @param runtime — the live MegaRuntime (mutated on advisory fire).
24
- * @param pi — pi ExtensionAPI for safeSendUserMessage.
24
+ * @param pi — pi ExtensionAPI for safeSendInvisibleMessage.
25
25
  * @param config — providerOutageAdviseThreshold config.
26
26
  * @param detail — R11: optional signal + rawText for forensics.
27
27
  */
@@ -53,7 +53,7 @@ export async function maybeSendProviderOutageAdvisory(effectiveCategory, runtime
53
53
  // injecting into the conversation scares users and triggers false-positive
54
54
  // re-classification of the resulting turn (2026-07-31 incident).
55
55
  if (!config.advisoryChannel) {
56
- // Legacy path (flag OFF): user-role message injection.
57
- await safeSendUserMessage(pi, `[mega-compact] the provider is having issues (${runtime.rt.consecutiveErrors} consecutive failures — timeouts/5xx/rate-limits). Retries are bounded and continue automatically; your context is fine — do NOT clear or reset it. Work resumes as soon as the provider recovers.`);
56
+ // Legacy path (flag OFF): invisible user-role message (display:false).
57
+ await safeSendInvisibleMessage(pi, `[mega-compact] the provider is having issues (${runtime.rt.consecutiveErrors} consecutive failures — timeouts/5xx/rate-limits). Retries are bounded and continue automatically; your context is fine — do NOT clear or reset it. Work resumes as soon as the provider recovers.`);
58
58
  }
59
59
  }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * safeSendUserMessage — await + catch-guard + queue-safe wrapper for
3
3
  * pi.sendUserMessage. Never throws; never produces an unhandled rejection.
4
+ * The message IS visible in the conversation UI ("Follow-up:").
4
5
  */
5
6
  export async function safeSendUserMessage(pi, content) {
6
7
  try {
@@ -10,3 +11,18 @@ export async function safeSendUserMessage(pi, content) {
10
11
  /* non-fatal: a failed/queued nudge never blocks the agent loop */
11
12
  }
12
13
  }
14
+ /**
15
+ * safeSendInvisibleMessage — like safeSendUserMessage but uses pi.sendMessage
16
+ * with `display: false` so the retry trigger is delivered to the agent WITHOUT
17
+ * rendering in the conversation UI. Fixes the "Follow-up: [mega-compact] ..."
18
+ * pile-up when the API retries repeatedly — the retry mechanism still fires,
19
+ * the user just doesn't see the queued messages.
20
+ */
21
+ export async function safeSendInvisibleMessage(pi, content) {
22
+ try {
23
+ await pi.sendMessage({ customType: "mega-compact-retry", content, display: false }, { deliverAs: "followUp" });
24
+ }
25
+ catch {
26
+ /* non-fatal: a failed/queued nudge never blocks the agent loop */
27
+ }
28
+ }
@@ -122,7 +122,7 @@ export function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } =
122
122
  registerCommand: () => {}, registerTool: () => {}, registerShortcut: () => {},
123
123
  registerFlag: () => {}, getFlag: () => undefined,
124
124
  registerMessageRenderer: () => {}, registerEntryRenderer: () => {},
125
- sendMessage: () => {}, sendUserMessage: (m: string) => { sendUserMessages.push(m); },
125
+ sendMessage: (m: { content?: string }) => { if (typeof m?.content === "string") sendUserMessages.push(m.content); }, sendUserMessage: (m: string) => { sendUserMessages.push(m); },
126
126
  appendEntry: (t: string, d: any) => appended.push({ t, d }),
127
127
  setSessionName: () => {}, getSessionName: () => undefined, setLabel: () => {},
128
128
  exec: async () => ({ stdout: "", stderr: "", code: 0 }),
@@ -197,7 +197,9 @@ export function harness(opts: { keepTier?: boolean; keepThreshold?: boolean } =
197
197
  getFlag: () => undefined,
198
198
  registerMessageRenderer: () => {},
199
199
  registerEntryRenderer: () => {},
200
- sendMessage: (_m: any) => {},
200
+ sendMessage: (m: { content?: string }) => {
201
+ if (typeof m?.content === "string") sendUserMessages.push(m.content);
202
+ },
201
203
  sendUserMessage: (m: string) => {
202
204
  sendUserMessages.push(m);
203
205
  },
@@ -10,7 +10,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
10
10
  import type { MegaRuntime } from "../../mega-runtime.js";
11
11
  import { piCompactWouldNoop } from "../../mega-pipeline.js";
12
12
  import type { MegaConfig } from "../../mega-config.js";
13
- import { safeSendUserMessage } from "../send-safe.js";
13
+ import { safeSendInvisibleMessage } from "../send-safe.js";
14
14
 
15
15
  /** Handle the `agent_end` pi event. Non-fatal end-to-end. */
16
16
  export async function handleAgentEnd(
@@ -228,7 +228,7 @@ export async function handleAgentEnd(
228
228
  lengthStop && !didDurableTrim
229
229
  ? "[mega-compact] the last response hit the output-token cap; continue from where it stopped."
230
230
  : "[mega-compact] continue from the compacted context above.";
231
- await safeSendUserMessage(pi, nudgeMsg);
231
+ await safeSendInvisibleMessage(pi, nudgeMsg);
232
232
  }
233
233
  } catch {
234
234
  /* non-fatal: a failed nudge never blocks */
@@ -0,0 +1,170 @@
1
+ /**
2
+ * turnEndHandler/errorRetry-transient.ts — transient/permanent retry branch.
3
+ *
4
+ * Extracted from errorRetry.ts (delegate-shell split) to keep every source
5
+ * file under the extensions soft limit. Contains the S38.6 circuit breaker,
6
+ * R10 provider-outage advisory, per-burst max, R2 session cap, R1 in-flight
7
+ * dedup + gating backoff, and the actual retry nudge send.
8
+ */
9
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
+ import type { MegaRuntime } from "../../../mega-runtime.js";
11
+ import type { MegaConfig } from "../../../mega-config.js";
12
+ import { errorRetryBackoffMs } from "../../error-classifier.js";
13
+ import { safeSendInvisibleMessage } from "../../send-safe.js";
14
+ import { maybeSendProviderOutageAdvisory } from "../../outage-advisor.js";
15
+ import type { TurnEndEvent } from "./event.js";
16
+
17
+ /** Context needed from the caller's scope for the transient/permanent branch. */
18
+ export interface TransientRetryContext {
19
+ readonly effectiveCategory: string;
20
+ readonly detail: { signal?: string };
21
+ readonly errSig: string | undefined;
22
+ }
23
+
24
+ /** Handle the transient or permanent error-retry path. Non-fatal. */
25
+ export async function transientRetry(
26
+ event: TurnEndEvent,
27
+ _ctx: ExtensionContext,
28
+ pi: ExtensionAPI,
29
+ runtime: MegaRuntime,
30
+ config: MegaConfig,
31
+ tc: TransientRetryContext,
32
+ ): Promise<void> {
33
+ const { effectiveCategory, detail, errSig } = tc;
34
+ // S38.7: hard-stop switch — bypass ALL retry logic when set.
35
+ if (config.errorRetryHardStop) {
36
+ runtime.rt.errorRetryCount = 0;
37
+ runtime.dashboard.event("error_retry_disabled", {
38
+ category: effectiveCategory,
39
+ turnIndex: event.turnIndex,
40
+ reason: "hard-stop",
41
+ });
42
+ return; // early exit — no retry
43
+ }
44
+ // S38.6: circuit-breaker — stop retrying after too many consecutive errors.
45
+ runtime.rt.consecutiveErrors++;
46
+ // R10: send calm "provider outage" advisory once per episode.
47
+ await maybeSendProviderOutageAdvisory(
48
+ effectiveCategory, runtime, pi, config,
49
+ { signal: detail.signal, rawText: (errSig || '').slice(0, 500) },
50
+ );
51
+ if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
52
+ runtime.dashboard.event("error_retry_circuit_open", {
53
+ consecutive: runtime.rt.consecutiveErrors,
54
+ max: config.maxConsecutiveErrors,
55
+ turnIndex: event.turnIndex,
56
+ });
57
+ runtime.logger.warn("error-retry-circuit-open", {
58
+ sessionId: runtime.rt.sessionId,
59
+ consecutive: runtime.rt.consecutiveErrors,
60
+ max: config.maxConsecutiveErrors,
61
+ });
62
+ return; // early exit — circuit breaker tripped
63
+ }
64
+ const max =
65
+ effectiveCategory === "transient"
66
+ ? config.autoRetryTransientMax
67
+ : config.autoRetryPermanentMax;
68
+ // max === 0 disables the category entirely (revert to S28-only).
69
+ if (max <= 0) {
70
+ runtime.rt.errorRetryCount = 0;
71
+ return;
72
+ }
73
+ runtime.rt.errorRetryCount++;
74
+ if (runtime.rt.errorRetryCount > max) {
75
+ // Exhausted — surface the error, reset for the next burst.
76
+ runtime.dashboard.event("error_retry_exhausted", {
77
+ category: effectiveCategory,
78
+ count: runtime.rt.errorRetryCount,
79
+ max,
80
+ turnIndex: event.turnIndex,
81
+ });
82
+ runtime.logger.info("error-retry-exhausted", {
83
+ sessionId: runtime.rt.sessionId,
84
+ category: effectiveCategory,
85
+ count: runtime.rt.errorRetryCount,
86
+ max,
87
+ });
88
+ runtime.rt.errorRetryCount = 0;
89
+ return;
90
+ }
91
+ // R2: session-global cap — total S38 nudges per session across ALL bursts.
92
+ // Independent of the per-burst max and the circuit breaker. Hitting it is
93
+ // terminal for the session: log + dashboard event, stop nudging. `0` disables.
94
+ if (
95
+ config.errorRetrySessionMax > 0 &&
96
+ runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax
97
+ ) {
98
+ runtime.dashboard.event("error_retry_session_exhausted", {
99
+ count: runtime.rt.errorRetrySessionCount,
100
+ max: config.errorRetrySessionMax,
101
+ category: effectiveCategory,
102
+ turnIndex: event.turnIndex,
103
+ });
104
+ runtime.logger.warn("error-retry-session-exhausted", {
105
+ sessionId: runtime.rt.sessionId,
106
+ count: runtime.rt.errorRetrySessionCount,
107
+ max: config.errorRetrySessionMax,
108
+ category: effectiveCategory,
109
+ });
110
+ runtime.rt.errorRetryCount = 0;
111
+ return; // terminal for the session — no nudge
112
+ }
113
+ // R1: in-flight nudge dedup — a nudge queued via deliverAs:'followUp' must
114
+ // not be re-sent until consumed by an actual new agent turn (turn_start
115
+ // resets retryNudgePending). Without this, a fast-erroring provider + a
116
+ // per-turn nudge → N nudges queue up and pi dispatches N retry turns, each
117
+ // re-submitting the same failing prompt (the 2026-07-28 incident).
118
+ if (runtime.rt.retryNudgePending) {
119
+ runtime.dashboard.event("error_retry_dedup_skip", {
120
+ category: effectiveCategory,
121
+ count: runtime.rt.errorRetryCount,
122
+ max,
123
+ turnIndex: event.turnIndex,
124
+ });
125
+ return; // pending nudge not yet consumed — skip
126
+ }
127
+ // R1: gating backoff — errorRetryUntil is GATING. A nudge cannot fire before
128
+ // the previous backoff elapses. Paces retries (5s/10s/20s/30s default).
129
+ const now = Date.now();
130
+ if (now < runtime.rt.errorRetryUntil) {
131
+ runtime.dashboard.event("error_retry_backoff_skip", {
132
+ category: effectiveCategory,
133
+ count: runtime.rt.errorRetryCount,
134
+ max,
135
+ turnIndex: event.turnIndex,
136
+ });
137
+ return; // backoff not elapsed — skip
138
+ }
139
+ // Fire the retry nudge. Set pending (R1 dedup) + backoff (R1 pacing) +
140
+ // session count (R2 cap) BEFORE the await so a re-entrant turn_end during
141
+ // the send can't double-fire.
142
+ runtime.rt.retryNudgePending = true;
143
+ runtime.rt.lastErrorRetryAt = now;
144
+ runtime.rt.errorRetryUntil =
145
+ now +
146
+ errorRetryBackoffMs(
147
+ runtime.rt.errorRetryCount,
148
+ config.errorRetryBackoffMs,
149
+ );
150
+ runtime.rt.errorRetrySessionCount++;
151
+ runtime.dashboard.event("error_retry", {
152
+ category: effectiveCategory,
153
+ count: runtime.rt.errorRetryCount,
154
+ max,
155
+ turnIndex: event.turnIndex,
156
+ });
157
+ runtime.logger.info("error-retry", {
158
+ sessionId: runtime.rt.sessionId,
159
+ category: effectiveCategory,
160
+ count: runtime.rt.errorRetryCount,
161
+ max,
162
+ });
163
+ // PREVENT-PI-003: user-role only (queued + catch-guarded).
164
+ // Invisible: display:false so the retry trigger fires without
165
+ // rendering "Follow-up:" spam in the conversation UI.
166
+ await safeSendInvisibleMessage(
167
+ pi,
168
+ "[mega-compact] the last turn ended with an error; please retry.",
169
+ );
170
+ }
@@ -17,12 +17,11 @@ import type { MegaConfig } from "../../../mega-config.js";
17
17
  import {
18
18
  classifyError,
19
19
  classifyErrorDetailed,
20
- errorRetryBackoffMs,
21
20
  extractErrorSignature,
22
21
  isKnownRetryableTransient,
23
22
  } from "../../error-classifier.js";
24
- import { safeSendUserMessage } from "../../send-safe.js";
25
- import { maybeSendProviderOutageAdvisory } from "../../outage-advisor.js";
23
+ import { safeSendInvisibleMessage } from "../../send-safe.js";
24
+ import { transientRetry } from "./errorRetry-transient.js";
26
25
  import type { TurnEndEvent } from "./event.js";
27
26
 
28
27
  /** S38: broader error-retry safety net. Non-fatal end-to-end. */
@@ -232,7 +231,10 @@ export async function errorRetry(
232
231
  // Default ON (advisoryChannel=true): dashboard-only, no user message.
233
232
  if (!config.advisoryChannel) {
234
233
  // PREVENT-PI-003: user-role sendUserMessage only.
235
- await safeSendUserMessage(
234
+ // Invisible: display:false — the dashboard event + log already
235
+ // capture the poisoned-context signal; the user does not need a
236
+ // visible "Follow-up:" message.
237
+ await safeSendInvisibleMessage(
236
238
  pi,
237
239
  "[mega-compact] this session's context may be poisoned (the provider is rejecting every request). Run /clear or /new to start a fresh context.",
238
240
  );
@@ -270,149 +272,12 @@ export async function errorRetry(
270
272
  }
271
273
  } else {
272
274
  // (5) transient or permanent — retry with exponential backoff.
273
- // S38.7: hard-stop switch — bypass ALL retry logic when set.
274
- if (config.errorRetryHardStop) {
275
- runtime.rt.errorRetryCount = 0;
276
- runtime.dashboard.event("error_retry_disabled", {
277
- category: effectiveCategory,
278
- turnIndex: event.turnIndex,
279
- reason: "hard-stop",
280
- });
281
- return; // early exit — no retry
282
- }
283
- // S38.6: circuit-breaker — stop retrying after too many consecutive errors.
284
- runtime.rt.consecutiveErrors++;
285
- // R10: send calm "provider outage" advisory once per episode.
286
- await maybeSendProviderOutageAdvisory(
287
- effectiveCategory, runtime, pi, config,
288
- { signal: detail.signal, rawText: (errSig || '').slice(0, 500) },
289
- );
290
- if (runtime.rt.consecutiveErrors > config.maxConsecutiveErrors) {
291
- runtime.dashboard.event("error_retry_circuit_open", {
292
- consecutive: runtime.rt.consecutiveErrors,
293
- max: config.maxConsecutiveErrors,
294
- turnIndex: event.turnIndex,
295
- });
296
- runtime.logger.warn("error-retry-circuit-open", {
297
- sessionId: runtime.rt.sessionId,
298
- consecutive: runtime.rt.consecutiveErrors,
299
- max: config.maxConsecutiveErrors,
300
- });
301
- return; // early exit — circuit breaker tripped
302
- }
303
- const max =
304
- effectiveCategory === "transient"
305
- ? config.autoRetryTransientMax
306
- : config.autoRetryPermanentMax;
307
- // max === 0 disables the category entirely (revert to S28-only).
308
- if (max <= 0) {
309
- runtime.rt.errorRetryCount = 0;
310
- } else {
311
- runtime.rt.errorRetryCount++;
312
- if (runtime.rt.errorRetryCount > max) {
313
- // Exhausted — surface the error, reset for the next burst.
314
- runtime.dashboard.event("error_retry_exhausted", {
315
- category: effectiveCategory,
316
- count: runtime.rt.errorRetryCount,
317
- max,
318
- turnIndex: event.turnIndex,
319
- });
320
- runtime.logger.info("error-retry-exhausted", {
321
- sessionId: runtime.rt.sessionId,
322
- category: effectiveCategory,
323
- count: runtime.rt.errorRetryCount,
324
- max,
325
- });
326
- runtime.rt.errorRetryCount = 0;
327
- } else {
328
- // R2: session-global cap — total S38 nudges per session across
329
- // ALL bursts. Independent of the per-burst max and the circuit
330
- // breaker. Hitting it is terminal for the session: log +
331
- // dashboard event, stop nudging. `0` disables (reverts to
332
- // per-burst + circuit-breaker only).
333
- if (
334
- config.errorRetrySessionMax > 0 &&
335
- runtime.rt.errorRetrySessionCount >= config.errorRetrySessionMax
336
- ) {
337
- runtime.dashboard.event("error_retry_session_exhausted", {
338
- count: runtime.rt.errorRetrySessionCount,
339
- max: config.errorRetrySessionMax,
340
- category: effectiveCategory,
341
- turnIndex: event.turnIndex,
342
- });
343
- runtime.logger.warn("error-retry-session-exhausted", {
344
- sessionId: runtime.rt.sessionId,
345
- count: runtime.rt.errorRetrySessionCount,
346
- max: config.errorRetrySessionMax,
347
- category: effectiveCategory,
348
- });
349
- runtime.rt.errorRetryCount = 0;
350
- return; // terminal for the session — no nudge
351
- }
352
- // R1: in-flight nudge dedup — a nudge queued via
353
- // deliverAs:'followUp' must not be re-sent until it has been
354
- // consumed by an actual new agent turn (turn_start resets
355
- // retryNudgePending). Without this, a fast-erroring provider +
356
- // a per-turn nudge → N nudges queue up and pi dispatches N
357
- // retry turns, each re-submitting the same failing prompt
358
- // (the 2026-07-28 incident). errorRetryCount still advances
359
- // (this IS an error turn), so the per-burst max + circuit
360
- // breaker still bound the burst.
361
- if (runtime.rt.retryNudgePending) {
362
- runtime.dashboard.event("error_retry_dedup_skip", {
363
- category: effectiveCategory,
364
- count: runtime.rt.errorRetryCount,
365
- max,
366
- turnIndex: event.turnIndex,
367
- });
368
- return; // pending nudge not yet consumed — skip
369
- }
370
- // R1: gating backoff — errorRetryUntil is now GATING
371
- // (previously documented as non-gating). A nudge cannot fire
372
- // before the previous backoff elapses. This paces retries
373
- // (5s/10s/20s/30s by default) so a fast-erroring provider
374
- // doesn't slam N nudges in <1s.
375
- const now = Date.now();
376
- if (now < runtime.rt.errorRetryUntil) {
377
- runtime.dashboard.event("error_retry_backoff_skip", {
378
- category: effectiveCategory,
379
- count: runtime.rt.errorRetryCount,
380
- max,
381
- turnIndex: event.turnIndex,
382
- });
383
- return; // backoff not elapsed — skip
384
- }
385
- // Fire the retry nudge. Set pending (R1 dedup) + backoff
386
- // (R1 pacing) + session count (R2 cap) BEFORE the await so a
387
- // re-entrant turn_end during the send can't double-fire.
388
- runtime.rt.retryNudgePending = true;
389
- runtime.rt.lastErrorRetryAt = now;
390
- runtime.rt.errorRetryUntil =
391
- now +
392
- errorRetryBackoffMs(
393
- runtime.rt.errorRetryCount,
394
- config.errorRetryBackoffMs,
395
- );
396
- runtime.rt.errorRetrySessionCount++;
397
- runtime.dashboard.event("error_retry", {
398
- category: effectiveCategory,
399
- count: runtime.rt.errorRetryCount,
400
- max,
401
- turnIndex: event.turnIndex,
402
- });
403
- runtime.logger.info("error-retry", {
404
- sessionId: runtime.rt.sessionId,
405
- category: effectiveCategory,
406
- count: runtime.rt.errorRetryCount,
407
- max,
408
- });
409
- // PREVENT-PI-003: user-role sendUserMessage only (queued + catch-guarded).
410
- await safeSendUserMessage(
411
- pi,
412
- "[mega-compact] the last turn ended with an error; please retry.",
413
- );
414
- }
415
- }
275
+ // Delegated to errorRetry-transient.ts (delegate-shell split).
276
+ await transientRetry(event, ctx, pi, runtime, config, {
277
+ effectiveCategory,
278
+ detail,
279
+ errSig,
280
+ });
416
281
  }
417
282
  }
418
283
  } catch {
@@ -22,7 +22,7 @@ import type { MegaConfig } from "../mega-config.js";
22
22
  import { recordScore, getDedupStats } from "../../src/store/sqlite.js";
23
23
  import { evaluateAndUnlockAchievements } from "../../src/store/sqlite/game-achievements.js";
24
24
  import { resolveRepoRoot } from "../mega-config.js";
25
- import { safeSendUserMessage } from "./send-safe.js";
25
+ import { safeSendInvisibleMessage } from "./send-safe.js";
26
26
 
27
27
  /**
28
28
  * Build a minimal fallback compaction so pi never runs its throwing compact().
@@ -59,7 +59,7 @@ function fallbackCompaction(
59
59
  * Debounced resume-nudge: restart the agent loop after a compaction (which
60
60
  * may have stopped it). Idempotent — one nudge per 30s, never blocks.
61
61
  *
62
- * Uses safeSendUserMessage ({ deliverAs: 'followUp' } + catch-guard) so that a
62
+ * Uses safeSendInvisibleMessage ({ deliverAs: 'followUp' } + catch-guard) so that a
63
63
  * nudge fired during session_before_compact (which is mid-prompt-submission,
64
64
  * so the agent can be busy) QUEUES instead of throwing
65
65
  * "Agent is already processing. Specify streamingBehavior (steer or followUp)".
@@ -70,7 +70,7 @@ async function nudgeResume(pi: ExtensionAPI, runtime: MegaRuntime): Promise<void
70
70
  if (now >= runtime.resumeNudgeUntil) {
71
71
  runtime.resumeNudgeUntil = now + 30_000;
72
72
  runtime.rt.extensionInitiatedTurn = true; // R13: suppress self-classification
73
- await safeSendUserMessage(
73
+ await safeSendInvisibleMessage(
74
74
  pi,
75
75
  "[mega-compact] continue from the compacted context above.",
76
76
  );
@@ -9,12 +9,12 @@
9
9
  * Distinct from the poisoned-context /clear advise: the outage advisory is
10
10
  * for transient errors where the user's context is fine.
11
11
  *
12
- * PREVENT-PI-003: sends via safeSendUserMessage (user-role only).
12
+ * PREVENT-PI-003: sends via safeSendInvisibleMessage (user-role only).
13
13
  * PREVENT-PI-004: local ctx call, no network.
14
14
  */
15
15
 
16
16
  import type { MegaRuntime } from "../mega-runtime.js";
17
- import { safeSendUserMessage } from "./send-safe.js";
17
+ import { safeSendInvisibleMessage } from "./send-safe.js";
18
18
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
19
 
20
20
  /** R11: optional diagnostic detail passed from the classifier. */
@@ -31,7 +31,7 @@ export interface OutageDetail {
31
31
  *
32
32
  * @param effectiveCategory — must be "transient" for the advisory to fire.
33
33
  * @param runtime — the live MegaRuntime (mutated on advisory fire).
34
- * @param pi — pi ExtensionAPI for safeSendUserMessage.
34
+ * @param pi — pi ExtensionAPI for safeSendInvisibleMessage.
35
35
  * @param config — providerOutageAdviseThreshold config.
36
36
  * @param detail — R11: optional signal + rawText for forensics.
37
37
  */
@@ -70,8 +70,8 @@ export async function maybeSendProviderOutageAdvisory(
70
70
  // injecting into the conversation scares users and triggers false-positive
71
71
  // re-classification of the resulting turn (2026-07-31 incident).
72
72
  if (!config.advisoryChannel) {
73
- // Legacy path (flag OFF): user-role message injection.
74
- await safeSendUserMessage(
73
+ // Legacy path (flag OFF): invisible user-role message (display:false).
74
+ await safeSendInvisibleMessage(
75
75
  pi,
76
76
  `[mega-compact] the provider is having issues (${runtime.rt.consecutiveErrors} consecutive failures — timeouts/5xx/rate-limits). Retries are bounded and continue automatically; your context is fine — do NOT clear or reset it. Work resumes as soon as the provider recovers.`,
77
77
  );
@@ -1,16 +1,20 @@
1
1
  /**
2
- * send-safe.ts — queue-safe wrapper for pi.sendUserMessage.
2
+ * send-safe.ts — queue-safe wrappers for pi.sendUserMessage / pi.sendMessage.
3
3
  *
4
4
  * All extension-initiated user-role nudges (resume after compaction, length-stop
5
- * continue, error-retry) MUST go through this wrapper. It:
6
- * 1. Passes { deliverAs: 'followUp' } so that when the agent is busy (e.g. a
5
+ * continue, error-retry) MUST go through these wrappers. They:
6
+ * 1. Pass { deliverAs: 'followUp' } so that when the agent is busy (e.g. a
7
7
  * resume nudge fired during session_before_compact, which is mid-prompt
8
8
  * submission) pi QUEUES the message instead of throwing
9
9
  * "Agent is already processing. Specify streamingBehavior (steer or
10
10
  * followUp) to queue the message" (pi agent-session.js:830).
11
- * 2. Awaits the call and catch-guards it so a failed/queued nudge never throws
11
+ * 2. Await the call and catch-guard it so a failed/queued nudge never throws
12
12
  * or produces an unhandled rejection — it must never block the agent loop.
13
13
  *
14
+ * safeSendUserMessage renders the text in the conversation UI (visible).
15
+ * safeSendInvisibleMessage delivers via pi.sendMessage with display: false —
16
+ * the agent receives the retry trigger but the user never sees the text.
17
+ *
14
18
  * PREVENT-PI-003: user-role sendUserMessage only (no role:'system' injection).
15
19
  * PREVENT-PI-004: local pi ctx call, no network.
16
20
  */
@@ -19,6 +23,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
23
  /**
20
24
  * safeSendUserMessage — await + catch-guard + queue-safe wrapper for
21
25
  * pi.sendUserMessage. Never throws; never produces an unhandled rejection.
26
+ * The message IS visible in the conversation UI ("Follow-up:").
22
27
  */
23
28
  export async function safeSendUserMessage(
24
29
  pi: ExtensionAPI,
@@ -35,3 +40,29 @@ export async function safeSendUserMessage(
35
40
  /* non-fatal: a failed/queued nudge never blocks the agent loop */
36
41
  }
37
42
  }
43
+
44
+ /**
45
+ * safeSendInvisibleMessage — like safeSendUserMessage but uses pi.sendMessage
46
+ * with `display: false` so the retry trigger is delivered to the agent WITHOUT
47
+ * rendering in the conversation UI. Fixes the "Follow-up: [mega-compact] ..."
48
+ * pile-up when the API retries repeatedly — the retry mechanism still fires,
49
+ * the user just doesn't see the queued messages.
50
+ */
51
+ export async function safeSendInvisibleMessage(
52
+ pi: ExtensionAPI,
53
+ content: string,
54
+ ): Promise<void> {
55
+ try {
56
+ await (
57
+ pi.sendMessage as (
58
+ m: { customType: string; content: string; display: boolean },
59
+ o?: { deliverAs?: "steer" | "followUp" },
60
+ ) => Promise<void> | void
61
+ )(
62
+ { customType: "mega-compact-retry", content, display: false },
63
+ { deliverAs: "followUp" },
64
+ );
65
+ } catch {
66
+ /* non-fatal: a failed/queued nudge never blocks the agent loop */
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.20.65",
3
+ "version": "0.20.66",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-3-Clause",