@monotykamary/pi-vcc 0.7.4 → 0.8.0

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
@@ -236,12 +236,18 @@ pi -e https://github.com/monotykamary/pi-vcc@tom
236
236
  Once installed, pi-vcc registers a `session_before_compact` hook.
237
237
 
238
238
  - Run `/pi-vcc` to trigger pi-vcc compaction manually.
239
- - By default, pi-vcc handles all compaction paths (`/compact`, auto-threshold, `/pi-vcc`). Set `overrideDefaultCompaction: false` in the config to fall back to pi core's LLM-based compaction for `/compact` and auto-threshold.
239
+ - By default, pi-vcc handles all compaction paths (`/compact`, auto-threshold, `/pi-vcc`). Set `overrideDefaultCompaction: false` in the config to fall back to pi core's LLM-based compaction for `/compact` and auto-threshold. When pi-fabric's compaction engine is active, pi-vcc retains its threshold and recovery triggers but delegates non-`/pi-vcc` summary compilation to pi-fabric.
240
240
  - To search older active-lineage history after compaction, use `vcc_recall`.
241
241
  - To intentionally search across all lineages, pass `scope:"all"` to `vcc_recall` or run `/pi-vcc-recall <query> scope:all`.
242
242
  - To search and feed results to agent yourself, run `/pi-vcc-recall <query> [page:N]`.
243
243
  - Tip: type `/recall` and Pi will autocomplete to `/pi-vcc-recall`.
244
244
 
245
+ ### pi-fabric interop
246
+
247
+ When `PI_FABRIC_COMPACTION_ENGINE=fabric`, pi-vcc defers non-explicit summary compilation to pi-fabric while preserving its proactive-threshold and Codex recovery trigger behavior. pi-fabric marks an event it has already claimed with `_fabricCompaction === true`, which prevents pi-vcc from replacing that result. An explicit `/pi-vcc` uses the `__pi_vcc__` instruction sentinel and always runs pi-vcc's deterministic compiler.
248
+
249
+ The precedence is: explicit `/pi-vcc` sentinel > pi-fabric engine > pi-vcc default override.
250
+
245
251
  ### How compaction works
246
252
 
247
253
  Pi splits the conversation at the **last user message**. Everything after — the **kept tail** — stays intact and untouched. pi-vcc only summarizes the older portion before that cut point.
@@ -573,7 +579,7 @@ Config lives at `~/.pi/agent/pi-vcc-config.json` (auto-scaffolded on first load
573
579
  }
574
580
  ```
575
581
 
576
- - **`overrideDefaultCompaction`** *(default `true`)*: when `true` (default), pi-vcc handles all compaction paths (`/compact`, auto-threshold, `/pi-vcc`). Set `false` to let pi core handle `/compact` and auto-threshold compactions via its default LLM-based compaction.
582
+ - **`overrideDefaultCompaction`** *(default `true`)*: when `true` (default), pi-vcc handles all compaction paths (`/compact`, auto-threshold, `/pi-vcc`) unless the active pi-fabric engine owns summary compilation. Set `false` to let pi core handle `/compact` and auto-threshold compactions via its default LLM-based compaction. Explicit `/pi-vcc` always uses pi-vcc.
577
583
  - **`debug`** *(default `false`)*: when `true`, each compaction writes detailed info to `/tmp/pi-vcc-debug.json` — message counts, cut boundary, summary preview, sections.
578
584
  - **`modelThresholds`** *(default: none)*: per-model compaction thresholds. Keys match against `"provider/modelId"` (e.g., `"neuralwatt/zai-org/GLM-5.1-FP8"`) or just `"modelId"` (e.g., `"GLM-5.1"` — matched only when `provider/modelId` doesn't). Each value has:
579
585
  - **`reserveTokens`**: tokens to reserve for the LLM response. Overrides pi-core's global `compaction.reserveTokens` for matching models. Controls *when* compaction triggers: `contextTokens > contextWindow − reserveTokens`. A higher value compacts earlier (more conservative); a lower value lets context grow larger. Takes precedence over `compactAtTokens` and `compactPercent` when multiple are set.
package/index.ts CHANGED
@@ -5,17 +5,14 @@ import { registerProactiveThresholdHook } from "./src/hooks/proactive-threshold"
5
5
  import { registerPiVccCommand } from "./src/commands/pi-vcc";
6
6
  import { registerVccRecallCommand } from "./src/commands/vcc-recall";
7
7
  import { registerRecallTool } from "./src/tools/recall";
8
- import { resetInvisibleContinue } from "./src/core/invisible-continue";
8
+ import { registerInvisibleContinue } from "./src/core/invisible-continue";
9
9
 
10
10
  export default (pi: ExtensionAPI) => {
11
11
  scaffoldSettings();
12
+ registerInvisibleContinue(pi);
12
13
  registerBeforeCompactHook(pi);
13
14
  registerProactiveThresholdHook(pi);
14
15
  registerPiVccCommand(pi);
15
16
  registerVccRecallCommand(pi);
16
17
  registerRecallTool(pi);
17
-
18
- pi.on("session_start", () => {
19
- resetInvisibleContinue();
20
- });
21
18
  };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-vcc",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@monotykamary/pi-vcc",
9
- "version": "0.7.0",
9
+ "version": "0.8.0",
10
10
  "devDependencies": {
11
11
  "knip": "6.14.1",
12
12
  "typescript": "6.0.3"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-vcc",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "Algorithmic conversation compactor for pi - transcript-preserving structured summaries, no LLM calls",
5
5
  "main": "index.ts",
6
6
  "keywords": [
@@ -1,176 +1,32 @@
1
- import { Agent } from "@earendil-works/pi-agent-core";
2
-
3
- /**
4
- * Invisible continue — resume the agent loop after compaction without
5
- * injecting any message into context.
6
- *
7
- * Same mechanism as pi-retry: capture the live Agent instance via
8
- * subscribe() monkey-patch, then call agent.prompt([]) to start a
9
- * fresh agent loop that picks up where the compacted transcript left off.
10
- * The LLM sees the exact same message list — no new user prompt.
11
- *
12
- * Used after threshold compaction when the agent would otherwise stall
13
- * (willRetry=false, no queued messages).
14
- */
15
-
16
- let _agent: Agent | null = null;
17
- let _continueInProgress = false;
18
-
19
- // Timestamp of the last completed triggerInvisibleContinue().
20
- // Used by the continue() monkey-patch to detect when triggerInvisibleContinue
21
- // just ran (so it shouldn't fall back to prompt([]) again — the agent
22
- // already continued). This prevents double continuation when both
23
- // pi-retry and pi-vcc are installed.
24
- let _lastInvisibleContinueTime = 0;
25
-
26
- // Monkey-patch Agent.prototype.subscribe to capture the live instance.
27
- // Chain the previous patch (if pi-retry already patched it) so both
28
- // extensions can coexist.
29
- const _prevSubscribe = Agent.prototype.subscribe;
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- Agent.prototype.subscribe = function (this: Agent, ...args: any[]) {
32
- _agent = this;
33
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
- return (_prevSubscribe as any).apply(this, args);
35
- };
36
-
37
- // Monkey-patch continue() so that after compaction, when the rebuilt
38
- // context ends with an assistant message, the agent loop can still
39
- // continue. Without this patch, the session's continue() call throws
40
- // "Cannot continue from message role: assistant" and the agent loop
41
- // exits — leaving mid-task work unfinished.
42
- //
43
- // Note (pi 0.79+): Agent.continue() now drains queued steering/follow-up
44
- // messages before throwing, so this throw path only fires when there are
45
- // genuinely no queued messages — the prompt([]) fallback is still correct.
46
- //
47
- // The fix: when continue() would throw because the last message is an
48
- // assistant, fall back to prompt([]) instead. prompt([]) doesn't check
49
- // the last message role — it starts a fresh agent loop with the current
50
- // context. This is equivalent to the invisible-continue mechanism
51
- // (same call the extension fires from session_compact), but triggered
52
- // from the session's own continue() path so the while loop stays alive.
53
- //
54
- // Chains the previous patch (pi-retry, pi-invisible-continue) so all
55
- // mutexes are respected.
56
- const _prevContinue = Agent.prototype.continue;
57
- Agent.prototype.continue = function (this: Agent): Promise<void> {
58
- const self = this;
59
- return (async () => {
60
- while (_continueInProgress) {
61
- await new Promise(r => setTimeout(r, 10));
62
- }
63
- try {
64
- return await (_prevContinue as any).call(self);
65
- } catch (e: unknown) {
66
- const msg = e instanceof Error ? e.message : String(e);
67
-
68
- // After compaction, the rebuilt context often ends with an assistant
69
- // message. The original continue() throws "Cannot continue from
70
- // message role: assistant". Instead of swallowing the error and
71
- // letting the agent loop die (which leaves mid-task work unfinished),
72
- // fall back to prompt([]). This restarts the agent loop with the
73
- // compacted context — the LLM picks up where it left off.
74
- //
75
- // prompt([]) is safe regardless of last message role. The agent
76
- // processes the current context and continues.
77
- //
78
- // We only do this when the agent is NOT currently processing
79
- // (no other triggerInvisibleContinue in flight) to avoid races.
80
- if (msg.includes("Cannot continue from message role") ||
81
- msg.includes("Cannot continue from an assistant message")) {
82
- // Check stopReason of the last message — only continue if
83
- // the agent was mid-task (not a clean stop or user abort).
84
- const lastMsg = self.state.messages[self.state.messages.length - 1];
85
- if (lastMsg?.role === "assistant" &&
86
- lastMsg.stopReason !== "stop" &&
87
- lastMsg.stopReason !== "aborted") {
88
- // Agent was mid-task — fall through to prompt([])
89
- // (after the error-handling block)
90
- } else {
91
- // Agent finished cleanly or was aborted — don't continue.
92
- // The session loop should exit (task complete).
93
- return;
94
- }
95
- // Fall back to prompt([]) to actually continue the agent.
96
- // The while loop in _runAgentPrompt stays alive because prompt([])
97
- // runs the agent (which emits events, updates _lastAssistantMessage,
98
- // etc.).
99
- //
100
- // Guard: if triggerInvisibleContinue() just completed (within the
101
- // last 500ms), the agent already continued — skip the fallback to
102
- // avoid double continuation. This happens when both pi-retry and
103
- // pi-vcc are installed: pi-retry's triggerInvisibleContinue runs
104
- // first, then the session's continue() wrapper unblocks and would
105
- // fall back to prompt([]) again.
106
- if (!_continueInProgress && Date.now() - _lastInvisibleContinueTime > 500) {
107
- _continueInProgress = true;
108
- try {
109
- await self.prompt([]);
110
- } catch {
111
- // Agent already processing or other transient error
112
- } finally {
113
- _continueInProgress = false;
114
- }
115
- }
116
- return;
117
- }
118
-
119
- if (msg.includes("Agent is already processing")) {
120
- // Another extension is driving the agent — wait it out.
121
- return;
122
- }
123
- throw e;
124
- }
125
- })();
126
- };
127
-
128
- function sleep(ms: number): Promise<void> {
129
- return new Promise(resolve => setTimeout(resolve, ms));
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ export const VCC_RESUME_CUSTOM_TYPE = "pi-vcc:resume-after-compaction";
4
+
5
+ export function registerInvisibleContinue(pi: ExtensionAPI): void {
6
+ pi.on("context", (event) => {
7
+ const messages = event.messages.filter((message) => {
8
+ if (message.role !== "custom") return true;
9
+ return message.customType !== VCC_RESUME_CUSTOM_TYPE;
10
+ });
11
+ if (messages.length !== event.messages.length) return { messages };
12
+ });
130
13
  }
131
14
 
132
15
  /**
133
- * Fire an invisible continue after the agent becomes idle.
134
- *
135
- * MUST NOT be awaited — it schedules work that runs after the current
136
- * session loop iteration finishes. Awaiting would deadlock inside the
137
- * session's _handlePostAgentRun → _runAutoCompaction call stack.
16
+ * Queue a hidden follow-up through AgentSession. This keeps Pi's session-level
17
+ * busy state, retry, compaction, and native steer/follow-up queues coherent.
138
18
  */
139
- export function triggerInvisibleContinue(): void {
140
- if (!_agent) return;
141
- if (_continueInProgress) return;
142
- _continueInProgress = true;
143
-
144
- const run = async () => {
145
- try {
146
- // Wait for the current run to finish (activeRun resolves in
147
- // finishRun() after agent_end listeners return).
148
- await _agent!.waitForIdle();
149
-
150
- // Small delay to ensure the session's while-loop has exited
151
- // after _handlePostAgentRun returns false.
152
- await sleep(50);
153
-
154
- try {
155
- // Await so _continueInProgress stays true for the full cycle.
156
- await _agent!.prompt([]);
157
- } catch {
158
- // Ignore — if prompt throws, something else is driving.
159
- }
160
- } finally {
161
- _continueInProgress = false;
162
- // Record completion time so the continue() monkey-patch can
163
- // detect that an invisible continue just ran and avoid firing
164
- // a duplicate prompt([]) (RC7: double continuation guard).
165
- _lastInvisibleContinueTime = Date.now();
166
- }
167
- };
168
-
169
- void run();
170
- }
171
-
172
- /** Reset state on new session. */
173
- export function resetInvisibleContinue(): void {
174
- _continueInProgress = false;
175
- _lastInvisibleContinueTime = 0;
19
+ export function triggerInvisibleContinue(pi: ExtensionAPI): void {
20
+ pi.sendMessage(
21
+ {
22
+ customType: VCC_RESUME_CUSTOM_TYPE,
23
+ content: [],
24
+ display: false,
25
+ details: undefined,
26
+ },
27
+ {
28
+ triggerTurn: true,
29
+ deliverAs: "followUp",
30
+ },
31
+ );
176
32
  }
@@ -11,6 +11,7 @@ import {
11
11
  } from "../core/codex-output-limit";
12
12
  import { loadSettings, type PiVccSettings } from "../core/settings";
13
13
  import { triggerInvisibleContinue } from "../core/invisible-continue";
14
+ import { isProactiveTriggerActive } from "./proactive-threshold";
14
15
  import { countPiVccCompactionsFromSession, ordinalSuffix } from "../core/compaction-count";
15
16
  import type { PiVccCompactionDetails } from "../details";
16
17
 
@@ -25,7 +26,10 @@ export interface CompactionStats {
25
26
  let lastStats: CompactionStats | null = null;
26
27
  let lastCompactWasPiVcc = false;
27
28
  let lastCompactWasCodexRecovery = false;
29
+ let lastCompactWasProactive = false;
28
30
  let lastCompactHandledByVcc = false;
31
+ let lastCompactDeferredToFabric = false;
32
+ let hasNotifiedFabricDeferral = false;
29
33
  export const getLastCompactionStats = () => lastStats;
30
34
 
31
35
  const formatTokens = (n: number): string => {
@@ -98,6 +102,19 @@ interface EntryWithMessage {
98
102
  message: { role: string; content: unknown };
99
103
  }
100
104
 
105
+ const isHiddenEmptyCustomMessage = (message: unknown): boolean => {
106
+ if (!message || typeof message !== "object") return false;
107
+ const candidate = message as {
108
+ role?: unknown;
109
+ content?: unknown;
110
+ display?: unknown;
111
+ };
112
+ if (candidate.role !== "custom" || candidate.display !== false) return false;
113
+ return candidate.content === "" || (
114
+ Array.isArray(candidate.content) && candidate.content.length === 0
115
+ );
116
+ };
117
+
101
118
  export type OwnCutCancelReason =
102
119
  | "no_live_messages"
103
120
  | "too_few_live_messages";
@@ -265,7 +282,11 @@ export function buildOwnCut(branchEntries: any[], options?: { maxKeptTokens?: nu
265
282
  for (let i = lastCompactionIdx + 1; i < branchEntries.length; i++) {
266
283
  const e = branchEntries[i];
267
284
  if (e.type === "compaction") continue;
268
- if (e.type === "message" && e.message) {
285
+ if (
286
+ e.type === "message" &&
287
+ e.message &&
288
+ !isHiddenEmptyCustomMessage(e.message)
289
+ ) {
269
290
  liveMessages.push({ entry: e, message: e.message });
270
291
  }
271
292
  }
@@ -275,7 +296,11 @@ export function buildOwnCut(branchEntries: any[], options?: { maxKeptTokens?: nu
275
296
  if (!foundKept && e.id === lastKeptId) foundKept = true;
276
297
  if (!foundKept) continue;
277
298
  if (e.type === "compaction") continue;
278
- if (e.type === "message" && e.message) {
299
+ if (
300
+ e.type === "message" &&
301
+ e.message &&
302
+ !isHiddenEmptyCustomMessage(e.message)
303
+ ) {
279
304
  liveMessages.push({ entry: e, message: e.message });
280
305
  }
281
306
  }
@@ -413,8 +438,32 @@ export const shouldResumeAfterCompaction = (
413
438
  return true;
414
439
  };
415
440
 
441
+ export type CompactionCompletionMetadata = {
442
+ reason?: "manual" | "threshold" | "overflow";
443
+ willRetry?: boolean;
444
+ };
445
+
446
+ export function shouldTriggerResumeForCompaction(
447
+ event: CompactionCompletionMetadata,
448
+ sessionIsIdle: boolean,
449
+ wasProactiveCompaction: boolean,
450
+ wasCodexRecoveryCompaction: boolean,
451
+ ): boolean {
452
+ if (event.willRetry) return true;
453
+ if (wasCodexRecoveryCompaction || wasProactiveCompaction) return true;
454
+ if (event.reason === "manual" || event.reason === "overflow") return false;
455
+ if (event.reason === "threshold") return !sessionIsIdle;
456
+
457
+ // Pi before 0.79.10 did not expose compaction reason metadata. Be
458
+ // conservative there: an idle session may be manual or pre-prompt.
459
+ return !sessionIsIdle;
460
+ }
461
+
416
462
  export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
417
463
  pi.on("session_before_compact", (event, ctx) => {
464
+ // pi-fabric may run before pi-vcc and mark the shared event after claiming it.
465
+ if ((event as any)._fabricCompaction === true) return;
466
+
418
467
  const { preparation, branchEntries, customInstructions } = event;
419
468
  const settings = loadSettings();
420
469
 
@@ -451,6 +500,32 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
451
500
  const isPiVccHandled =
452
501
  isPiVcc || isCodexOutputLimitCompaction || isCodexContextOverflowCompaction;
453
502
 
503
+ // Explicit /pi-vcc takes precedence over the pi-fabric engine. For every
504
+ // other compaction, retain pi-vcc's trigger/recovery bookkeeping but let
505
+ // pi-fabric compile the summary.
506
+ if (!isPiVcc && process.env.PI_FABRIC_COMPACTION_ENGINE === "fabric") {
507
+ if (!isPiVccHandled && !settings.overrideDefaultCompaction) return;
508
+ if (isCodexContextOverflowCompaction) clearCodexContextOverflowPending();
509
+
510
+ lastCompactWasPiVcc = false;
511
+ lastCompactWasCodexRecovery =
512
+ isCodexOutputLimitCompaction || isCodexContextOverflowCompaction;
513
+ lastCompactWasProactive = isProactiveTriggerActive();
514
+ lastCompactHandledByVcc = true;
515
+ lastCompactDeferredToFabric = true;
516
+
517
+ if (settings.overrideDefaultCompaction && !hasNotifiedFabricDeferral) {
518
+ hasNotifiedFabricDeferral = true;
519
+ try {
520
+ ctx?.ui?.notify?.(
521
+ "pi-vcc: deferring compaction to pi-fabric engine (explicit /pi-vcc still uses pi-vcc)",
522
+ "info",
523
+ );
524
+ } catch {}
525
+ }
526
+ return;
527
+ }
528
+
454
529
  // Always handle explicit /pi-vcc and Codex recovery markers. Otherwise,
455
530
  // only handle when the user opted in via settings.
456
531
  if (!isPiVccHandled && !settings.overrideDefaultCompaction) return;
@@ -626,7 +701,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
626
701
  lastCompactWasPiVcc = isPiVcc;
627
702
  lastCompactWasCodexRecovery =
628
703
  isCodexOutputLimitCompaction || isCodexContextOverflowCompaction;
704
+ lastCompactWasProactive = isProactiveTriggerActive();
629
705
  lastCompactHandledByVcc = true;
706
+ lastCompactDeferredToFabric = false;
630
707
 
631
708
  // Signal to neuralwatt-mcr that pi-vcc is handling compaction
632
709
  // so it doesn't cancel the event. Without this flag, neuralwatt-mcr
@@ -644,6 +721,10 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
644
721
  };
645
722
  });
646
723
 
724
+ pi.on("session_start", () => {
725
+ hasNotifiedFabricDeferral = false;
726
+ });
727
+
647
728
  // After compaction completes, check if the agent loop stalled and needs
648
729
  // an invisible continue to resume. This handles threshold compaction
649
730
  // where willRetry=false — pi-core doesn't auto-retry, and the agent loop
@@ -652,15 +733,20 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
652
733
  // that isn't a clean end_turn), the agent was interrupted and should
653
734
  // continue.
654
735
  pi.on("session_compact", (event, ctx) => {
655
- // Only act when pi-vcc drove the compaction
736
+ // Act when pi-vcc compiled the summary or tracked a compaction delegated
737
+ // to pi-fabric so proactive and Codex recovery continuation still works.
656
738
  if (!lastCompactHandledByVcc) return;
657
739
  const wasCodexRecoveryCompaction = lastCompactWasCodexRecovery;
740
+ const wasProactiveCompaction = lastCompactWasProactive;
741
+ const wasDeferredToFabric = lastCompactDeferredToFabric;
658
742
  lastCompactHandledByVcc = false;
659
743
  lastCompactWasCodexRecovery = false;
744
+ lastCompactWasProactive = false;
745
+ lastCompactDeferredToFabric = false;
660
746
 
661
- // Fire success toast for /compact path only (delayed to let UI settle).
662
- // /pi-vcc path uses its own onComplete callback in the command handler.
663
- if (!lastCompactWasPiVcc) {
747
+ // Fire success toast for pi-vcc's /compact path only (delayed to let UI
748
+ // settle). /pi-vcc has its own callback, and pi-fabric owns delegated UI.
749
+ if (!lastCompactWasPiVcc && !wasDeferredToFabric) {
664
750
  const stats = lastStats;
665
751
  const count = countPiVccCompactionsFromSession(ctx?.sessionManager as any);
666
752
  const compactionLabel = count > 0
@@ -708,11 +794,29 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
708
794
  break;
709
795
  }
710
796
  }
711
- if (!shouldResumeAfterCompaction(lastMsg, wasCodexRecoveryCompaction)) return;
712
-
713
- // Agent was mid-task (toolUse or length). The Codex recovery marker
714
- // additionally permits its known provider errors to continue.
715
- triggerInvisibleContinue();
797
+ // A core-owned retry also gets a marker. Agent.continue() drains the
798
+ // queued marker before checking the assistant-tail role, so no Agent
799
+ // prototype fallback is needed.
800
+ const completion = event as CompactionCompletionMetadata;
801
+ const legacyActiveCompaction =
802
+ completion.reason === undefined &&
803
+ completion.willRetry === undefined &&
804
+ !ctx.isIdle();
805
+ if (
806
+ !completion.willRetry &&
807
+ !legacyActiveCompaction &&
808
+ !shouldResumeAfterCompaction(lastMsg, wasCodexRecoveryCompaction)
809
+ ) return;
810
+ if (!shouldTriggerResumeForCompaction(
811
+ completion,
812
+ ctx.isIdle(),
813
+ wasProactiveCompaction,
814
+ wasCodexRecoveryCompaction,
815
+ )) return;
816
+
817
+ // Queue through Pi's native follow-up path so a concurrent user prompt
818
+ // wins cleanly instead of racing a low-level Agent.prompt([]) call.
819
+ triggerInvisibleContinue(pi);
716
820
  } catch {
717
821
  // Non-critical — if context inspection fails, don't block compaction
718
822
  }