@narumitw/pi-codex-compact 0.52.0 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -46
- package/dist/chunks/{chunk-JYXZMCXD.js → chunk-7H7JJ7ZV.js} +9 -15
- package/dist/chunks/chunk-7H7JJ7ZV.js.map +7 -0
- package/dist/chunks/{settings-menu-VYQ6ABCP.js → settings-menu-OAWFW2PH.js} +5 -15
- package/dist/chunks/settings-menu-OAWFW2PH.js.map +7 -0
- package/dist/index.ts +91 -94
- package/dist/index.ts.map +2 -2
- package/package.json +53 -54
- package/src/checkpoint.ts +261 -249
- package/src/codex-compact.ts +250 -254
- package/src/model-api.ts +40 -29
- package/src/protocol.ts +293 -312
- package/src/remote-compact.ts +230 -252
- package/src/remote-shared.ts +12 -15
- package/src/remote-types.ts +27 -26
- package/src/remote-v2.ts +57 -61
- package/src/remote.ts +7 -9
- package/src/settings-menu.ts +207 -231
- package/src/settings.ts +227 -190
- package/src/terminal.ts +4 -4
- package/dist/chunks/chunk-JYXZMCXD.js.map +0 -7
- package/dist/chunks/settings-menu-VYQ6ABCP.js.map +0 -7
package/src/codex-compact.ts
CHANGED
|
@@ -1,306 +1,302 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Api, Context, Model, Tool } from "@earendil-works/pi-ai";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
buildContextEntries,
|
|
5
|
+
buildSessionContext,
|
|
6
|
+
convertToLlm,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ExtensionContext,
|
|
9
|
+
type SessionBeforeCompactEvent,
|
|
10
|
+
sessionEntryToContextMessages,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
buildReplacementHistory,
|
|
14
|
+
type CodexCheckpointDetails,
|
|
15
|
+
checkpointMarker,
|
|
16
|
+
createCheckpointDetails,
|
|
17
|
+
fallbackSummary,
|
|
18
|
+
latestCheckpoint,
|
|
19
|
+
projectCheckpointContext,
|
|
20
20
|
} from "./checkpoint.js";
|
|
21
|
-
import {
|
|
21
|
+
import { type CompactionRoute, resolveCompactionRoute } from "./model-api.js";
|
|
22
22
|
import { hasCheckpointMarker, rewriteCheckpointMarker } from "./protocol.js";
|
|
23
23
|
import { requestRemoteCompaction } from "./remote.js";
|
|
24
24
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
type CodexCompactSettings,
|
|
26
|
+
type CodexCompactSettingsRuntime,
|
|
27
|
+
type CodexCompactSettingsState,
|
|
28
|
+
createCodexCompactSettingsRuntime,
|
|
29
29
|
} from "./settings.js";
|
|
30
30
|
import { terminalText } from "./terminal.js";
|
|
31
31
|
|
|
32
32
|
const STATUS_KEY = "codex-compact";
|
|
33
33
|
|
|
34
34
|
function activeCheckpoint(ctx: ExtensionContext) {
|
|
35
|
-
|
|
35
|
+
return latestCheckpoint(ctx.sessionManager.getBranch());
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function isCheckpointCompatible(
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
details: CodexCheckpointDetails,
|
|
40
|
+
model: Model<Api> | undefined,
|
|
41
|
+
settings: CodexCompactSettings,
|
|
41
42
|
): boolean {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const route = resolveCompactionRoute(model, settings);
|
|
44
|
+
return (
|
|
45
|
+
route.kind === "remote" &&
|
|
46
|
+
model !== undefined &&
|
|
47
|
+
route.api === details.api &&
|
|
48
|
+
route.profile === details.profile &&
|
|
49
|
+
model.id === details.modelId
|
|
50
|
+
);
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
function keptMessages(event: SessionBeforeCompactEvent): AgentMessage[] {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
return contextEntries.slice(keptIndex).flatMap(sessionEntryToContextMessages);
|
|
54
|
+
const leafId = event.branchEntries.at(-1)?.id ?? null;
|
|
55
|
+
const contextEntries = buildContextEntries(event.branchEntries, leafId);
|
|
56
|
+
const keptIndex = contextEntries.findIndex((entry) => entry.id === event.preparation.firstKeptEntryId);
|
|
57
|
+
if (keptIndex < 0) {
|
|
58
|
+
throw new Error("Pi compaction cut point is not present in the active context");
|
|
59
|
+
}
|
|
60
|
+
return contextEntries.slice(keptIndex).flatMap(sessionEntryToContextMessages);
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
function activeTools(pi: ExtensionAPI): Tool[] {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
const available = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
|
|
65
|
+
return pi.getActiveTools().flatMap((name) => {
|
|
66
|
+
const tool = available.get(name);
|
|
67
|
+
return tool
|
|
68
|
+
? [
|
|
69
|
+
{
|
|
70
|
+
name: tool.name,
|
|
71
|
+
description: tool.description,
|
|
72
|
+
parameters: tool.parameters,
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
: [];
|
|
76
|
+
});
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
function projectedCurrentMessages(
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
event: SessionBeforeCompactEvent,
|
|
81
|
+
model: Model<Api>,
|
|
82
|
+
route: Extract<CompactionRoute, { kind: "remote" }>,
|
|
78
83
|
): { messages: AgentMessage[]; prior?: CodexCheckpointDetails } {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
const leafId = event.branchEntries.at(-1)?.id ?? null;
|
|
85
|
+
const session = buildSessionContext(event.branchEntries, leafId);
|
|
86
|
+
const prior = latestCheckpoint(event.branchEntries);
|
|
87
|
+
if (!prior) return { messages: session.messages };
|
|
88
|
+
if (
|
|
89
|
+
prior.details.api !== route.api ||
|
|
90
|
+
prior.details.profile !== route.profile ||
|
|
91
|
+
prior.details.modelId !== model.id
|
|
92
|
+
) {
|
|
93
|
+
throw new Error("The active opaque checkpoint belongs to a different Responses model");
|
|
94
|
+
}
|
|
95
|
+
const projected = projectCheckpointContext(session.messages, prior.details, prior.entry.summary);
|
|
96
|
+
if (!projected) {
|
|
97
|
+
throw new Error("The previous opaque checkpoint could not be projected safely");
|
|
98
|
+
}
|
|
99
|
+
return { messages: projected, prior: prior.details };
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
function notifyFailure(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
): void {
|
|
98
|
-
if (!ctx.hasUI || !settings.notifyOnFallback) return;
|
|
99
|
-
const message = terminalText(error instanceof Error ? error.message : String(error));
|
|
100
|
-
ctx.ui.notify(`Responses compaction failed; using Pi compaction. ${message}`, "warning");
|
|
102
|
+
function notifyFailure(ctx: ExtensionContext, error: unknown, settings: CodexCompactSettings): void {
|
|
103
|
+
if (!ctx.hasUI || !settings.notifyOnFallback) return;
|
|
104
|
+
const message = terminalText(error instanceof Error ? error.message : String(error));
|
|
105
|
+
ctx.ui.notify(`Responses compaction failed; using Pi compaction. ${message}`, "warning");
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
function sessionStillOwned(ctx: ExtensionContext, sessionId: string, signal: AbortSignal): boolean {
|
|
104
|
-
|
|
109
|
+
return !signal.aborted && ctx.sessionManager.getSessionId() === sessionId;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
async function compactRemotely(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
pi: ExtensionAPI,
|
|
114
|
+
event: SessionBeforeCompactEvent,
|
|
115
|
+
ctx: ExtensionContext,
|
|
116
|
+
settings: CodexCompactSettings,
|
|
117
|
+
ownerSignal: AbortSignal,
|
|
118
|
+
fetch?: typeof globalThis.fetch,
|
|
114
119
|
) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
120
|
+
const model = ctx.model;
|
|
121
|
+
const route = resolveCompactionRoute(model, settings);
|
|
122
|
+
if (route.kind === "native" || !model) return undefined;
|
|
123
|
+
const signal = AbortSignal.any([event.signal, ownerSignal]);
|
|
124
|
+
if (signal.aborted) return { cancel: true };
|
|
125
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
126
|
+
ctx.ui.setStatus(STATUS_KEY, route.protocol === "remote-v2" ? "Responses Remote V2…" : "Responses Compact API…");
|
|
127
|
+
try {
|
|
128
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
129
|
+
if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
|
|
130
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
131
|
+
const provider = ctx.modelRegistry.getProvider(model.provider);
|
|
132
|
+
if (!provider) throw new Error("The active Responses provider is unavailable");
|
|
133
|
+
const current = projectedCurrentMessages(event, model, route);
|
|
134
|
+
const context: Context = {
|
|
135
|
+
systemPrompt: ctx.getSystemPrompt(),
|
|
136
|
+
messages: convertToLlm(current.messages),
|
|
137
|
+
tools: activeTools(pi),
|
|
138
|
+
};
|
|
139
|
+
const response = await requestRemoteCompaction({
|
|
140
|
+
provider,
|
|
141
|
+
model,
|
|
142
|
+
context,
|
|
143
|
+
protocol: route.protocol,
|
|
144
|
+
profile: route.profile,
|
|
145
|
+
apiKey: auth.apiKey,
|
|
146
|
+
headers: auth.headers,
|
|
147
|
+
env: auth.env,
|
|
148
|
+
signal,
|
|
149
|
+
priorCheckpoint: current.prior
|
|
150
|
+
? {
|
|
151
|
+
marker: checkpointMarker(current.prior.checkpointId),
|
|
152
|
+
replacementHistory: current.prior.replacementHistory,
|
|
153
|
+
}
|
|
154
|
+
: undefined,
|
|
155
|
+
requestTimeoutMs: settings.requestTimeoutMs,
|
|
156
|
+
maxRetries: settings.maxRetries,
|
|
157
|
+
fetch,
|
|
158
|
+
});
|
|
159
|
+
if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
|
|
160
|
+
const replacementHistory = buildReplacementHistory(
|
|
161
|
+
response.compactedOutput?.slice(0, -1) ?? response.promptInput,
|
|
162
|
+
response.item,
|
|
163
|
+
{ tokenBudget: settings.replacementTokenBudget },
|
|
164
|
+
);
|
|
165
|
+
const details = createCheckpointDetails({
|
|
166
|
+
provider: model.provider,
|
|
167
|
+
api: route.api,
|
|
168
|
+
profile: route.profile,
|
|
169
|
+
modelId: model.id,
|
|
170
|
+
protocol: route.protocol,
|
|
171
|
+
replacementHistory,
|
|
172
|
+
keptMessages: keptMessages(event),
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
compaction: {
|
|
176
|
+
summary: fallbackSummary(details.checkpointId),
|
|
177
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
178
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
179
|
+
usage: response.usage,
|
|
180
|
+
details,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (signal.aborted || ctx.sessionManager.getSessionId() !== sessionId) {
|
|
185
|
+
return { cancel: true };
|
|
186
|
+
}
|
|
187
|
+
notifyFailure(ctx, error, settings);
|
|
188
|
+
return undefined;
|
|
189
|
+
} finally {
|
|
190
|
+
if (ctx.sessionManager.getSessionId() === sessionId) ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
191
|
+
}
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
export function createCodexCompactExtension(
|
|
191
|
-
|
|
195
|
+
options: { fetch?: typeof globalThis.fetch; settingsRuntime?: CodexCompactSettingsRuntime } = {},
|
|
192
196
|
): (pi: ExtensionAPI) => void {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
return (pi) => {
|
|
198
|
+
const providerWarnings = new Set<string>();
|
|
199
|
+
const settingsRuntime = options.settingsRuntime ?? createCodexCompactSettingsRuntime();
|
|
200
|
+
let sessionController = new AbortController();
|
|
201
|
+
let generation = 0;
|
|
198
202
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
203
|
+
pi.registerCommand("codex-compact", {
|
|
204
|
+
description: "Compact now or configure Responses compaction",
|
|
205
|
+
handler: async (args, ctx) => {
|
|
206
|
+
if (args.trim()) throw new Error("Usage: /codex-compact");
|
|
207
|
+
const ownerGeneration = generation;
|
|
208
|
+
const controller = sessionController;
|
|
209
|
+
const { showCodexCompactMenu } = await import("./settings-menu.js");
|
|
210
|
+
if (ownerGeneration !== generation || controller.signal.aborted) return;
|
|
211
|
+
await showCodexCompactMenu(settingsRuntime, ctx, {
|
|
212
|
+
signal: controller.signal,
|
|
213
|
+
isCurrent: () => ownerGeneration === generation && !controller.signal.aborted,
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
});
|
|
213
217
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
218
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
219
|
+
sessionController.abort();
|
|
220
|
+
sessionController = new AbortController();
|
|
221
|
+
generation += 1;
|
|
222
|
+
const ownerGeneration = generation;
|
|
223
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
224
|
+
providerWarnings.clear();
|
|
225
|
+
let state: Readonly<CodexCompactSettingsState>;
|
|
226
|
+
try {
|
|
227
|
+
state = await settingsRuntime.reload(sessionController.signal);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (sessionController.signal.aborted || ownerGeneration !== generation) return;
|
|
230
|
+
if (ctx.hasUI) {
|
|
231
|
+
ctx.ui.notify(
|
|
232
|
+
`Could not load pi-codex-compact.json; using defaults. ${terminalText(error instanceof Error ? error.message : String(error))}`,
|
|
233
|
+
"warning",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (
|
|
239
|
+
sessionController.signal.aborted ||
|
|
240
|
+
ownerGeneration !== generation ||
|
|
241
|
+
ctx.sessionManager.getSessionId() !== sessionId
|
|
242
|
+
) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (ctx.hasUI && state.kind === "invalid") {
|
|
246
|
+
ctx.ui.notify(
|
|
247
|
+
`Invalid pi-codex-compact.json; using defaults without overwriting it. ${terminalText(state.issue ?? "unknown validation error")}`,
|
|
248
|
+
"warning",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
248
252
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
event,
|
|
253
|
-
ctx,
|
|
254
|
-
settingsRuntime.get().settings,
|
|
255
|
-
sessionController.signal,
|
|
256
|
-
options.fetch,
|
|
257
|
-
),
|
|
258
|
-
);
|
|
253
|
+
pi.on("session_before_compact", (event, ctx) =>
|
|
254
|
+
compactRemotely(pi, event, ctx, settingsRuntime.get().settings, sessionController.signal, options.fetch),
|
|
255
|
+
);
|
|
259
256
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
);
|
|
269
|
-
return messages ? { messages } : undefined;
|
|
270
|
-
});
|
|
257
|
+
pi.on("context", (event, ctx) => {
|
|
258
|
+
if (!settingsRuntime.get().settings.enabled) return undefined;
|
|
259
|
+
const settings = settingsRuntime.get().settings;
|
|
260
|
+
const checkpoint = activeCheckpoint(ctx);
|
|
261
|
+
if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model, settings)) return undefined;
|
|
262
|
+
const messages = projectCheckpointContext(event.messages, checkpoint.details, checkpoint.entry.summary);
|
|
263
|
+
return messages ? { messages } : undefined;
|
|
264
|
+
});
|
|
271
265
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
266
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
267
|
+
if (!settingsRuntime.get().settings.enabled) return undefined;
|
|
268
|
+
const settings = settingsRuntime.get().settings;
|
|
269
|
+
const checkpoint = activeCheckpoint(ctx);
|
|
270
|
+
if (!checkpoint || !isCheckpointCompatible(checkpoint.details, ctx.model, settings)) return undefined;
|
|
271
|
+
const marker = checkpointMarker(checkpoint.details.checkpointId);
|
|
272
|
+
if (!hasCheckpointMarker(event.payload, marker)) return undefined;
|
|
273
|
+
return rewriteCheckpointMarker(event.payload, marker, checkpoint.details.replacementHistory);
|
|
274
|
+
});
|
|
280
275
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
276
|
+
pi.on("model_select", (event, ctx) => {
|
|
277
|
+
if (!settingsRuntime.get().settings.enabled) return;
|
|
278
|
+
const settings = settingsRuntime.get().settings;
|
|
279
|
+
const checkpoint = activeCheckpoint(ctx);
|
|
280
|
+
if (!checkpoint || isCheckpointCompatible(checkpoint.details, event.model, settings)) return;
|
|
281
|
+
const key = `${ctx.sessionManager.getSessionId()}:${event.model.provider}:${event.model.id}`;
|
|
282
|
+
if (providerWarnings.has(key)) return;
|
|
283
|
+
providerWarnings.add(key);
|
|
284
|
+
if (ctx.hasUI) {
|
|
285
|
+
ctx.ui.notify(
|
|
286
|
+
"The active Responses checkpoint cannot replay on this model; Pi will expose only its fallback marker and retained recent messages.",
|
|
287
|
+
"warning",
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
295
291
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
292
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
293
|
+
generation += 1;
|
|
294
|
+
sessionController.abort();
|
|
295
|
+
providerWarnings.clear();
|
|
296
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
297
|
+
await settingsRuntime.flush();
|
|
298
|
+
});
|
|
299
|
+
};
|
|
304
300
|
}
|
|
305
301
|
|
|
306
302
|
export default createCodexCompactExtension();
|
package/src/model-api.ts
CHANGED
|
@@ -1,48 +1,59 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import type { CodexCompactSettings } from "./settings.js";
|
|
3
3
|
|
|
4
4
|
export const RESPONSES_COMPACTION_APIS = [
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
"openai-codex-responses",
|
|
6
|
+
"openai-responses",
|
|
7
|
+
"azure-openai-responses",
|
|
8
8
|
] as const;
|
|
9
9
|
|
|
10
|
-
export type
|
|
10
|
+
export type BuiltInResponsesCompactionApi = (typeof RESPONSES_COMPACTION_APIS)[number];
|
|
11
|
+
export type ResponsesCompactionApi = BuiltInResponsesCompactionApi;
|
|
12
|
+
export type ResponsesCompactionProfile = "codex-responses-v1" | "openai-responses-v1";
|
|
11
13
|
export type RemoteCompactionProtocol = "remote-v2" | "responses-compact";
|
|
12
14
|
export type RemoteCompactionProtocolSetting = "auto" | RemoteCompactionProtocol;
|
|
13
15
|
|
|
14
16
|
export type CompactionRoute =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
| { kind: "remote"; protocol: RemoteCompactionProtocol; api: Api; profile: ResponsesCompactionProfile }
|
|
18
|
+
| { kind: "native"; reason: string };
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
function resolveResponsesCompactionProfile(
|
|
21
|
+
api: Api | undefined,
|
|
22
|
+
apiProfiles: Readonly<Record<string, "codex-responses-v1">> = {},
|
|
23
|
+
): ResponsesCompactionProfile | undefined {
|
|
24
|
+
if (api === "openai-codex-responses") return "codex-responses-v1";
|
|
25
|
+
if (api === "openai-responses" || api === "azure-openai-responses") return "openai-responses-v1";
|
|
26
|
+
return api && Object.hasOwn(apiProfiles, api) && apiProfiles[api] === "codex-responses-v1"
|
|
27
|
+
? "codex-responses-v1"
|
|
28
|
+
: undefined;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
export function resolveCompactionRouteForApi(
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
api: Api | undefined,
|
|
33
|
+
options: Pick<CodexCompactSettings, "enabled" | "protocol"> & {
|
|
34
|
+
apiProfiles?: Readonly<Record<string, "codex-responses-v1">>;
|
|
35
|
+
},
|
|
27
36
|
): CompactionRoute {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
if (!options.enabled) return { kind: "native", reason: "remote compaction is disabled" };
|
|
38
|
+
if (!api) return { kind: "native", reason: "no active model" };
|
|
39
|
+
const profile = resolveResponsesCompactionProfile(api, options.apiProfiles);
|
|
40
|
+
if (!profile) {
|
|
41
|
+
return { kind: "native", reason: `API ${api} does not support Responses compaction` };
|
|
42
|
+
}
|
|
43
|
+
const protocol =
|
|
44
|
+
options.protocol === "auto"
|
|
45
|
+
? profile === "codex-responses-v1"
|
|
46
|
+
? "remote-v2"
|
|
47
|
+
: "responses-compact"
|
|
48
|
+
: options.protocol;
|
|
49
|
+
return { kind: "remote", protocol, api, profile };
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
export function resolveCompactionRoute(
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
model: Model<Api> | undefined,
|
|
54
|
+
options: Pick<CodexCompactSettings, "enabled" | "protocol"> & {
|
|
55
|
+
apiProfiles?: Readonly<Record<string, "codex-responses-v1">>;
|
|
56
|
+
},
|
|
46
57
|
): CompactionRoute {
|
|
47
|
-
|
|
58
|
+
return resolveCompactionRouteForApi(model?.api, options);
|
|
48
59
|
}
|