@monotykamary/pi-vcc 0.7.2 → 0.7.4
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,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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
20
|
-
candidate.role
|
|
21
|
-
isCodexMessage
|
|
22
|
-
candidate.stopReason
|
|
23
|
-
typeof candidate.errorMessage
|
|
24
|
-
|
|
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
|
};
|
package/src/core/settings.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { dirname, join } from "path";
|
|
3
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
const settingsPath = (): string =>
|
|
6
6
|
process.env.PI_VCC_CONFIG_PATH ?? join(getAgentDir(), "pi-vcc-config.json");
|
|
@@ -181,6 +181,32 @@ const readJson = (path: string): Record<string, unknown> | null => {
|
|
|
181
181
|
}
|
|
182
182
|
};
|
|
183
183
|
|
|
184
|
+
const readPiCoreCompactionEnabled = (path: string): boolean | undefined => {
|
|
185
|
+
const parsed = readJson(path);
|
|
186
|
+
if (!parsed) return undefined;
|
|
187
|
+
const enabled = (parsed as { compaction?: { enabled?: unknown } }).compaction?.enabled;
|
|
188
|
+
return typeof enabled === "boolean" ? enabled : undefined;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Read pi-core's effective `compaction.enabled` setting.
|
|
193
|
+
*
|
|
194
|
+
* pi-core's `_checkCompaction` bails immediately when this is false, so neither
|
|
195
|
+
* overflow nor threshold auto-compaction fires. When that happens pi-vcc must
|
|
196
|
+
* drive compaction itself via `ctx.compact()` (the manual path skips the
|
|
197
|
+
* `enabled` gate). Mirrors pi-core's merge: project (`<cwd>/<CONFIG_DIR_NAME>/
|
|
198
|
+
* settings.json`) overrides global (`<agentDir>/settings.json`); defaults true.
|
|
199
|
+
*/
|
|
200
|
+
export function isPiCoreCompactionEnabled(projectCwd?: string): boolean {
|
|
201
|
+
const globalEnabled = readPiCoreCompactionEnabled(join(getAgentDir(), "settings.json"));
|
|
202
|
+
if (projectCwd) {
|
|
203
|
+
const projectEnabled = readPiCoreCompactionEnabled(join(projectCwd, CONFIG_DIR_NAME, "settings.json"));
|
|
204
|
+
if (typeof projectEnabled === "boolean") return projectEnabled;
|
|
205
|
+
}
|
|
206
|
+
if (typeof globalEnabled === "boolean") return globalEnabled;
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
184
210
|
export function loadSettings(): PiVccSettings {
|
|
185
211
|
const parsed = readJson(settingsPath());
|
|
186
212
|
if (!parsed || typeof parsed !== "object") return { ...DEFAULT_SETTINGS };
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
|
657
|
+
const wasCodexRecoveryCompaction = lastCompactWasCodexRecovery;
|
|
637
658
|
lastCompactHandledByVcc = false;
|
|
638
|
-
|
|
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
|
-
//
|
|
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
|
|
676
|
-
//
|
|
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,
|
|
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
|
|
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,15 +1,20 @@
|
|
|
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
|
-
import { loadSettings, getModelThreshold, resolveTriggerTokens } from "../core/settings";
|
|
10
|
+
import { loadSettings, getModelThreshold, isPiCoreCompactionEnabled, resolveTriggerTokens } from "../core/settings";
|
|
7
11
|
|
|
8
12
|
type ProactiveContext = {
|
|
9
13
|
model?: any;
|
|
10
14
|
getContextUsage?: () => any;
|
|
11
15
|
compact?: (options?: any) => void;
|
|
12
16
|
ui?: any;
|
|
17
|
+
sessionManager?: { getCwd?: () => string };
|
|
13
18
|
};
|
|
14
19
|
|
|
15
20
|
const formatTokens = (n: number): string => {
|
|
@@ -40,6 +45,7 @@ export const isProactiveTriggerActive = () => proactiveTriggerActive;
|
|
|
40
45
|
export const resetProactiveState = () => {
|
|
41
46
|
lastCompactTime = 0;
|
|
42
47
|
proactiveTriggerActive = false;
|
|
48
|
+
clearCodexContextOverflowPending();
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
/**
|
|
@@ -105,6 +111,28 @@ const triggerCodexOutputLimitCompaction = (ctx: ProactiveContext) => {
|
|
|
105
111
|
ctx.compact?.({ customInstructions: CODEX_OUTPUT_LIMIT_COMPACT_INSTRUCTION });
|
|
106
112
|
};
|
|
107
113
|
|
|
114
|
+
/** Force compaction when Codex omits the model identity from an overflow error. */
|
|
115
|
+
const triggerCodexContextOverflowCompaction = (ctx: ProactiveContext) => {
|
|
116
|
+
if (isCoolingDown()) return;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
ctx?.ui?.notify?.(
|
|
120
|
+
"pi-vcc: Codex input exceeded the context window. Compacting...",
|
|
121
|
+
"info",
|
|
122
|
+
);
|
|
123
|
+
} catch {}
|
|
124
|
+
|
|
125
|
+
setCooldown();
|
|
126
|
+
proactiveTriggerActive = true;
|
|
127
|
+
ctx.compact?.({ customInstructions: CODEX_CONTEXT_OVERFLOW_COMPACT_INSTRUCTION });
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const hasCurrentModelIdentity = (message: unknown, model: any): boolean => {
|
|
131
|
+
if (!message || typeof message !== "object" || !model) return false;
|
|
132
|
+
const candidate = message as { model?: unknown; provider?: unknown };
|
|
133
|
+
return candidate.model === model.id && candidate.provider === model.provider;
|
|
134
|
+
};
|
|
135
|
+
|
|
108
136
|
const lastAssistantMessage = (event: unknown): unknown => {
|
|
109
137
|
const messages = (event as any)?.messages;
|
|
110
138
|
if (!Array.isArray(messages)) return undefined;
|
|
@@ -142,14 +170,34 @@ const lastAssistantMessage = (event: unknown): unknown => {
|
|
|
142
170
|
* "Compacting..." then "Skipped compaction" notifications.
|
|
143
171
|
*/
|
|
144
172
|
export const registerProactiveThresholdHook = (pi: ExtensionAPI) => {
|
|
145
|
-
// Codex reports
|
|
146
|
-
//
|
|
147
|
-
//
|
|
173
|
+
// Codex reports output-limit responses as errors instead of the standard
|
|
174
|
+
// "length" stop reason. Those errors have no usable context usage, so
|
|
175
|
+
// pi-core cannot discover the need to compact from its normal checks.
|
|
176
|
+
// Context-window errors are tracked separately because pi-core already
|
|
177
|
+
// recognizes and compacts those.
|
|
148
178
|
pi.on("agent_end", (event, ctx) => {
|
|
149
|
-
|
|
179
|
+
const lastMessage = lastAssistantMessage(event);
|
|
180
|
+
if (isCodexOutputLimitError(lastMessage)) {
|
|
150
181
|
triggerCodexOutputLimitCompaction(ctx);
|
|
151
182
|
return;
|
|
152
183
|
}
|
|
184
|
+
if (isCodexContextOverflowError(lastMessage)) {
|
|
185
|
+
markCodexContextOverflowPending();
|
|
186
|
+
// pi-core's overflow compaction only fires when compaction is enabled AND
|
|
187
|
+
// the assistant message carries the current model identity. If either
|
|
188
|
+
// is missing pi-core bails (or its LLM compaction would re-overflow), so
|
|
189
|
+
// pi-vcc must drive the recovery compaction itself via ctx.compact(),
|
|
190
|
+
// whose manual path skips the `enabled` gate and uses pi-vcc's static
|
|
191
|
+
// summary. When pi-core will handle it, defer to avoid a racing second
|
|
192
|
+
// compaction that would abort pi-core's own retry.
|
|
193
|
+
const piCoreWillHandle =
|
|
194
|
+
hasCurrentModelIdentity(lastMessage, ctx.model) &&
|
|
195
|
+
isPiCoreCompactionEnabled(ctx.sessionManager?.getCwd?.());
|
|
196
|
+
if (!piCoreWillHandle) {
|
|
197
|
+
triggerCodexContextOverflowCompaction(ctx);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
153
201
|
checkAndTrigger(ctx, "auto");
|
|
154
202
|
});
|
|
155
203
|
|
|
@@ -162,11 +210,13 @@ export const registerProactiveThresholdHook = (pi: ExtensionAPI) => {
|
|
|
162
210
|
pi.on("session_compact", () => {
|
|
163
211
|
setCooldown();
|
|
164
212
|
proactiveTriggerActive = false;
|
|
213
|
+
clearCodexContextOverflowPending();
|
|
165
214
|
});
|
|
166
215
|
|
|
167
216
|
// Reset state on session start so state doesn't leak between sessions
|
|
168
217
|
pi.on("session_start", () => {
|
|
169
218
|
lastCompactTime = 0;
|
|
170
219
|
proactiveTriggerActive = false;
|
|
220
|
+
clearCodexContextOverflowPending();
|
|
171
221
|
});
|
|
172
222
|
};
|