@howaboua/pi-codex-conversion 1.5.10 → 1.5.11-dev.46.3146d2f
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/CHANGELOG.md +7 -0
- package/README.md +5 -1
- package/package.json +5 -4
- package/src/adapter/codex-context-budget.ts +93 -0
- package/src/adapter/compaction.ts +118 -18
- package/src/adapter/payload-rewrite.ts +54 -9
- package/src/adapter/provider-request.ts +3 -1
- package/src/adapter/state.ts +14 -0
- package/src/adapter/types.ts +1 -1
- package/src/codex-settings/ui.ts +2 -1
- package/src/index.ts +11 -0
- package/src/providers/openai-codex-custom-provider.ts +110 -63
- package/vendor/apply-patch/win32-arm64/apply_patch.exe +0 -0
- package/vendor/apply-patch/win32-x64/apply_patch.exe +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.11
|
|
4
|
+
|
|
5
|
+
- Aligned the custom OpenAI Codex provider and Pi development dependencies with Pi 0.75.4.
|
|
6
|
+
- Added Codex context-budget alignment so Pi auto-compaction for OpenAI Codex subscription models triggers near Codex's native 90% compacting threshold.
|
|
7
|
+
- Improved native Responses compaction fallback: failed native compactions now fall back to Pi compaction, and reuse the previous native compacted window when available.
|
|
8
|
+
- Pruned low-value compatibility tests while keeping focused coverage for adapter activation, native tools, compaction fallback, and Codex context budgeting.
|
|
9
|
+
|
|
3
10
|
## 1.5.10
|
|
4
11
|
|
|
5
12
|
- Added `/codex usage` and a Usage tab for OpenAI Codex subscription limits, with automatic refresh and aligned 5-hour/weekly usage columns.
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ GPT/Codex models are strongest when the tool surface looks like the Codex CLI th
|
|
|
9
9
|
|
|
10
10
|
The point is to give the model tools it already knows how to use well: shell-first inspection, resumable command sessions, and large one-shot patch edits instead of piecemeal read/edit/write steps.
|
|
11
11
|
|
|
12
|
-
You can also opt into using the adapter on every provider/model. YMMV: Codex-tuned models are still the best fit, but the shell/patch workflow can help elsewhere too. The extension also has a small `/codex` settings UI for toggling adapter behavior, web search, image generation, fast mode, and verbosity. See [Settings](#settings).
|
|
12
|
+
You can also opt into using the adapter on every provider/model. YMMV: Codex-tuned models are still the best fit, but the shell/patch workflow can help elsewhere too. The extension also has a small `/codex` settings UI for toggling adapter behavior, web search, image generation, fast mode, native Responses compaction, and verbosity. See [Settings](#settings).
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -55,6 +55,10 @@ The settings UI also has **Usage**, **Overrides**, and **About** tabs. **Usage**
|
|
|
55
55
|
|
|
56
56
|
- add only the Pi `apply_patch` tool for GPT/Codex models while keeping Pi's default toolkit, prompt, provider behavior, and compaction flow
|
|
57
57
|
|
|
58
|
+
The **Compaction** tab can enable native OpenAI Responses compaction and choose the compaction model/reasoning. If native compaction fails, the extension falls back to Pi's normal compaction flow; when an older native compacted window exists, it is included in that Pi fallback summarization request so OpenAI can still use the prior opaque context server-side.
|
|
59
|
+
|
|
60
|
+
For OpenAI Codex subscription models, the extension also adjusts Pi's registered model context windows so Pi's fixed reserve-token compaction heuristic trips at roughly Codex's native auto-compact budget: 90% of Pi's resolved model window. This is calculated from Pi's current model metadata instead of hardcoded per-model limits.
|
|
61
|
+
|
|
58
62
|
When `all` is on, non-Codex providers get the shell, patch, skill, and prompt-adapter behavior, but keep their normal Pi provider path. Native web search, native image generation, and priority service tier stay limited to the OpenAI Codex provider. Verbosity is applied to Responses API providers.
|
|
59
63
|
|
|
60
64
|
The footer shows the active state, for example:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-codex-conversion",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.11-dev.46.3146d2f",
|
|
4
4
|
"description": "Codex-oriented tool and prompt adapter for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -60,9 +60,10 @@
|
|
|
60
60
|
"typebox": "*"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@earendil-works/pi-
|
|
64
|
-
"@earendil-works/pi-
|
|
65
|
-
"@earendil-works/pi-
|
|
63
|
+
"@earendil-works/pi-agent-core": "^0.75.4",
|
|
64
|
+
"@earendil-works/pi-ai": "^0.75.4",
|
|
65
|
+
"@earendil-works/pi-coding-agent": "^0.75.4",
|
|
66
|
+
"@earendil-works/pi-tui": "^0.75.4",
|
|
66
67
|
"tsx": "^4.20.5",
|
|
67
68
|
"typebox": "^1.1.24",
|
|
68
69
|
"typescript": "^5.9.3"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { AdapterState } from "./state.ts";
|
|
6
|
+
|
|
7
|
+
const OPENAI_CODEX_PROVIDER = "openai-codex";
|
|
8
|
+
const OPENAI_CODEX_API = "openai-codex-responses";
|
|
9
|
+
const CODEX_AUTO_COMPACT_NUMERATOR = 9;
|
|
10
|
+
const CODEX_AUTO_COMPACT_DENOMINATOR = 10;
|
|
11
|
+
const PI_DEFAULT_COMPACTION_RESERVE_TOKENS = 16_384;
|
|
12
|
+
|
|
13
|
+
export function getCodexAutoCompactBudget(contextWindow: number): number {
|
|
14
|
+
return Math.floor((contextWindow * CODEX_AUTO_COMPACT_NUMERATOR) / CODEX_AUTO_COMPACT_DENOMINATOR);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getPiContextWindowForCodexAutoCompact(contextWindow: number, reserveTokens = PI_DEFAULT_COMPACTION_RESERVE_TOKENS): number {
|
|
18
|
+
return Math.min(contextWindow, getCodexAutoCompactBudget(contextWindow) + reserveTokens);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readSettings(path: string): Record<string, unknown> | undefined {
|
|
22
|
+
if (!existsSync(path)) return undefined;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
25
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getReserveTokens(settings: Record<string, unknown> | undefined): number | undefined {
|
|
32
|
+
const compaction = settings?.compaction;
|
|
33
|
+
if (!compaction || typeof compaction !== "object" || Array.isArray(compaction)) return undefined;
|
|
34
|
+
const reserveTokens = (compaction as { reserveTokens?: unknown }).reserveTokens;
|
|
35
|
+
return typeof reserveTokens === "number" && Number.isFinite(reserveTokens) && reserveTokens >= 0 ? reserveTokens : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readPiCompactionReserveTokens(cwd: string): number {
|
|
39
|
+
return (
|
|
40
|
+
getReserveTokens(readSettings(join(cwd, ".pi", "settings.json"))) ??
|
|
41
|
+
getReserveTokens(readSettings(join(getAgentDir(), "settings.json"))) ??
|
|
42
|
+
PI_DEFAULT_COMPACTION_RESERVE_TOKENS
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function shouldUseCodexContextBudgetModel(model: Model<Api> | undefined): model is Model<Api> {
|
|
47
|
+
return model?.provider === OPENAI_CODEX_PROVIDER && model.api === OPENAI_CODEX_API;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getModelKey(model: Model<Api>): string {
|
|
51
|
+
return `${model.provider}:${model.api}:${model.id}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveRawContextWindow<TApi extends Api>(model: Model<TApi>, state: AdapterState | undefined): number {
|
|
55
|
+
if (!state) return model.contextWindow;
|
|
56
|
+
const key = getModelKey(model);
|
|
57
|
+
const cachedRaw = state.codexContextBudgetRawWindows?.[key];
|
|
58
|
+
if (cachedRaw === undefined) {
|
|
59
|
+
state.codexContextBudgetRawWindows ??= {};
|
|
60
|
+
state.codexContextBudgetRawWindows[key] = model.contextWindow;
|
|
61
|
+
return model.contextWindow;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const cachedAdjusted = getPiContextWindowForCodexAutoCompact(cachedRaw, state.codexContextBudgetReserveTokens);
|
|
65
|
+
const previousAdjusted = state.codexContextBudgetAdjustedWindows?.[key];
|
|
66
|
+
if (model.contextWindow !== cachedRaw && model.contextWindow !== cachedAdjusted && model.contextWindow !== previousAdjusted) {
|
|
67
|
+
state.codexContextBudgetRawWindows ??= {};
|
|
68
|
+
state.codexContextBudgetRawWindows[key] = model.contextWindow;
|
|
69
|
+
delete state.codexContextBudgetAdjustedWindows?.[key];
|
|
70
|
+
return model.contextWindow;
|
|
71
|
+
}
|
|
72
|
+
return cachedRaw;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getCodexContextBudgetAdjustedModel<TApi extends Api>(model: Model<TApi>, state?: AdapterState): Model<TApi> {
|
|
76
|
+
if (!shouldUseCodexContextBudgetModel(model)) return model;
|
|
77
|
+
const rawContextWindow = resolveRawContextWindow(model, state);
|
|
78
|
+
const contextWindow = getPiContextWindowForCodexAutoCompact(rawContextWindow, state?.codexContextBudgetReserveTokens);
|
|
79
|
+
if (state) {
|
|
80
|
+
state.codexContextBudgetAdjustedWindows ??= {};
|
|
81
|
+
state.codexContextBudgetAdjustedWindows[getModelKey(model)] = contextWindow;
|
|
82
|
+
}
|
|
83
|
+
return contextWindow === model.contextWindow ? model : { ...model, contextWindow };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function applyCodexContextBudgetToModel<TApi extends Api>(model: Model<TApi> | undefined, state: AdapterState): void {
|
|
87
|
+
if (!model) return;
|
|
88
|
+
state.codexContextBudgetReserveTokens ??= readPiCompactionReserveTokens(state.cwd);
|
|
89
|
+
const adjustedModel = getCodexContextBudgetAdjustedModel(model, state);
|
|
90
|
+
if (adjustedModel !== model) {
|
|
91
|
+
model.contextWindow = adjustedModel.contextWindow;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -2,9 +2,9 @@ import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent } from "
|
|
|
2
2
|
import { clampThinkingLevel, type ModelThinkingLevel, type Tool } from "@earendil-works/pi-ai";
|
|
3
3
|
import { executeNativeCompaction } from "./compact-client.ts";
|
|
4
4
|
import { extractCompactionSummaryText, hasCompactionOutputItem, sanitizeCompactedWindow, summarizeCompactionOutputForDiagnostics } from "./compaction-output.ts";
|
|
5
|
-
import { resolveLatestNativeCompactionEntry } from "./details-store.ts";
|
|
5
|
+
import { findLatestNativeCompactionEntry, findLatestNativeCompactionEntryIndex, resolveLatestNativeCompactionEntry } from "./details-store.ts";
|
|
6
6
|
import { rewriteResponsesPayloadWithNativeReplay, serializeLiveTailToResponsesInput } from "./payload-rewrite.ts";
|
|
7
|
-
import { resolveNativeCompactionEnvironment } from "./compaction-runtime.ts";
|
|
7
|
+
import { isResponsesCompatiblePayload, resolveNativeCompactionEnvironment, type ResponsesCompatibleRequestPayload } from "./compaction-runtime.ts";
|
|
8
8
|
import { convertResponsesTools } from "../providers/openai-responses-shared.ts";
|
|
9
9
|
import {
|
|
10
10
|
serializeCompactionPreparationToRequest,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type NativeCompactionRequestOptions,
|
|
13
13
|
type ResponsesInputItem,
|
|
14
14
|
} from "./serializer.ts";
|
|
15
|
-
import { createNativeCompactionDetails, createNativeCompactionShimResult, isNativeCompactionDetails, NATIVE_COMPACTION_SHIM_SUMMARY } from "./types.ts";
|
|
15
|
+
import { createNativeCompactionDetails, createNativeCompactionShimResult, isNativeCompactionDetails, NATIVE_COMPACTION_SHIM_SUMMARY, type NativeCompactionEntry } from "./types.ts";
|
|
16
16
|
import { isOpenAICodexContext, isResponsesContext } from "./codex-model.ts";
|
|
17
17
|
import { shouldUseCodexAdapter } from "./activation.ts";
|
|
18
18
|
import type { AdapterState } from "./state.ts";
|
|
@@ -23,6 +23,31 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
23
23
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function stashLatestNativeWindowForPiCompactionFallback(
|
|
27
|
+
ctx: ExtensionContext,
|
|
28
|
+
branchEntries: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>,
|
|
29
|
+
runtime: { provider: string; api: string; baseUrl: string },
|
|
30
|
+
state: AdapterState,
|
|
31
|
+
): boolean {
|
|
32
|
+
state.pendingPiCompactionNativeWindow = undefined;
|
|
33
|
+
const nativeEntry = findLatestNativeCompactionEntry(branchEntries, {
|
|
34
|
+
provider: runtime.provider,
|
|
35
|
+
api: runtime.api,
|
|
36
|
+
baseUrl: runtime.baseUrl,
|
|
37
|
+
});
|
|
38
|
+
const compactedWindow = cloneCompactedWindow(nativeEntry?.details?.compactedWindow ?? []);
|
|
39
|
+
if (!compactedWindow || compactedWindow.length === 0) return false;
|
|
40
|
+
state.pendingPiCompactionNativeWindow = {
|
|
41
|
+
window: compactedWindow,
|
|
42
|
+
provider: runtime.provider,
|
|
43
|
+
api: runtime.api,
|
|
44
|
+
baseUrl: runtime.baseUrl,
|
|
45
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
46
|
+
sourceCompactionEntryId: nativeEntry?.id,
|
|
47
|
+
};
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
function cloneCompactedWindow(window: readonly unknown[]): ResponsesInputItem[] | undefined {
|
|
27
52
|
if (!window.every(isRecord)) return undefined;
|
|
28
53
|
return window.map((item) => structuredClone(item));
|
|
@@ -70,12 +95,20 @@ function clampCodexReasoningEffort(modelId: string, effort: string): string {
|
|
|
70
95
|
return effort;
|
|
71
96
|
}
|
|
72
97
|
|
|
98
|
+
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
99
|
+
|
|
100
|
+
function clampOpenAIPromptCacheKey(key: string): string {
|
|
101
|
+
const chars = Array.from(key);
|
|
102
|
+
if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return key;
|
|
103
|
+
return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
|
|
104
|
+
}
|
|
105
|
+
|
|
73
106
|
function buildCompactionRequestOptions(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState, compactionModel: string): NativeCompactionRequestOptions {
|
|
74
107
|
const tools = buildCompactionTools(pi, ctx, state);
|
|
75
108
|
const reasoning = buildCompactionReasoning(pi, ctx, state, compactionModel);
|
|
76
109
|
return {
|
|
77
110
|
parallel_tool_calls: true,
|
|
78
|
-
prompt_cache_key: ctx.sessionManager.getSessionId(),
|
|
111
|
+
prompt_cache_key: clampOpenAIPromptCacheKey(ctx.sessionManager.getSessionId()),
|
|
79
112
|
...(isOpenAICodexContext(ctx) && state.config.fast ? { service_tier: "priority" } : {}),
|
|
80
113
|
text: { verbosity: state.config.verbosity },
|
|
81
114
|
...(tools ? { tools } : {}),
|
|
@@ -94,7 +127,7 @@ function formatCompactFailureMessage(compactResult: Awaited<ReturnType<typeof ex
|
|
|
94
127
|
const status = compactResult.status ? ` HTTP ${compactResult.status}` : "";
|
|
95
128
|
const response = compactResult.responseText?.trim();
|
|
96
129
|
const detail = response ? `: ${response.slice(0, 500)}` : compactResult.errorMessage ? `: ${compactResult.errorMessage}` : "";
|
|
97
|
-
return `OpenAI native compaction failed (${compactResult.reason}${status})${detail}
|
|
130
|
+
return `OpenAI native compaction failed (${compactResult.reason}${status})${detail}`;
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
function formatCompactRequestDiagnostics(request: NativeCompactionRequestBody): string {
|
|
@@ -104,6 +137,33 @@ function formatCompactRequestDiagnostics(request: NativeCompactionRequestBody):
|
|
|
104
137
|
return `model=${request.model}, input=${request.input.length}, tools=${tools}, reasoning=${reasoning}, service_tier=${serviceTier}`;
|
|
105
138
|
}
|
|
106
139
|
|
|
140
|
+
function notifyNativeCompactionFallback(ctx: ExtensionContext, state: AdapterState, branchEntries: ReturnType<ExtensionContext["sessionManager"]["getBranch"]>, runtime: { provider: string; api: string; baseUrl: string }, message: string): void {
|
|
141
|
+
const stashed = stashLatestNativeWindowForPiCompactionFallback(ctx, branchEntries, runtime, state);
|
|
142
|
+
ctx.ui.notify(`${message}; Pi compaction will run.${stashed ? " Previous native compacted window will be included in Pi compaction fallback." : ""}`, "error");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function textFromResponsesContent(content: unknown): string {
|
|
146
|
+
if (typeof content === "string") return content;
|
|
147
|
+
if (!Array.isArray(content)) return "";
|
|
148
|
+
return content
|
|
149
|
+
.map((item) => isRecord(item) && item.type === "input_text" && typeof item.text === "string" ? item.text : "")
|
|
150
|
+
.join("\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isPiCompactionSummarizationPayload(payload: ResponsesCompatibleRequestPayload): boolean {
|
|
154
|
+
const instructions = typeof payload.instructions === "string" ? payload.instructions : "";
|
|
155
|
+
if (/compact|summar/i.test(instructions)) return true;
|
|
156
|
+
|
|
157
|
+
return payload.input.some((item) => {
|
|
158
|
+
if (!isRecord(item)) return false;
|
|
159
|
+
const role = item.role;
|
|
160
|
+
const text = textFromResponsesContent(item.content);
|
|
161
|
+
if ((role === "system" || role === "developer") && /compact|summar/i.test(text)) return true;
|
|
162
|
+
if (role === "user" && /<conversation>|previous compaction summary|summary/i.test(text)) return true;
|
|
163
|
+
return false;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
107
167
|
export async function handleCodexSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx: ExtensionContext, state: AdapterState, pi: ExtensionAPI) {
|
|
108
168
|
if (!state.config.responsesCompaction || !shouldUseCodexAdapter(ctx, state.config)) {
|
|
109
169
|
return undefined;
|
|
@@ -127,6 +187,9 @@ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactE
|
|
|
127
187
|
|
|
128
188
|
const resolution = await resolveNativeCompactionEnvironment(ctx, { enabled: true });
|
|
129
189
|
if (!resolution.ok) {
|
|
190
|
+
if (resolution.reason === "unsupported-provider" || resolution.reason === "unsupported-api") {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
130
193
|
ctx.ui.notify(`OpenAI native compaction is enabled but unavailable (${resolution.reason}); Pi compaction was not run.`, "error");
|
|
131
194
|
return { cancel: true };
|
|
132
195
|
}
|
|
@@ -203,23 +266,23 @@ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactE
|
|
|
203
266
|
const compactResult = await executeNativeCompaction({ runtime, request, signal: event.signal });
|
|
204
267
|
if (!compactResult.ok) {
|
|
205
268
|
if (compactResult.reason !== "aborted") {
|
|
206
|
-
ctx
|
|
269
|
+
notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, formatCompactFailureMessage(compactResult));
|
|
207
270
|
}
|
|
208
|
-
return { cancel: true };
|
|
271
|
+
return compactResult.reason === "aborted" ? { cancel: true } : undefined;
|
|
209
272
|
}
|
|
210
273
|
const compactedWindow = sanitizeCompactedWindow(compactResult.compactedWindow);
|
|
211
274
|
if (compactedWindow.length === 0) {
|
|
212
|
-
ctx
|
|
213
|
-
return
|
|
275
|
+
notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, `OpenAI native compaction returned no installable compacted context. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`);
|
|
276
|
+
return undefined;
|
|
214
277
|
}
|
|
215
278
|
if (!hasCompactionOutputItem(compactedWindow)) {
|
|
216
|
-
ctx
|
|
217
|
-
return
|
|
279
|
+
notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, `OpenAI native compaction did not return a compaction item. Response=${compactResult.compactResponseId ?? "<none>"}. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`);
|
|
280
|
+
return undefined;
|
|
218
281
|
}
|
|
219
282
|
const encryptedSummary = extractCompactionSummaryText(compactedWindow);
|
|
220
283
|
if (!encryptedSummary) {
|
|
221
|
-
ctx
|
|
222
|
-
return
|
|
284
|
+
notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, `OpenAI native compaction returned compacted context without a displayable summary. Response=${compactResult.compactResponseId ?? "<none>"}. Request: ${formatCompactRequestDiagnostics(request)}. Output: ${summarizeCompactionOutputForDiagnostics(compactResult.compactedWindow, compactedWindow)}`);
|
|
285
|
+
return undefined;
|
|
223
286
|
}
|
|
224
287
|
try {
|
|
225
288
|
const details = createNativeCompactionDetails({
|
|
@@ -234,8 +297,8 @@ async function handleCodexSessionBeforeCompactInner(event: SessionBeforeCompactE
|
|
|
234
297
|
});
|
|
235
298
|
return { compaction: createNativeCompactionShimResult({ summary: NATIVE_COMPACTION_SHIM_SUMMARY, firstKeptEntryId: event.preparation.firstKeptEntryId, tokensBefore: event.preparation.tokensBefore, details }) };
|
|
236
299
|
} catch {
|
|
237
|
-
ctx
|
|
238
|
-
return
|
|
300
|
+
notifyNativeCompactionFallback(ctx, state, branchEntries, runtime, "OpenAI native compaction produced details Pi could not store");
|
|
301
|
+
return undefined;
|
|
239
302
|
}
|
|
240
303
|
}
|
|
241
304
|
|
|
@@ -245,17 +308,54 @@ export async function rewriteCodexCompactedProviderRequest(payload: unknown, ctx
|
|
|
245
308
|
if (!resolution.ok) return undefined;
|
|
246
309
|
const runtime = resolution.runtime;
|
|
247
310
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
248
|
-
const
|
|
311
|
+
const latestNativeCompactionIndex = findLatestNativeCompactionEntryIndex(branchEntries, {
|
|
249
312
|
provider: runtime.provider,
|
|
250
313
|
api: runtime.api,
|
|
251
314
|
baseUrl: runtime.baseUrl,
|
|
252
315
|
});
|
|
253
|
-
if (
|
|
316
|
+
if (latestNativeCompactionIndex === undefined) return undefined;
|
|
254
317
|
if (!runtime.payload) return undefined;
|
|
255
|
-
const rewrite = rewriteResponsesPayloadWithNativeReplay({ model: runtime.currentModel, payload: runtime.payload, branchEntries, compactionEntry:
|
|
318
|
+
const rewrite = rewriteResponsesPayloadWithNativeReplay({ model: runtime.currentModel, payload: runtime.payload, branchEntries, compactionEntry: branchEntries[latestNativeCompactionIndex] as NativeCompactionEntry });
|
|
256
319
|
if (rewrite.ok) return rewrite.rewrittenPayload;
|
|
257
320
|
const detail = rewrite.parity?.mismatches.slice(0, 3).join("; ");
|
|
258
321
|
const message = `OpenAI native compaction replay failed (${rewrite.reason})${detail ? `: ${detail}` : ""}; request was not sent with placeholder compaction context.`;
|
|
259
322
|
ctx.ui.notify(message, "error");
|
|
260
323
|
throw new Error(message);
|
|
261
324
|
}
|
|
325
|
+
|
|
326
|
+
export async function injectPendingNativeWindowIntoPiCompactionRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): Promise<unknown | undefined> {
|
|
327
|
+
const pending = state.pendingPiCompactionNativeWindow;
|
|
328
|
+
if (!pending || pending.window.length === 0) return undefined;
|
|
329
|
+
if (!isResponsesCompatiblePayload(payload)) return undefined;
|
|
330
|
+
if (pending.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
331
|
+
state.pendingPiCompactionNativeWindow = undefined;
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
if (!isPiCompactionSummarizationPayload(payload)) return undefined;
|
|
335
|
+
|
|
336
|
+
const resolution = await resolveNativeCompactionEnvironment(ctx, { enabled: true }, payload);
|
|
337
|
+
if (!resolution.ok) return undefined;
|
|
338
|
+
const runtime = resolution.runtime;
|
|
339
|
+
if (pending.provider !== runtime.provider || pending.api !== runtime.api || pending.baseUrl !== runtime.baseUrl) {
|
|
340
|
+
state.pendingPiCompactionNativeWindow = undefined;
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const input = [...payload.input];
|
|
345
|
+
let insertAt = 0;
|
|
346
|
+
while (insertAt < input.length) {
|
|
347
|
+
const item = input[insertAt];
|
|
348
|
+
if (!isRecord(item) || (item.role !== "system" && item.role !== "developer")) break;
|
|
349
|
+
insertAt++;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
state.pendingPiCompactionNativeWindow = undefined;
|
|
353
|
+
return {
|
|
354
|
+
...payload,
|
|
355
|
+
input: [
|
|
356
|
+
...input.slice(0, insertAt),
|
|
357
|
+
...pending.window.map((item) => structuredClone(item)),
|
|
358
|
+
...input.slice(insertAt),
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -390,6 +390,15 @@ function createReplayVariants<TApi extends Api>(args: {
|
|
|
390
390
|
return [contextSet, createReplayMessageSet(args.model, piMessages)];
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
function clonePayloadConversationInput(args: {
|
|
394
|
+
payloadInput: readonly unknown[];
|
|
395
|
+
freshPreamble: FreshAuthoritativePreamble;
|
|
396
|
+
}): ResponsesInputItem[] | undefined {
|
|
397
|
+
const tailEndIndex = args.payloadInput.length - args.freshPreamble.trailingInput.length;
|
|
398
|
+
if (tailEndIndex < args.freshPreamble.leadingInput.length) return undefined;
|
|
399
|
+
return cloneResponsesInputSlice(args.payloadInput.slice(args.freshPreamble.leadingInput.length, tailEndIndex));
|
|
400
|
+
}
|
|
401
|
+
|
|
393
402
|
function findReplayMatch<TApi extends Api>(args: {
|
|
394
403
|
model: Model<TApi>;
|
|
395
404
|
payloadInput: readonly unknown[];
|
|
@@ -506,21 +515,57 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
506
515
|
};
|
|
507
516
|
}
|
|
508
517
|
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
.some((entry) => entry.type === "compaction");
|
|
512
|
-
if (newerCompactionEntry) {
|
|
518
|
+
const compactedWindow = cloneOpaqueCompactedWindow(args.compactionEntry.details?.compactedWindow ?? []);
|
|
519
|
+
if (!compactedWindow) {
|
|
513
520
|
return {
|
|
514
521
|
ok: false,
|
|
515
|
-
reason: "
|
|
522
|
+
reason: "invalid-compacted-window",
|
|
516
523
|
};
|
|
517
524
|
}
|
|
518
525
|
|
|
519
|
-
const
|
|
520
|
-
|
|
526
|
+
const newerCompactionEntry = args.branchEntries
|
|
527
|
+
.slice(boundaryIndex + 1)
|
|
528
|
+
.some((entry) => entry.type === "compaction");
|
|
529
|
+
if (newerCompactionEntry) {
|
|
530
|
+
const conversationInput = clonePayloadConversationInput({ payloadInput: args.payload.input, freshPreamble });
|
|
531
|
+
const originalPiReplayInput = cloneResponsesInputSlice(args.payload.input);
|
|
532
|
+
if (!conversationInput || !originalPiReplayInput) {
|
|
533
|
+
return {
|
|
534
|
+
ok: false,
|
|
535
|
+
reason: "unexpected-compaction-after-boundary",
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
521
539
|
return {
|
|
522
|
-
ok:
|
|
523
|
-
|
|
540
|
+
ok: true,
|
|
541
|
+
segments: {
|
|
542
|
+
boundaryIndex,
|
|
543
|
+
firstKeptEntryIndex,
|
|
544
|
+
instructions: freshPreamble.instructions,
|
|
545
|
+
freshPreamble: freshPreamble.leadingInput,
|
|
546
|
+
trailingPreamble: freshPreamble.trailingInput,
|
|
547
|
+
compactionSummary: [],
|
|
548
|
+
preCompactionKeptWindow: createReplaySlice([], [], []),
|
|
549
|
+
compactedWindow,
|
|
550
|
+
postCompactionTail: createReplaySlice(args.branchEntries.slice(boundaryIndex + 1), [], conversationInput),
|
|
551
|
+
originalPiReplayInput,
|
|
552
|
+
replayInput: [
|
|
553
|
+
...freshPreamble.leadingInput,
|
|
554
|
+
...compactedWindow,
|
|
555
|
+
...conversationInput,
|
|
556
|
+
...freshPreamble.trailingInput,
|
|
557
|
+
],
|
|
558
|
+
},
|
|
559
|
+
rewrittenPayload: {
|
|
560
|
+
...args.payload,
|
|
561
|
+
...(freshPreamble.instructions !== undefined ? { instructions: freshPreamble.instructions } : {}),
|
|
562
|
+
input: [
|
|
563
|
+
...freshPreamble.leadingInput,
|
|
564
|
+
...compactedWindow,
|
|
565
|
+
...conversationInput,
|
|
566
|
+
...freshPreamble.trailingInput,
|
|
567
|
+
],
|
|
568
|
+
},
|
|
524
569
|
};
|
|
525
570
|
}
|
|
526
571
|
|
|
@@ -5,7 +5,7 @@ import type { AdapterState } from "./state.ts";
|
|
|
5
5
|
import { rewriteNativeImageGenerationTool } from "../tools/image-generation-tool.ts";
|
|
6
6
|
import { rewriteNativeWebSearchTool } from "../tools/web-search-tool.ts";
|
|
7
7
|
import { shouldUseCodexAdapter } from "./activation.ts";
|
|
8
|
-
import { rewriteCodexCompactedProviderRequest } from "./compaction.ts";
|
|
8
|
+
import { injectPendingNativeWindowIntoPiCompactionRequest, rewriteCodexCompactedProviderRequest } from "./compaction.ts";
|
|
9
9
|
|
|
10
10
|
export async function rewriteCodexProviderRequest(payload: unknown, ctx: ExtensionContext, state: AdapterState): Promise<unknown | undefined> {
|
|
11
11
|
if (!shouldUseCodexAdapter(ctx, state.config) || (!isOpenAICodexContext(ctx) && !isResponsesContext(ctx))) {
|
|
@@ -21,5 +21,7 @@ export async function rewriteCodexProviderRequest(payload: unknown, ctx: Extensi
|
|
|
21
21
|
serviceTier: isOpenAICodex,
|
|
22
22
|
verbosity: true,
|
|
23
23
|
});
|
|
24
|
+
const piCompactionPayload = await injectPendingNativeWindowIntoPiCompactionRequest(configuredPayload, ctx, state);
|
|
25
|
+
if (piCompactionPayload !== undefined) return piCompactionPayload;
|
|
24
26
|
return (await rewriteCodexCompactedProviderRequest(configuredPayload, ctx, state)) ?? configuredPayload;
|
|
25
27
|
}
|
package/src/adapter/state.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { PromptSkill } from "../prompt/build-system-prompt.ts";
|
|
2
2
|
import type { CodexConversionConfig } from "./config.ts";
|
|
3
|
+
import type { ResponsesInputItem } from "./serializer.ts";
|
|
4
|
+
|
|
5
|
+
export interface PendingPiCompactionNativeWindow {
|
|
6
|
+
window: ResponsesInputItem[];
|
|
7
|
+
provider: string;
|
|
8
|
+
api: string;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
sourceCompactionEntryId?: string;
|
|
12
|
+
}
|
|
3
13
|
|
|
4
14
|
export interface AdapterState {
|
|
5
15
|
enabled: boolean;
|
|
@@ -8,4 +18,8 @@ export interface AdapterState {
|
|
|
8
18
|
previousToolNames?: string[];
|
|
9
19
|
promptSkills: PromptSkill[];
|
|
10
20
|
config: CodexConversionConfig;
|
|
21
|
+
pendingPiCompactionNativeWindow?: PendingPiCompactionNativeWindow;
|
|
22
|
+
codexContextBudgetRawWindows?: Record<string, number>;
|
|
23
|
+
codexContextBudgetAdjustedWindows?: Record<string, number>;
|
|
24
|
+
codexContextBudgetReserveTokens?: number;
|
|
11
25
|
}
|
package/src/adapter/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const NATIVE_COMPACTION_DISPLAY_TEXT = [
|
|
|
9
9
|
"",
|
|
10
10
|
"The compaction result is encrypted by OpenAI and is not human-readable in Pi.",
|
|
11
11
|
"",
|
|
12
|
-
"Warning: do not turn Responses compaction off mid-session; old context may be much less reliable.",
|
|
12
|
+
"Warning: do not turn Responses compaction off or switch providers mid-session; old context may be much less reliable.",
|
|
13
13
|
].join("\n");
|
|
14
14
|
|
|
15
15
|
export type NativeCompactionStrategy = typeof NATIVE_COMPACTION_STRATEGY;
|
package/src/codex-settings/ui.ts
CHANGED
|
@@ -92,7 +92,8 @@ export async function openCodexSettingsScreen(ctx: ExtensionContext, options: Co
|
|
|
92
92
|
function formatCompactionNotes(theme: Theme): string[] {
|
|
93
93
|
return [
|
|
94
94
|
theme.fg("dim", " Beta: native OpenAI Responses compaction is experimental. Please report any issues."),
|
|
95
|
-
theme.fg("error", " Warning: do not turn this off mid-session; old context may be much less reliable."),
|
|
95
|
+
theme.fg("error", " Warning: do not turn this off or switch providers mid-session; old context may be much less reliable."),
|
|
96
|
+
theme.fg("warning", " If native compaction recovery fails, go back below 90% context and compact from there."),
|
|
96
97
|
];
|
|
97
98
|
}
|
|
98
99
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
2
3
|
import { Box, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
4
|
import { getCodexRuntimeShell } from "./adapter/runtime-shell.ts";
|
|
4
5
|
import { clearApplyPatchRenderState, registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
|
|
@@ -22,6 +23,7 @@ import { getCodexSkillPaths, hasNoSkillsFlag } from "./adapter/skills.ts";
|
|
|
22
23
|
import type { AdapterState } from "./adapter/state.ts";
|
|
23
24
|
import { registerCodexCommand } from "./codex-settings/command.ts";
|
|
24
25
|
import { WEB_SEARCH_TOOL_NAME } from "./adapter/tool-set.ts";
|
|
26
|
+
import { applyCodexContextBudgetToModel, readPiCompactionReserveTokens } from "./adapter/codex-context-budget.ts";
|
|
25
27
|
|
|
26
28
|
function getCommandArg(args: unknown): string | undefined {
|
|
27
29
|
if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
|
|
@@ -62,6 +64,10 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function ensureCodexContextBudgetModel(ctx: { model: Model<any> | undefined }): void {
|
|
68
|
+
applyCodexContextBudgetToModel(ctx.model, state);
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
registerOpenAICodexCustomProvider(pi, {
|
|
66
72
|
getCurrentCwd: () => state.cwd,
|
|
67
73
|
getNativeToolRewriteConfig: () => ({
|
|
@@ -92,6 +98,8 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
92
98
|
pi.on("session_start", async (_event, ctx) => {
|
|
93
99
|
state.cwd = ctx.cwd;
|
|
94
100
|
state.config = readCodexConversionConfig();
|
|
101
|
+
state.codexContextBudgetReserveTokens = readPiCompactionReserveTokens(ctx.cwd);
|
|
102
|
+
ensureCodexContextBudgetModel(ctx);
|
|
95
103
|
ensureOptionalNativeToolsRegistered();
|
|
96
104
|
state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
|
|
97
105
|
registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
|
|
@@ -108,6 +116,8 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
108
116
|
|
|
109
117
|
pi.on("model_select", async (_event, ctx) => {
|
|
110
118
|
state.cwd = ctx.cwd;
|
|
119
|
+
state.codexContextBudgetReserveTokens = readPiCompactionReserveTokens(ctx.cwd);
|
|
120
|
+
ensureCodexContextBudgetModel(ctx);
|
|
111
121
|
state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
|
|
112
122
|
registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
|
|
113
123
|
syncAdapter(pi, ctx, state);
|
|
@@ -163,6 +173,7 @@ export default function codexConversion(pi: ExtensionAPI) {
|
|
|
163
173
|
});
|
|
164
174
|
|
|
165
175
|
pi.on("session_compact", async (event) => {
|
|
176
|
+
state.pendingPiCompactionNativeWindow = undefined;
|
|
166
177
|
if (!event.fromExtension || !isNativeCompactionDetails(event.compactionEntry.details)) return;
|
|
167
178
|
pi.sendMessage(
|
|
168
179
|
{
|
|
@@ -37,6 +37,14 @@ const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
|
|
|
37
37
|
const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
|
|
38
38
|
const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
39
39
|
const dynamicImport = (specifier: string) => import(specifier);
|
|
40
|
+
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
41
|
+
|
|
42
|
+
function clampOpenAIPromptCacheKey(key: string | undefined): string | undefined {
|
|
43
|
+
if (key === undefined) return undefined;
|
|
44
|
+
const chars = Array.from(key);
|
|
45
|
+
if (chars.length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return key;
|
|
46
|
+
return chars.slice(0, OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH).join("");
|
|
47
|
+
}
|
|
40
48
|
let _os: { platform(): string; release(): string; arch(): string } | null = null;
|
|
41
49
|
|
|
42
50
|
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
|
|
@@ -87,6 +95,7 @@ interface QueuedWebSearchActivity {
|
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
type PendingActivity = QueuedImageActivity | QueuedWebSearchActivity;
|
|
98
|
+
type SendActivityMessage = ExtensionAPI["sendMessage"];
|
|
90
99
|
|
|
91
100
|
interface CachedImagePreview {
|
|
92
101
|
data: string;
|
|
@@ -577,7 +586,7 @@ export function buildRequestBody<TApi extends Api>(model: Model<TApi>, context:
|
|
|
577
586
|
input: messages,
|
|
578
587
|
text: { verbosity: ((options as { textVerbosity?: string } | undefined)?.textVerbosity ?? "low") as string },
|
|
579
588
|
include: ["reasoning.encrypted_content"],
|
|
580
|
-
prompt_cache_key: options?.sessionId,
|
|
589
|
+
prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId),
|
|
581
590
|
tool_choice: "auto",
|
|
582
591
|
parallel_tool_calls: true,
|
|
583
592
|
};
|
|
@@ -1449,6 +1458,87 @@ export function buildWebSearchSummaryText(searches: SurfacedWebSearch[]): string
|
|
|
1449
1458
|
return searches.length === 1 ? "Searched the web once" : `Searched the web ${searches.length} times`;
|
|
1450
1459
|
}
|
|
1451
1460
|
|
|
1461
|
+
function sendActivityMessages(
|
|
1462
|
+
sendMessage: SendActivityMessage,
|
|
1463
|
+
imagePreviewCache: Map<string, CachedImagePreview>,
|
|
1464
|
+
activities: PendingActivity[],
|
|
1465
|
+
): void {
|
|
1466
|
+
for (let index = 0; index < activities.length; index++) {
|
|
1467
|
+
const activity = activities[index];
|
|
1468
|
+
if (activity.kind === "image") {
|
|
1469
|
+
imagePreviewCache.set(activity.savedImage.absolutePath, activity.imageData);
|
|
1470
|
+
sendMessage(
|
|
1471
|
+
{
|
|
1472
|
+
customType: IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
|
|
1473
|
+
content: [{ type: "text", text: buildGeneratedImageDisplayText(activity.savedImage, { expanded: false }) }],
|
|
1474
|
+
display: true,
|
|
1475
|
+
details: { savedImages: [activity.savedImage] } satisfies ImageDisplayMessageDetails,
|
|
1476
|
+
},
|
|
1477
|
+
{ triggerTurn: false },
|
|
1478
|
+
);
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const searches = [activity.search];
|
|
1483
|
+
while (index + 1 < activities.length && activities[index + 1]?.kind === "web-search") {
|
|
1484
|
+
searches.push((activities[++index] as QueuedWebSearchActivity).search);
|
|
1485
|
+
}
|
|
1486
|
+
sendMessage(
|
|
1487
|
+
{
|
|
1488
|
+
customType: WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
|
|
1489
|
+
content: buildWebSearchActivityMessage(searches),
|
|
1490
|
+
display: true,
|
|
1491
|
+
details: { searches },
|
|
1492
|
+
},
|
|
1493
|
+
{ triggerTurn: false },
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
export function createActivityMessageDispatcher(sendMessage: SendActivityMessage): {
|
|
1499
|
+
imagePreviewCache: Map<string, CachedImagePreview>;
|
|
1500
|
+
enqueueSettledActivities(activities: PendingActivity[]): void;
|
|
1501
|
+
flushNow(): void;
|
|
1502
|
+
scheduleFlush(): void;
|
|
1503
|
+
clear(): void;
|
|
1504
|
+
} {
|
|
1505
|
+
const completedActivities: PendingActivity[] = [];
|
|
1506
|
+
const imagePreviewCache = new Map<string, CachedImagePreview>();
|
|
1507
|
+
let pendingFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1508
|
+
|
|
1509
|
+
const flush = () => {
|
|
1510
|
+
pendingFlushTimer = undefined;
|
|
1511
|
+
const activities = completedActivities.splice(0, completedActivities.length);
|
|
1512
|
+
if (activities.length > 0) sendActivityMessages(sendMessage, imagePreviewCache, activities);
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
return {
|
|
1516
|
+
imagePreviewCache,
|
|
1517
|
+
enqueueSettledActivities(activities) {
|
|
1518
|
+
completedActivities.push(...activities);
|
|
1519
|
+
},
|
|
1520
|
+
flushNow() {
|
|
1521
|
+
if (pendingFlushTimer) {
|
|
1522
|
+
clearTimeout(pendingFlushTimer);
|
|
1523
|
+
pendingFlushTimer = undefined;
|
|
1524
|
+
}
|
|
1525
|
+
flush();
|
|
1526
|
+
},
|
|
1527
|
+
scheduleFlush() {
|
|
1528
|
+
if (pendingFlushTimer || completedActivities.length === 0) return;
|
|
1529
|
+
pendingFlushTimer = setTimeout(flush, 0);
|
|
1530
|
+
},
|
|
1531
|
+
clear() {
|
|
1532
|
+
if (pendingFlushTimer) {
|
|
1533
|
+
clearTimeout(pendingFlushTimer);
|
|
1534
|
+
pendingFlushTimer = undefined;
|
|
1535
|
+
}
|
|
1536
|
+
completedActivities.length = 0;
|
|
1537
|
+
imagePreviewCache.clear();
|
|
1538
|
+
},
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1452
1542
|
function loadCachedImagePreview(savedImage: SavedGeneratedImage, imagePreviewCache: Map<string, CachedImagePreview>): CachedImagePreview | undefined {
|
|
1453
1543
|
const cached = imagePreviewCache.get(savedImage.absolutePath);
|
|
1454
1544
|
if (cached) return cached;
|
|
@@ -1543,6 +1633,7 @@ function createCodexStream<TApi extends Api>(
|
|
|
1543
1633
|
getNativeToolRewriteConfig?: () => { webSearch: boolean; imageGeneration: boolean };
|
|
1544
1634
|
onImageSaved?: (savedImage: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
|
|
1545
1635
|
onWebSearchCaptured?: (search: SurfacedWebSearch) => void;
|
|
1636
|
+
onStreamSettled?: () => void;
|
|
1546
1637
|
},
|
|
1547
1638
|
): AssistantMessageEventStream {
|
|
1548
1639
|
const stream = createAssistantMessageEventStream();
|
|
@@ -1696,6 +1787,8 @@ function createCodexStream<TApi extends Api>(
|
|
|
1696
1787
|
error: createErrorMessage(output, error, !!options?.signal?.aborted),
|
|
1697
1788
|
});
|
|
1698
1789
|
stream.end();
|
|
1790
|
+
} finally {
|
|
1791
|
+
deps.onStreamSettled?.();
|
|
1699
1792
|
}
|
|
1700
1793
|
})();
|
|
1701
1794
|
|
|
@@ -1703,75 +1796,31 @@ function createCodexStream<TApi extends Api>(
|
|
|
1703
1796
|
}
|
|
1704
1797
|
|
|
1705
1798
|
export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { getCurrentCwd: () => string; getNativeToolRewriteConfig?: () => { webSearch: boolean; imageGeneration: boolean } }): void {
|
|
1706
|
-
const
|
|
1707
|
-
const imagePreviewCache = new Map<string, CachedImagePreview>();
|
|
1708
|
-
let pendingFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1709
|
-
|
|
1710
|
-
const flushPendingMessages = () => {
|
|
1711
|
-
pendingFlushTimer = undefined;
|
|
1712
|
-
const activities = pendingActivities.splice(0, pendingActivities.length);
|
|
1713
|
-
|
|
1714
|
-
for (let index = 0; index < activities.length; index++) {
|
|
1715
|
-
const activity = activities[index];
|
|
1716
|
-
if (activity.kind === "image") {
|
|
1717
|
-
imagePreviewCache.set(activity.savedImage.absolutePath, activity.imageData);
|
|
1718
|
-
pi.sendMessage(
|
|
1719
|
-
{
|
|
1720
|
-
customType: IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
|
|
1721
|
-
content: [{ type: "text", text: buildGeneratedImageDisplayText(activity.savedImage, { expanded: false }) }],
|
|
1722
|
-
display: true,
|
|
1723
|
-
details: { savedImages: [activity.savedImage] } satisfies ImageDisplayMessageDetails,
|
|
1724
|
-
},
|
|
1725
|
-
{ triggerTurn: false },
|
|
1726
|
-
);
|
|
1727
|
-
continue;
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
const searches = [activity.search];
|
|
1731
|
-
while (index + 1 < activities.length && activities[index + 1]?.kind === "web-search") {
|
|
1732
|
-
searches.push((activities[++index] as QueuedWebSearchActivity).search);
|
|
1733
|
-
}
|
|
1734
|
-
pi.sendMessage(
|
|
1735
|
-
{
|
|
1736
|
-
customType: WEB_SEARCH_ACTIVITY_MESSAGE_TYPE,
|
|
1737
|
-
content: buildWebSearchActivityMessage(searches),
|
|
1738
|
-
display: true,
|
|
1739
|
-
details: { searches },
|
|
1740
|
-
},
|
|
1741
|
-
{ triggerTurn: false },
|
|
1742
|
-
);
|
|
1743
|
-
}
|
|
1744
|
-
};
|
|
1745
|
-
|
|
1746
|
-
const schedulePendingMessageFlush = () => {
|
|
1747
|
-
if (pendingFlushTimer || pendingActivities.length === 0) {
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
pendingFlushTimer = setTimeout(flushPendingMessages, 0);
|
|
1751
|
-
};
|
|
1799
|
+
const activityDispatcher = createActivityMessageDispatcher(pi.sendMessage.bind(pi));
|
|
1752
1800
|
|
|
1753
1801
|
const clearPendingMessages = () => {
|
|
1754
|
-
|
|
1755
|
-
clearTimeout(pendingFlushTimer);
|
|
1756
|
-
pendingFlushTimer = undefined;
|
|
1757
|
-
}
|
|
1758
|
-
pendingActivities.length = 0;
|
|
1759
|
-
imagePreviewCache.clear();
|
|
1802
|
+
activityDispatcher.clear();
|
|
1760
1803
|
};
|
|
1761
1804
|
|
|
1762
1805
|
pi.registerProvider("openai-codex", {
|
|
1763
1806
|
api: "openai-codex-responses",
|
|
1764
|
-
streamSimple: (model, context, streamOptions) =>
|
|
1765
|
-
|
|
1807
|
+
streamSimple: (model, context, streamOptions) => {
|
|
1808
|
+
const turnActivities: PendingActivity[] = [];
|
|
1809
|
+
return createCodexStream(model, context, streamOptions, {
|
|
1766
1810
|
getCurrentCwd: options.getCurrentCwd,
|
|
1767
1811
|
getNativeToolRewriteConfig: options.getNativeToolRewriteConfig,
|
|
1768
1812
|
onImageSaved: (savedImage, imageData) => {
|
|
1769
|
-
|
|
1813
|
+
turnActivities.push({ kind: "image", savedImage, imageData });
|
|
1770
1814
|
},
|
|
1771
1815
|
onWebSearchCaptured: (search) => {
|
|
1772
|
-
|
|
1816
|
+
turnActivities.push({ kind: "web-search", search });
|
|
1817
|
+
},
|
|
1818
|
+
onStreamSettled: () => {
|
|
1819
|
+
const activities = turnActivities.splice(0, turnActivities.length);
|
|
1820
|
+
if (activities.length > 0) activityDispatcher.enqueueSettledActivities(activities);
|
|
1773
1821
|
},
|
|
1774
|
-
})
|
|
1822
|
+
});
|
|
1823
|
+
},
|
|
1775
1824
|
});
|
|
1776
1825
|
|
|
1777
1826
|
pi.on("session_start", async () => {
|
|
@@ -1779,15 +1828,13 @@ export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { g
|
|
|
1779
1828
|
});
|
|
1780
1829
|
|
|
1781
1830
|
pi.on("session_shutdown", async () => {
|
|
1782
|
-
|
|
1783
|
-
flushPendingMessages();
|
|
1784
|
-
}
|
|
1831
|
+
activityDispatcher.flushNow();
|
|
1785
1832
|
clearPendingMessages();
|
|
1786
1833
|
closeOpenAICodexWebSocketSessions();
|
|
1787
1834
|
});
|
|
1788
1835
|
|
|
1789
1836
|
pi.on("agent_end", async () => {
|
|
1790
|
-
|
|
1837
|
+
activityDispatcher.scheduleFlush();
|
|
1791
1838
|
});
|
|
1792
1839
|
|
|
1793
1840
|
pi.registerMessageRenderer<ImageDisplayMessageDetails>(IMAGE_SAVE_DISPLAY_MESSAGE_TYPE, (message, options, theme) => {
|
|
@@ -1804,7 +1851,7 @@ export function registerOpenAICodexCustomProvider(pi: ExtensionAPI, options: { g
|
|
|
1804
1851
|
.join("\n");
|
|
1805
1852
|
box.addChild(new Text(`\n${theme.fg("customMessageText", textContent)}`, 0, 0));
|
|
1806
1853
|
if (savedImage) {
|
|
1807
|
-
const preview = loadCachedImagePreview(savedImage, imagePreviewCache);
|
|
1854
|
+
const preview = loadCachedImagePreview(savedImage, activityDispatcher.imagePreviewCache);
|
|
1808
1855
|
if (preview) {
|
|
1809
1856
|
box.addChild(new Spacer(1));
|
|
1810
1857
|
box.addChild(
|
|
Binary file
|
|
Binary file
|