@monotykamary/pi-vcc 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monotykamary/pi-vcc",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Algorithmic conversation compactor for pi - transcript-preserving structured summaries, no LLM calls",
5
5
  "main": "index.ts",
6
6
  "keywords": [
@@ -1,8 +1,10 @@
1
1
  export const CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION = "__pi_vcc_codex_output_limit__";
2
+ export const CODEX_CONTEXT_OVERFLOW_COMPACT_INSTRUCTION = "__pi_vcc_codex_context_overflow__";
2
3
 
3
- /** Identify the Codex provider error used when a response reaches its output cap. */
4
- export const isCodexOutputLimitError = (message: unknown): boolean => {
5
- if (!message || typeof message !== "object") return false;
4
+ let codexContextOverflowPending = false;
5
+
6
+ const codexErrorMessage = (message: unknown): string | undefined => {
7
+ if (!message || typeof message !== "object") return undefined;
6
8
 
7
9
  const candidate = message as {
8
10
  role?: unknown;
@@ -16,11 +18,39 @@ export const isCodexOutputLimitError = (message: unknown): boolean => {
16
18
  candidate.provider === "openai-codex" ||
17
19
  candidate.api === "openai-codex-responses";
18
20
 
19
- return (
20
- candidate.role === "assistant" &&
21
- isCodexMessage &&
22
- candidate.stopReason === "error" &&
23
- typeof candidate.errorMessage === "string" &&
24
- /maximum output token limit/i.test(candidate.errorMessage)
25
- );
21
+ if (
22
+ candidate.role !== "assistant" ||
23
+ !isCodexMessage ||
24
+ candidate.stopReason !== "error" ||
25
+ typeof candidate.errorMessage !== "string"
26
+ ) {
27
+ return undefined;
28
+ }
29
+
30
+ return candidate.errorMessage;
31
+ };
32
+
33
+ /** Identify the Codex provider error used when a response reaches its output cap. */
34
+ export const isCodexOutputLimitError = (message: unknown): boolean => {
35
+ const errorMessage = codexErrorMessage(message);
36
+ return errorMessage !== undefined && /maximum output token limit/i.test(errorMessage);
37
+ };
38
+
39
+ /** Identify the Codex provider error used when the request input is too large. */
40
+ export const isCodexContextOverflowError = (message: unknown): boolean => {
41
+ const errorMessage = codexErrorMessage(message);
42
+ return errorMessage !== undefined && /exceeds the context window/i.test(errorMessage);
43
+ };
44
+
45
+ /** Mark a Codex context overflow until pi-core begins its recovery compaction. */
46
+ export const markCodexContextOverflowPending = (): void => {
47
+ codexContextOverflowPending = true;
48
+ };
49
+
50
+ /** Check whether the pending Codex overflow belongs to the next compaction. */
51
+ export const isCodexContextOverflowPending = (): boolean => codexContextOverflowPending;
52
+
53
+ /** Clear pending Codex overflow state after compaction or session reset. */
54
+ export const clearCodexContextOverflowPending = (): void => {
55
+ codexContextOverflowPending = false;
26
56
  };
@@ -2,7 +2,11 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { writeFileSync } from "fs";
3
3
  import { compile, type CompileInput } from "../core/summarize";
4
4
  import {
5
+ CODEX_CONTEXT_OVERFLOW_COMPACT_INSTRUCTION,
5
6
  CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION,
7
+ clearCodexContextOverflowPending,
8
+ isCodexContextOverflowError,
9
+ isCodexContextOverflowPending,
6
10
  isCodexOutputLimitError,
7
11
  } from "../core/codex-output-limit";
8
12
  import { loadSettings, type PiVccSettings } from "../core/settings";
@@ -20,7 +24,7 @@ export interface CompactionStats {
20
24
 
21
25
  let lastStats: CompactionStats | null = null;
22
26
  let lastCompactWasPiVcc = false;
23
- let lastCompactWasCodexOutputLimit = false;
27
+ let lastCompactWasCodexRecovery = false;
24
28
  let lastCompactHandledByVcc = false;
25
29
  export const getLastCompactionStats = () => lastStats;
26
30
 
@@ -394,14 +398,17 @@ const REASON_MESSAGES: Record<OwnCutCancelReason, string> = {
394
398
 
395
399
  export const shouldResumeAfterCompaction = (
396
400
  lastMsg: unknown,
397
- allowCodexOutputLimit = false,
401
+ allowCodexRecovery = false,
398
402
  ): boolean => {
399
403
  if (!lastMsg || typeof lastMsg !== "object") return false;
400
404
  const message = lastMsg as { role?: unknown; stopReason?: unknown };
401
405
  if (message.role !== "assistant") return false;
402
406
  if (message.stopReason === "stop" || message.stopReason === "aborted") return false;
403
407
  if (message.stopReason === "error") {
404
- return allowCodexOutputLimit && isCodexOutputLimitError(lastMsg);
408
+ return allowCodexRecovery && (
409
+ isCodexOutputLimitError(lastMsg) ||
410
+ isCodexContextOverflowError(lastMsg)
411
+ );
405
412
  }
406
413
  return true;
407
414
  };
@@ -430,11 +437,24 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
430
437
  const isPiVcc = customInstructions === PI_VCC_COMPACT_INSTRUCTION;
431
438
  const isCodexOutputLimitCompaction =
432
439
  customInstructions === CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION;
433
- const isPiVccHandled = isPiVcc || isCodexOutputLimitCompaction;
434
-
435
- // Always handle explicit /pi-vcc and Codex output-limit markers.
436
- // Otherwise, only handle when user opted in via settings.
440
+ const isCodexContextOverflowMarker =
441
+ customInstructions === CODEX_CONTEXT_OVERFLOW_COMPACT_INSTRUCTION;
442
+ const lastBranchAssistant = [...(branchEntries as any[])]
443
+ .reverse()
444
+ .find((entry: any) => entry.type === "message" && entry.message?.role === "assistant")
445
+ ?.message;
446
+ const isCodexContextOverflowCompaction =
447
+ isCodexContextOverflowMarker ||
448
+ (settings.overrideDefaultCompaction &&
449
+ isCodexContextOverflowPending() &&
450
+ isCodexContextOverflowError(lastBranchAssistant));
451
+ const isPiVccHandled =
452
+ isPiVcc || isCodexOutputLimitCompaction || isCodexContextOverflowCompaction;
453
+
454
+ // Always handle explicit /pi-vcc and Codex recovery markers. Otherwise,
455
+ // only handle when the user opted in via settings.
437
456
  if (!isPiVccHandled && !settings.overrideDefaultCompaction) return;
457
+ if (isCodexContextOverflowCompaction) clearCodexContextOverflowPending();
438
458
 
439
459
  // Budget the kept tail so that summary + kept + system/tools + the model's
440
460
  // OUTPUT budget (maxTokens) all fit in the window. Without reserving
@@ -604,7 +624,8 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
604
624
  };
605
625
 
606
626
  lastCompactWasPiVcc = isPiVcc;
607
- lastCompactWasCodexOutputLimit = isCodexOutputLimitCompaction;
627
+ lastCompactWasCodexRecovery =
628
+ isCodexOutputLimitCompaction || isCodexContextOverflowCompaction;
608
629
  lastCompactHandledByVcc = true;
609
630
 
610
631
  // Signal to neuralwatt-mcr that pi-vcc is handling compaction
@@ -633,9 +654,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
633
654
  pi.on("session_compact", (event, ctx) => {
634
655
  // Only act when pi-vcc drove the compaction
635
656
  if (!lastCompactHandledByVcc) return;
636
- const wasCodexOutputLimitCompaction = lastCompactWasCodexOutputLimit;
657
+ const wasCodexRecoveryCompaction = lastCompactWasCodexRecovery;
637
658
  lastCompactHandledByVcc = false;
638
- lastCompactWasCodexOutputLimit = false;
659
+ lastCompactWasCodexRecovery = false;
639
660
 
640
661
  // Fire success toast for /compact path only (delayed to let UI settle).
641
662
  // /pi-vcc path uses its own onComplete callback in the command handler.
@@ -667,13 +688,13 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
667
688
  // - Last message is assistant with stopReason=stop (task finished)
668
689
  // - Last message is assistant with stopReason=aborted (user cancelled)
669
690
  // - Last message is assistant with stopReason=error, unless this was
670
- // the pi-vcc Codex maximum-output recovery compaction
691
+ // a pi-vcc Codex recovery compaction
671
692
  //
672
693
  // We DO continue when:
673
694
  // - Last message is assistant with stopReason=toolUse (mid-tool cycle)
674
695
  // - Last message is assistant with stopReason=length (hit max tokens)
675
- // - Last message is the known Codex maximum-output error from the
676
- // pi-vcc recovery compaction
696
+ // - Last message is a known Codex recovery error from the pi-vcc
697
+ // recovery compaction
677
698
  // - Compact-all (firstKeptEntryId="") — context is just the summary,
678
699
  // the agent needs to re-enter the loop to continue the task
679
700
  try {
@@ -687,10 +708,10 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
687
708
  break;
688
709
  }
689
710
  }
690
- if (!shouldResumeAfterCompaction(lastMsg, wasCodexOutputLimitCompaction)) return;
711
+ if (!shouldResumeAfterCompaction(lastMsg, wasCodexRecoveryCompaction)) return;
691
712
 
692
713
  // Agent was mid-task (toolUse or length). The Codex recovery marker
693
- // additionally permits its maximum-output error to continue.
714
+ // additionally permits its known provider errors to continue.
694
715
  triggerInvisibleContinue();
695
716
  } catch {
696
717
  // Non-critical — if context inspection fails, don't block compaction
@@ -1,7 +1,11 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
+ CODEX_CONTEXT_OVERFLOW_COMPACT_INSTRUCTION,
3
4
  CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION,
5
+ clearCodexContextOverflowPending,
6
+ isCodexContextOverflowError,
4
7
  isCodexOutputLimitError,
8
+ markCodexContextOverflowPending,
5
9
  } from "../core/codex-output-limit";
6
10
  import { loadSettings, getModelThreshold, resolveTriggerTokens } from "../core/settings";
7
11
 
@@ -40,6 +44,7 @@ export const isProactiveTriggerActive = () => proactiveTriggerActive;
40
44
  export const resetProactiveState = () => {
41
45
  lastCompactTime = 0;
42
46
  proactiveTriggerActive = false;
47
+ clearCodexContextOverflowPending();
43
48
  };
44
49
 
45
50
  /**
@@ -105,6 +110,28 @@ const triggerCodexOutputLimitCompaction = (ctx: ProactiveContext) => {
105
110
  ctx.compact?.({ customInstructions: CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION });
106
111
  };
107
112
 
113
+ /** Force compaction when Codex omits the model identity from an overflow error. */
114
+ const triggerCodexContextOverflowCompaction = (ctx: ProactiveContext) => {
115
+ if (isCoolingDown()) return;
116
+
117
+ try {
118
+ ctx?.ui?.notify?.(
119
+ "pi-vcc: Codex input exceeded the context window. Compacting...",
120
+ "info",
121
+ );
122
+ } catch {}
123
+
124
+ setCooldown();
125
+ proactiveTriggerActive = true;
126
+ ctx.compact?.({ customInstructions: CODEX_CONTEXT_OVERFLOW_COMPACT_INSTRUCTION });
127
+ };
128
+
129
+ const hasCurrentModelIdentity = (message: unknown, model: any): boolean => {
130
+ if (!message || typeof message !== "object" || !model) return false;
131
+ const candidate = message as { model?: unknown; provider?: unknown };
132
+ return candidate.model === model.id && candidate.provider === model.provider;
133
+ };
134
+
108
135
  const lastAssistantMessage = (event: unknown): unknown => {
109
136
  const messages = (event as any)?.messages;
110
137
  if (!Array.isArray(messages)) return undefined;
@@ -142,14 +169,28 @@ const lastAssistantMessage = (event: unknown): unknown => {
142
169
  * "Compacting..." then "Skipped compaction" notifications.
143
170
  */
144
171
  export const registerProactiveThresholdHook = (pi: ExtensionAPI) => {
145
- // Codex reports some output-limit responses as errors instead of the
146
- // standard "length" stop reason. The error has no usable context usage,
147
- // so pi-core cannot discover the need to compact from its normal checks.
172
+ // Codex reports output-limit responses as errors instead of the standard
173
+ // "length" stop reason. Those errors have no usable context usage, so
174
+ // pi-core cannot discover the need to compact from its normal checks.
175
+ // Context-window errors are tracked separately because pi-core already
176
+ // recognizes and compacts those.
148
177
  pi.on("agent_end", (event, ctx) => {
149
- if (isCodexOutputLimitError(lastAssistantMessage(event))) {
178
+ const lastMessage = lastAssistantMessage(event);
179
+ if (isCodexOutputLimitError(lastMessage)) {
150
180
  triggerCodexOutputLimitCompaction(ctx);
151
181
  return;
152
182
  }
183
+ if (isCodexContextOverflowError(lastMessage)) {
184
+ markCodexContextOverflowPending();
185
+ // pi-core's overflow check also requires the assistant message to carry
186
+ // the current model identity. Codex can omit it on failed responses;
187
+ // force the recovery only for that case so matching responses continue
188
+ // through pi-core's normal auto-retry path.
189
+ if (!hasCurrentModelIdentity(lastMessage, ctx.model)) {
190
+ triggerCodexContextOverflowCompaction(ctx);
191
+ }
192
+ return;
193
+ }
153
194
  checkAndTrigger(ctx, "auto");
154
195
  });
155
196
 
@@ -162,11 +203,13 @@ export const registerProactiveThresholdHook = (pi: ExtensionAPI) => {
162
203
  pi.on("session_compact", () => {
163
204
  setCooldown();
164
205
  proactiveTriggerActive = false;
206
+ clearCodexContextOverflowPending();
165
207
  });
166
208
 
167
209
  // Reset state on session start so state doesn't leak between sessions
168
210
  pi.on("session_start", () => {
169
211
  lastCompactTime = 0;
170
212
  proactiveTriggerActive = false;
213
+ clearCodexContextOverflowPending();
171
214
  });
172
215
  };