@monotykamary/pi-vcc 0.7.1 → 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
|
@@ -0,0 +1,56 @@
|
|
|
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__";
|
|
3
|
+
|
|
4
|
+
let codexContextOverflowPending = false;
|
|
5
|
+
|
|
6
|
+
const codexErrorMessage = (message: unknown): string | undefined => {
|
|
7
|
+
if (!message || typeof message !== "object") return undefined;
|
|
8
|
+
|
|
9
|
+
const candidate = message as {
|
|
10
|
+
role?: unknown;
|
|
11
|
+
provider?: unknown;
|
|
12
|
+
api?: unknown;
|
|
13
|
+
stopReason?: unknown;
|
|
14
|
+
errorMessage?: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const isCodexMessage =
|
|
18
|
+
candidate.provider === "openai-codex" ||
|
|
19
|
+
candidate.api === "openai-codex-responses";
|
|
20
|
+
|
|
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;
|
|
56
|
+
};
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
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
|
+
import {
|
|
5
|
+
CODEX_CONTEXT_OVERFLOW_COMPACT_INSTRUCTION,
|
|
6
|
+
CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION,
|
|
7
|
+
clearCodexContextOverflowPending,
|
|
8
|
+
isCodexContextOverflowError,
|
|
9
|
+
isCodexContextOverflowPending,
|
|
10
|
+
isCodexOutputLimitError,
|
|
11
|
+
} from "../core/codex-output-limit";
|
|
4
12
|
import { loadSettings, type PiVccSettings } from "../core/settings";
|
|
5
13
|
import { triggerInvisibleContinue } from "../core/invisible-continue";
|
|
6
14
|
import { countPiVccCompactionsFromSession, ordinalSuffix } from "../core/compaction-count";
|
|
@@ -16,6 +24,7 @@ export interface CompactionStats {
|
|
|
16
24
|
|
|
17
25
|
let lastStats: CompactionStats | null = null;
|
|
18
26
|
let lastCompactWasPiVcc = false;
|
|
27
|
+
let lastCompactWasCodexRecovery = false;
|
|
19
28
|
let lastCompactHandledByVcc = false;
|
|
20
29
|
export const getLastCompactionStats = () => lastStats;
|
|
21
30
|
|
|
@@ -387,6 +396,23 @@ const REASON_MESSAGES: Record<OwnCutCancelReason, string> = {
|
|
|
387
396
|
too_few_live_messages: "pi-vcc: Too few messages to compact",
|
|
388
397
|
};
|
|
389
398
|
|
|
399
|
+
export const shouldResumeAfterCompaction = (
|
|
400
|
+
lastMsg: unknown,
|
|
401
|
+
allowCodexRecovery = false,
|
|
402
|
+
): boolean => {
|
|
403
|
+
if (!lastMsg || typeof lastMsg !== "object") return false;
|
|
404
|
+
const message = lastMsg as { role?: unknown; stopReason?: unknown };
|
|
405
|
+
if (message.role !== "assistant") return false;
|
|
406
|
+
if (message.stopReason === "stop" || message.stopReason === "aborted") return false;
|
|
407
|
+
if (message.stopReason === "error") {
|
|
408
|
+
return allowCodexRecovery && (
|
|
409
|
+
isCodexOutputLimitError(lastMsg) ||
|
|
410
|
+
isCodexContextOverflowError(lastMsg)
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return true;
|
|
414
|
+
};
|
|
415
|
+
|
|
390
416
|
export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
391
417
|
pi.on("session_before_compact", (event, ctx) => {
|
|
392
418
|
const { preparation, branchEntries, customInstructions } = event;
|
|
@@ -409,10 +435,26 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
409
435
|
// per-model threshold's perspective, but this is preferable to blocking
|
|
410
436
|
// an explicit user action (/compact).
|
|
411
437
|
const isPiVcc = customInstructions === PI_VCC_COMPACT_INSTRUCTION;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
438
|
+
const isCodexOutputLimitCompaction =
|
|
439
|
+
customInstructions === CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION;
|
|
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.
|
|
456
|
+
if (!isPiVccHandled && !settings.overrideDefaultCompaction) return;
|
|
457
|
+
if (isCodexContextOverflowCompaction) clearCodexContextOverflowPending();
|
|
416
458
|
|
|
417
459
|
// Budget the kept tail so that summary + kept + system/tools + the model's
|
|
418
460
|
// OUTPUT budget (maxTokens) all fit in the window. Without reserving
|
|
@@ -582,6 +624,8 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
582
624
|
};
|
|
583
625
|
|
|
584
626
|
lastCompactWasPiVcc = isPiVcc;
|
|
627
|
+
lastCompactWasCodexRecovery =
|
|
628
|
+
isCodexOutputLimitCompaction || isCodexContextOverflowCompaction;
|
|
585
629
|
lastCompactHandledByVcc = true;
|
|
586
630
|
|
|
587
631
|
// Signal to neuralwatt-mcr that pi-vcc is handling compaction
|
|
@@ -610,7 +654,9 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
610
654
|
pi.on("session_compact", (event, ctx) => {
|
|
611
655
|
// Only act when pi-vcc drove the compaction
|
|
612
656
|
if (!lastCompactHandledByVcc) return;
|
|
657
|
+
const wasCodexRecoveryCompaction = lastCompactWasCodexRecovery;
|
|
613
658
|
lastCompactHandledByVcc = false;
|
|
659
|
+
lastCompactWasCodexRecovery = false;
|
|
614
660
|
|
|
615
661
|
// Fire success toast for /compact path only (delayed to let UI settle).
|
|
616
662
|
// /pi-vcc path uses its own onComplete callback in the command handler.
|
|
@@ -641,12 +687,14 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
641
687
|
// - Last message is user/toolResult (agent can continue naturally)
|
|
642
688
|
// - Last message is assistant with stopReason=stop (task finished)
|
|
643
689
|
// - Last message is assistant with stopReason=aborted (user cancelled)
|
|
644
|
-
// - Last message is assistant with stopReason=error
|
|
645
|
-
//
|
|
690
|
+
// - Last message is assistant with stopReason=error, unless this was
|
|
691
|
+
// a pi-vcc Codex recovery compaction
|
|
646
692
|
//
|
|
647
693
|
// We DO continue when:
|
|
648
694
|
// - Last message is assistant with stopReason=toolUse (mid-tool cycle)
|
|
649
695
|
// - Last message is assistant with stopReason=length (hit max tokens)
|
|
696
|
+
// - Last message is a known Codex recovery error from the pi-vcc
|
|
697
|
+
// recovery compaction
|
|
650
698
|
// - Compact-all (firstKeptEntryId="") — context is just the summary,
|
|
651
699
|
// the agent needs to re-enter the loop to continue the task
|
|
652
700
|
try {
|
|
@@ -660,21 +708,10 @@ export const registerBeforeCompactHook = (pi: ExtensionAPI) => {
|
|
|
660
708
|
break;
|
|
661
709
|
}
|
|
662
710
|
}
|
|
663
|
-
if (!lastMsg
|
|
664
|
-
|
|
665
|
-
// Agent completed its turn cleanly — no continuation needed
|
|
666
|
-
if (lastMsg.stopReason === "stop") return;
|
|
667
|
-
|
|
668
|
-
// Agent was aborted by user — don't auto-continue
|
|
669
|
-
if (lastMsg.stopReason === "aborted") return;
|
|
670
|
-
|
|
671
|
-
// Agent hit an error — pi-retry handles this via its agent_end handler.
|
|
672
|
-
// If we also fire triggerInvisibleContinue, both extensions race
|
|
673
|
-
// to call prompt([]), causing "Agent is already processing" (wasteful)
|
|
674
|
-
// or a duplicate continuation. Let pi-retry own error retries.
|
|
675
|
-
if (lastMsg.stopReason === "error") return;
|
|
711
|
+
if (!shouldResumeAfterCompaction(lastMsg, wasCodexRecoveryCompaction)) return;
|
|
676
712
|
|
|
677
|
-
// Agent was mid-task (toolUse or length)
|
|
713
|
+
// Agent was mid-task (toolUse or length). The Codex recovery marker
|
|
714
|
+
// additionally permits its known provider errors to continue.
|
|
678
715
|
triggerInvisibleContinue();
|
|
679
716
|
} catch {
|
|
680
717
|
// Non-critical — if context inspection fails, don't block compaction
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
CODEX_CONTEXT_OVERFLOW_COMPACT_INSTRUCTION,
|
|
4
|
+
CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION,
|
|
5
|
+
clearCodexContextOverflowPending,
|
|
6
|
+
isCodexContextOverflowError,
|
|
7
|
+
isCodexOutputLimitError,
|
|
8
|
+
markCodexContextOverflowPending,
|
|
9
|
+
} from "../core/codex-output-limit";
|
|
2
10
|
import { loadSettings, getModelThreshold, resolveTriggerTokens } from "../core/settings";
|
|
3
11
|
|
|
12
|
+
type ProactiveContext = {
|
|
13
|
+
model?: any;
|
|
14
|
+
getContextUsage?: () => any;
|
|
15
|
+
compact?: (options?: any) => void;
|
|
16
|
+
ui?: any;
|
|
17
|
+
};
|
|
18
|
+
|
|
4
19
|
const formatTokens = (n: number): string => {
|
|
5
20
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
6
21
|
return String(n);
|
|
@@ -29,6 +44,7 @@ export const isProactiveTriggerActive = () => proactiveTriggerActive;
|
|
|
29
44
|
export const resetProactiveState = () => {
|
|
30
45
|
lastCompactTime = 0;
|
|
31
46
|
proactiveTriggerActive = false;
|
|
47
|
+
clearCodexContextOverflowPending();
|
|
32
48
|
};
|
|
33
49
|
|
|
34
50
|
/**
|
|
@@ -36,7 +52,7 @@ export const resetProactiveState = () => {
|
|
|
36
52
|
* if so. Safe to call from multiple event handlers — cooldown prevents
|
|
37
53
|
* double-triggering.
|
|
38
54
|
*/
|
|
39
|
-
const checkAndTrigger = (ctx:
|
|
55
|
+
const checkAndTrigger = (ctx: ProactiveContext, source: string) => {
|
|
40
56
|
const settings = loadSettings();
|
|
41
57
|
const threshold = getModelThreshold(settings, ctx.model);
|
|
42
58
|
|
|
@@ -78,6 +94,54 @@ const checkAndTrigger = (ctx: { model?: any; getContextUsage?: () => any; compac
|
|
|
78
94
|
ctx.compact?.();
|
|
79
95
|
};
|
|
80
96
|
|
|
97
|
+
/** Force compaction for Codex responses that report an output limit as an error. */
|
|
98
|
+
const triggerCodexOutputLimitCompaction = (ctx: ProactiveContext) => {
|
|
99
|
+
if (isCoolingDown()) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
ctx?.ui?.notify?.(
|
|
103
|
+
"pi-vcc: Codex reached its maximum output token limit. Compacting...",
|
|
104
|
+
"info",
|
|
105
|
+
);
|
|
106
|
+
} catch {}
|
|
107
|
+
|
|
108
|
+
setCooldown();
|
|
109
|
+
proactiveTriggerActive = true;
|
|
110
|
+
ctx.compact?.({ customInstructions: CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION });
|
|
111
|
+
};
|
|
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
|
+
|
|
135
|
+
const lastAssistantMessage = (event: unknown): unknown => {
|
|
136
|
+
const messages = (event as any)?.messages;
|
|
137
|
+
if (!Array.isArray(messages)) return undefined;
|
|
138
|
+
|
|
139
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
140
|
+
if (messages[i]?.role === "assistant") return messages[i];
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
};
|
|
144
|
+
|
|
81
145
|
/**
|
|
82
146
|
* Registers proactive configured compaction thresholds.
|
|
83
147
|
*
|
|
@@ -105,8 +169,28 @@ const checkAndTrigger = (ctx: { model?: any; getContextUsage?: () => any; compac
|
|
|
105
169
|
* "Compacting..." then "Skipped compaction" notifications.
|
|
106
170
|
*/
|
|
107
171
|
export const registerProactiveThresholdHook = (pi: ExtensionAPI) => {
|
|
108
|
-
//
|
|
109
|
-
|
|
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.
|
|
177
|
+
pi.on("agent_end", (event, ctx) => {
|
|
178
|
+
const lastMessage = lastAssistantMessage(event);
|
|
179
|
+
if (isCodexOutputLimitError(lastMessage)) {
|
|
180
|
+
triggerCodexOutputLimitCompaction(ctx);
|
|
181
|
+
return;
|
|
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
|
+
}
|
|
110
194
|
checkAndTrigger(ctx, "auto");
|
|
111
195
|
});
|
|
112
196
|
|
|
@@ -119,11 +203,13 @@ export const registerProactiveThresholdHook = (pi: ExtensionAPI) => {
|
|
|
119
203
|
pi.on("session_compact", () => {
|
|
120
204
|
setCooldown();
|
|
121
205
|
proactiveTriggerActive = false;
|
|
206
|
+
clearCodexContextOverflowPending();
|
|
122
207
|
});
|
|
123
208
|
|
|
124
209
|
// Reset state on session start so state doesn't leak between sessions
|
|
125
210
|
pi.on("session_start", () => {
|
|
126
211
|
lastCompactTime = 0;
|
|
127
212
|
proactiveTriggerActive = false;
|
|
213
|
+
clearCodexContextOverflowPending();
|
|
128
214
|
});
|
|
129
215
|
};
|