pi-openai-toolkit 0.24.0 → 0.24.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/LICENSE +21 -21
- package/NOTICE +83 -83
- package/config.schema.json +264 -264
- package/docs/configuration.md +304 -304
- package/package.json +123 -123
- package/src/compact-client.ts +412 -412
- package/src/compaction-projection.ts +35 -35
- package/src/config/legacy.ts +469 -469
- package/src/config/migration.ts +115 -115
- package/src/config/notifications.ts +46 -42
- package/src/config/policy.ts +115 -115
- package/src/config/v2.ts +1128 -1128
- package/src/config.ts +320 -320
- package/src/debug.ts +238 -238
- package/src/deferred-tool-carryover.ts +282 -282
- package/src/details-store.ts +151 -151
- package/src/extension-runtime.ts +1095 -1095
- package/src/image-generation/extension.ts +189 -189
- package/src/image-generation/protocol.ts +579 -579
- package/src/image-generation/types.ts +300 -300
- package/src/native-fallback.ts +140 -140
- package/src/payload-rewrite.ts +515 -515
- package/src/pi-context-hook.ts +117 -117
- package/src/remote-v2-client.ts +811 -811
- package/src/request-context-cache.ts +155 -155
- package/src/responses-headers.ts +180 -180
- package/src/retired-remote-windows.ts +35 -35
- package/src/runtime.ts +875 -875
- package/src/serializer.ts +882 -882
- package/src/supported-environment.ts +27 -27
- package/src/types.ts +541 -541
- package/src/web-search/alpha-client.ts +876 -876
- package/src/web-search/extension.ts +530 -530
- package/src/web-search/payload.ts +299 -299
- package/src/web-search/prompt.ts +60 -60
- package/src/web-search/types.ts +166 -166
- package/tsconfig.check.json +50 -50
package/src/extension-runtime.ts
CHANGED
|
@@ -1,1095 +1,1095 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
-
import type {
|
|
4
|
-
BeforeProviderRequestEvent,
|
|
5
|
-
ContextWithSystemEvent,
|
|
6
|
-
CompactionResult,
|
|
7
|
-
ExtensionAPI,
|
|
8
|
-
ExtensionContext,
|
|
9
|
-
SessionBeforeCompactEvent,
|
|
10
|
-
} from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { assertConfigValid, loadToolkitConfig, resolveToolkitConfig, type ResolvedToolkitConfig } from "./config";
|
|
12
|
-
import { notifyConfigIssues } from "./config/notifications";
|
|
13
|
-
import { registerToolkitConfigCommand } from "./config-command";
|
|
14
|
-
import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
|
|
15
|
-
import {
|
|
16
|
-
COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
17
|
-
COMPACTION_CHECKPOINT_CONTEXT_EDITED,
|
|
18
|
-
COMPACTION_PROJECTION_UNAVAILABLE,
|
|
19
|
-
COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
|
|
20
|
-
COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
|
|
21
|
-
hasVerifiedCompactionInputProvenance,
|
|
22
|
-
type UnprojectedCompactionReason,
|
|
23
|
-
} from "./compaction-projection";
|
|
24
|
-
import { buildResponsesRequestHeaders, CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
|
|
25
|
-
import { isExactModelAllowed } from "./model-scope";
|
|
26
|
-
import { detectRetiredRemoteWindows, RETIRED_REMOTE_WINDOWS_MESSAGE } from "./retired-remote-windows";
|
|
27
|
-
import { executeNativeCompaction, type NativeCompactionClientResult } from "./compact-client";
|
|
28
|
-
import { qualifiesForStandaloneCompaction, type CompactionErrorInfo } from "./compaction-errors";
|
|
29
|
-
import { resolveLatestNativeCompactionEntry } from "./details-store";
|
|
30
|
-
import {
|
|
31
|
-
getPiContextHookProjector,
|
|
32
|
-
installPiContextHookPatch,
|
|
33
|
-
reportPiContextHookFailure,
|
|
34
|
-
type PiContextHookPatchResult,
|
|
35
|
-
} from "./pi-context-hook";
|
|
36
|
-
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
37
|
-
import {
|
|
38
|
-
rewriteResponsesPayloadWithNativeReplay,
|
|
39
|
-
hasInvalidatedCompactionContext,
|
|
40
|
-
removeNativeCompactionRetainedMessages,
|
|
41
|
-
serializeLiveTailToResponsesInputWithLimits,
|
|
42
|
-
} from "./payload-rewrite";
|
|
43
|
-
import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
|
|
44
|
-
import { resolveWebSearchRoute } from "./web-search/types";
|
|
45
|
-
import { executeRemoteV2Compaction } from "./remote-v2-client";
|
|
46
|
-
import {
|
|
47
|
-
registerPhysicalModelObserver,
|
|
48
|
-
resolveNativeCompactionEnvironment,
|
|
49
|
-
resolveRemoteCompactionExecution,
|
|
50
|
-
isStandaloneCompactionEligible,
|
|
51
|
-
parseModelSpec,
|
|
52
|
-
type RemoteCompactionExecution,
|
|
53
|
-
} from "./runtime";
|
|
54
|
-
import {
|
|
55
|
-
serializeMessagesToCompactRequestWithLimits,
|
|
56
|
-
serializeMessagesToResponsesInputWithLimits,
|
|
57
|
-
type NativeCompactionRequestBody,
|
|
58
|
-
type ResponsesInputItem,
|
|
59
|
-
} from "./serializer";
|
|
60
|
-
import {
|
|
61
|
-
createNativeCompactionDetails,
|
|
62
|
-
createNativeCompactionResult,
|
|
63
|
-
COMPACTION_EXTENSION_ID,
|
|
64
|
-
LEGACY_NATIVE_COMPACTION_STRATEGY,
|
|
65
|
-
REMOTE_V2_COMPACTION_STRATEGY,
|
|
66
|
-
getLatestDeferredToolCarryover,
|
|
67
|
-
getRemoteV2InputProvenance,
|
|
68
|
-
isNativeCompactionDetails,
|
|
69
|
-
type CompactionConfig,
|
|
70
|
-
type NativeCompactionDetails,
|
|
71
|
-
type NativeCompactionRequestMeta,
|
|
72
|
-
} from "./types";
|
|
73
|
-
|
|
74
|
-
type CompactionContextProjection = (
|
|
75
|
-
messages: readonly AgentMessage[],
|
|
76
|
-
ctx: ExtensionContext,
|
|
77
|
-
) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
|
|
78
|
-
|
|
79
|
-
function contextOperation(loadConfig: typeof loadToolkitConfig, ctx: ExtensionContext, model = ctx.model): ResolvedToolkitConfig {
|
|
80
|
-
const loaded = loadConfig();
|
|
81
|
-
const resolved = resolveToolkitConfig(loaded, model);
|
|
82
|
-
notifyConfigIssues(ctx, resolved);
|
|
83
|
-
return resolved;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function requireContextPolicy(resolved: ResolvedToolkitConfig, ctx: ExtensionContext): void {
|
|
87
|
-
try {
|
|
88
|
-
assertConfigValid(resolved, "context", "compatibility", "diagnostics");
|
|
89
|
-
} catch (error) {
|
|
90
|
-
if (typeof ctx.abort === "function") ctx.abort();
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
type CompactionDependencies = {
|
|
96
|
-
loadConfig: typeof loadToolkitConfig;
|
|
97
|
-
remoteCompact: typeof executeRemoteV2Compaction;
|
|
98
|
-
standaloneCompact: typeof executeNativeCompaction;
|
|
99
|
-
nativeFallback: typeof runNativeFallbackCompaction;
|
|
100
|
-
/** Revalidate operation ownership after awaits and at result consumption. */
|
|
101
|
-
isCurrentCompaction?: () => boolean;
|
|
102
|
-
/** Test seam for the same ordered projection exposed by the Pi host patch. */
|
|
103
|
-
projectCompactionContext?: CompactionContextProjection;
|
|
104
|
-
piContextHookPatch: PiContextHookPatchResult;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Notices are bounded, but every boundary freshly reads the current branch.
|
|
108
|
-
const retirementNotices = new WeakMap<object, string>();
|
|
109
|
-
function retirementReason(ctx: ExtensionContext): string | undefined {
|
|
110
|
-
try { return detectRetiredRemoteWindows(ctx.sessionManager.getBranch()); }
|
|
111
|
-
catch { return "branch-unavailable"; }
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function refuseRetiredSession(ctx: ExtensionContext, request = false): boolean {
|
|
115
|
-
const reason = retirementReason(ctx);
|
|
116
|
-
const owner = ctx.sessionManager;
|
|
117
|
-
if (!reason) {
|
|
118
|
-
retirementNotices.delete(owner);
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
const fingerprint = `${getSessionId(ctx) ?? ""}:${reason}`;
|
|
122
|
-
if (ctx.hasUI && retirementNotices.get(owner) !== fingerprint) {
|
|
123
|
-
retirementNotices.set(owner, fingerprint);
|
|
124
|
-
try {
|
|
125
|
-
ctx.ui.notify(
|
|
126
|
-
reason === "branch-unavailable"
|
|
127
|
-
? `${COMPACTION_EXTENSION_ID}: Cannot verify the active session branch; request blocked. Reopen the session before continuing.`
|
|
128
|
-
: `${COMPACTION_EXTENSION_ID}: ${RETIRED_REMOTE_WINDOWS_MESSAGE}`,
|
|
129
|
-
"error",
|
|
130
|
-
);
|
|
131
|
-
} catch {
|
|
132
|
-
// A detached UI must not prevent abort or turn compact refusal into fallback.
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (request) {
|
|
136
|
-
reportPiContextHookFailure(ctx, `retired-remote-windows:${reason}`);
|
|
137
|
-
ctx.abort();
|
|
138
|
-
}
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
type ResponsesCompactOutcome =
|
|
143
|
-
| { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
|
|
144
|
-
| { outcome: "aborted" }
|
|
145
|
-
| { outcome: "failed"; remoteFailure?: CompactionErrorInfo & { reason: string; status?: number }; protocol?: string }
|
|
146
|
-
| {
|
|
147
|
-
outcome: "unprojected-input";
|
|
148
|
-
reason: UnprojectedCompactionReason;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
|
|
152
|
-
return {
|
|
153
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
154
|
-
previousSummaryPresent: Boolean(event.preparation.previousSummary),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function getCurrentModelDebugInfo(ctx: ExtensionContext) {
|
|
159
|
-
return ctx.model
|
|
160
|
-
? {
|
|
161
|
-
provider: ctx.model.provider,
|
|
162
|
-
id: ctx.model.id,
|
|
163
|
-
}
|
|
164
|
-
: undefined;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
|
|
168
|
-
return isNativeCompactionDetails(entry?.details)
|
|
169
|
-
? {
|
|
170
|
-
provider: entry.details.provider,
|
|
171
|
-
api: entry.details.api,
|
|
172
|
-
model: entry.details.model,
|
|
173
|
-
baseUrl: entry.details.baseUrl,
|
|
174
|
-
compactionModel: entry.details.compactionModel,
|
|
175
|
-
}
|
|
176
|
-
: undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function getSessionId(ctx: ExtensionContext): string | undefined {
|
|
180
|
-
try {
|
|
181
|
-
return ctx.sessionManager.getSessionId();
|
|
182
|
-
} catch {
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function notifyWarning(ctx: ExtensionContext, message: string): void {
|
|
188
|
-
if (ctx.hasUI) {
|
|
189
|
-
ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${message}`, "warning");
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
|
|
194
|
-
return window.map((item) => structuredClone(item));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
|
|
198
|
-
const guidance = customInstructions?.trim();
|
|
199
|
-
if (!guidance) {
|
|
200
|
-
return systemPrompt;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function findLatestProjectedCompactionSummaryIndex(
|
|
207
|
-
messages: readonly AgentMessage[],
|
|
208
|
-
summary: string,
|
|
209
|
-
): number {
|
|
210
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
211
|
-
const message = messages[index];
|
|
212
|
-
if (message?.role === "compactionSummary" && message.summary === summary) {
|
|
213
|
-
return index;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return -1;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
|
|
221
|
-
try {
|
|
222
|
-
const projection = ctx.sessionManager.buildSessionProjection();
|
|
223
|
-
if (!projection || !Array.isArray(projection.messages)) return undefined;
|
|
224
|
-
return structuredClone(projection.messages);
|
|
225
|
-
} catch {
|
|
226
|
-
return undefined;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
type CompactionProjectionResult =
|
|
231
|
-
| { ok: true; messages: readonly AgentMessage[] }
|
|
232
|
-
| { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
|
|
233
|
-
|
|
234
|
-
async function projectSessionContextForCompaction(
|
|
235
|
-
ctx: ExtensionContext,
|
|
236
|
-
projectCompactionContext: CompactionContextProjection | undefined,
|
|
237
|
-
): Promise<CompactionProjectionResult> {
|
|
238
|
-
const messages = readSessionContextMessages(ctx);
|
|
239
|
-
if (!messages) {
|
|
240
|
-
return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
|
|
244
|
-
if (!projector) {
|
|
245
|
-
return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
const projected = await projector(messages, ctx);
|
|
250
|
-
return Array.isArray(projected)
|
|
251
|
-
? { ok: true, messages: projected }
|
|
252
|
-
: { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
253
|
-
} catch {
|
|
254
|
-
return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function getLegacySessionContextMessages(
|
|
259
|
-
event: SessionBeforeCompactEvent,
|
|
260
|
-
ctx: ExtensionContext,
|
|
261
|
-
): AgentMessage[] {
|
|
262
|
-
return readSessionContextMessages(ctx) ?? [
|
|
263
|
-
...event.preparation.messagesToSummarize,
|
|
264
|
-
...event.preparation.turnPrefixMessages,
|
|
265
|
-
];
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async function runResponsesNativeCompact(
|
|
269
|
-
event: SessionBeforeCompactEvent,
|
|
270
|
-
ctx: ExtensionContext,
|
|
271
|
-
config: CompactionConfig,
|
|
272
|
-
execution: RemoteCompactionExecution,
|
|
273
|
-
remoteCompact: typeof executeRemoteV2Compaction,
|
|
274
|
-
projectCompactionContext: CompactionContextProjection | undefined,
|
|
275
|
-
standaloneCompact: typeof executeNativeCompaction,
|
|
276
|
-
isCurrent: () => boolean,
|
|
277
|
-
): Promise<ResponsesCompactOutcome> {
|
|
278
|
-
const { consumer, compactor } = execution;
|
|
279
|
-
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
280
|
-
const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
|
|
281
|
-
const deferredToolCarryover = getLatestDeferredToolCarryover(branchEntries);
|
|
282
|
-
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
283
|
-
baseUrl: consumer.baseUrl,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
|
|
287
|
-
let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
|
|
288
|
-
let request: NativeCompactionRequestBody;
|
|
289
|
-
if (latestNativeCompaction.ok) {
|
|
290
|
-
if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
|
|
291
|
-
return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
|
|
292
|
-
}
|
|
293
|
-
const details = latestNativeCompaction.entry.details;
|
|
294
|
-
if (!details) {
|
|
295
|
-
return { outcome: "failed" };
|
|
296
|
-
}
|
|
297
|
-
if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
|
|
298
|
-
return {
|
|
299
|
-
outcome: "unprojected-input",
|
|
300
|
-
reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
requestSource = "latest-native-replay";
|
|
304
|
-
|
|
305
|
-
if (config.remoteV2ContextSource === "pi-context-hook") {
|
|
306
|
-
const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
|
|
307
|
-
if (!projection.ok) {
|
|
308
|
-
return { outcome: "unprojected-input", reason: projection.reason };
|
|
309
|
-
}
|
|
310
|
-
const summaryIndex = findLatestProjectedCompactionSummaryIndex(
|
|
311
|
-
projection.messages,
|
|
312
|
-
latestNativeCompaction.entry.summary,
|
|
313
|
-
);
|
|
314
|
-
if (summaryIndex < 0) {
|
|
315
|
-
return {
|
|
316
|
-
outcome: "unprojected-input",
|
|
317
|
-
reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
const input: ResponsesInputItem[] = [
|
|
321
|
-
...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
|
|
322
|
-
...(await serializeMessagesToResponsesInputWithLimits(
|
|
323
|
-
compactor.currentModel,
|
|
324
|
-
[...projection.messages.slice(summaryIndex + 1)],
|
|
325
|
-
{ firstSystemMessageIsUpdate: true },
|
|
326
|
-
)),
|
|
327
|
-
];
|
|
328
|
-
request = {
|
|
329
|
-
model: compactor.requestModel ?? compactor.model,
|
|
330
|
-
input,
|
|
331
|
-
instructions,
|
|
332
|
-
};
|
|
333
|
-
} else {
|
|
334
|
-
const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
|
|
335
|
-
const input: ResponsesInputItem[] = [
|
|
336
|
-
...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
|
|
337
|
-
...(await serializeLiveTailToResponsesInputWithLimits({ model: compactor.currentModel, entries: liveTailEntries })),
|
|
338
|
-
];
|
|
339
|
-
request = {
|
|
340
|
-
model: compactor.requestModel ?? compactor.model,
|
|
341
|
-
input,
|
|
342
|
-
instructions,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
} else if (
|
|
346
|
-
latestNativeCompaction.reason === "no-compaction" ||
|
|
347
|
-
(latestNativeCompaction.reason === "latest-compaction-not-native" &&
|
|
348
|
-
config.allowCompactionContinuityBreak)
|
|
349
|
-
) {
|
|
350
|
-
requestSource =
|
|
351
|
-
latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
|
|
352
|
-
if (config.remoteV2ContextSource === "pi-context-hook") {
|
|
353
|
-
const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
|
|
354
|
-
if (!projection.ok) {
|
|
355
|
-
return { outcome: "unprojected-input", reason: projection.reason };
|
|
356
|
-
}
|
|
357
|
-
const serialized = await serializeMessagesToCompactRequestWithLimits({
|
|
358
|
-
model: compactor.currentModel,
|
|
359
|
-
messages: [...projection.messages],
|
|
360
|
-
instructions,
|
|
361
|
-
});
|
|
362
|
-
request = { ...serialized, model: compactor.requestModel ?? serialized.model };
|
|
363
|
-
} else {
|
|
364
|
-
const serialized = await serializeMessagesToCompactRequestWithLimits({
|
|
365
|
-
model: compactor.currentModel,
|
|
366
|
-
messages: getLegacySessionContextMessages(event, ctx),
|
|
367
|
-
instructions,
|
|
368
|
-
});
|
|
369
|
-
request = { ...serialized, model: compactor.requestModel ?? serialized.model };
|
|
370
|
-
}
|
|
371
|
-
} else {
|
|
372
|
-
writeDebugArtifact(
|
|
373
|
-
"compaction-event",
|
|
374
|
-
{
|
|
375
|
-
event: "session_before_compact.remote-v2-skip",
|
|
376
|
-
reason: latestNativeCompaction.reason,
|
|
377
|
-
consumer: {
|
|
378
|
-
provider: consumer.provider,
|
|
379
|
-
api: consumer.api,
|
|
380
|
-
model: consumer.model,
|
|
381
|
-
baseUrl: consumer.baseUrl,
|
|
382
|
-
},
|
|
383
|
-
compactor: {
|
|
384
|
-
provider: compactor.provider,
|
|
385
|
-
api: compactor.api,
|
|
386
|
-
model: compactor.model,
|
|
387
|
-
baseUrl: compactor.baseUrl,
|
|
388
|
-
},
|
|
389
|
-
latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
|
|
390
|
-
latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
|
|
391
|
-
},
|
|
392
|
-
config,
|
|
393
|
-
ctx,
|
|
394
|
-
);
|
|
395
|
-
return { outcome: "failed" };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Mirror the latest codex_rs CompactionInput fields captured from the most
|
|
399
|
-
// recent live provider request for this model (tools, reasoning, etc.).
|
|
400
|
-
const extras = getCompactionRequestExtras({
|
|
401
|
-
provider: consumer.provider,
|
|
402
|
-
api: consumer.api,
|
|
403
|
-
model: consumer.requestModel ?? consumer.model,
|
|
404
|
-
baseUrl: consumer.baseUrl,
|
|
405
|
-
sessionId: getSessionId(ctx),
|
|
406
|
-
}, compactor.currentModel);
|
|
407
|
-
if (extras) {
|
|
408
|
-
request = { ...request, ...extras };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (event.signal?.aborted || !isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
|
|
412
|
-
let protocol: "remote-v2" | "standalone-compact" = "remote-v2";
|
|
413
|
-
let compactResult: Awaited<ReturnType<typeof executeRemoteV2Compaction>> | NativeCompactionClientResult = await remoteCompact({
|
|
414
|
-
runtime: compactor,
|
|
415
|
-
request,
|
|
416
|
-
signal: event.signal,
|
|
417
|
-
settings: config,
|
|
418
|
-
context: ctx,
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
if (!isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
|
|
422
|
-
if (!compactResult.ok) {
|
|
423
|
-
if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
|
|
424
|
-
const tryStandalone = !!standaloneCompact && isStandaloneCompactionEligible(compactor) && qualifiesForStandaloneCompaction(compactResult);
|
|
425
|
-
writeDebugArtifact("compaction-event", {
|
|
426
|
-
event: "session_before_compact.remote-v2-failure",
|
|
427
|
-
protocol,
|
|
428
|
-
reason: compactResult.reason,
|
|
429
|
-
status: compactResult.status,
|
|
430
|
-
errorCode: compactResult.errorCode,
|
|
431
|
-
errorType: compactResult.errorType,
|
|
432
|
-
errorParam: compactResult.errorParam,
|
|
433
|
-
errorMessage: compactResult.errorMessage,
|
|
434
|
-
nextStep: tryStandalone ? "standalone-compact" : "native-summary",
|
|
435
|
-
}, config, ctx);
|
|
436
|
-
if (tryStandalone && standaloneCompact) {
|
|
437
|
-
if (event.signal?.aborted || !isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
|
|
438
|
-
protocol = "standalone-compact";
|
|
439
|
-
compactResult = await standaloneCompact({ runtime: compactor, request, signal: event.signal, settings: config, context: ctx });
|
|
440
|
-
}
|
|
441
|
-
if (!compactResult.ok) {
|
|
442
|
-
if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
|
|
443
|
-
const remoteFailure = {
|
|
444
|
-
reason: compactResult.reason,
|
|
445
|
-
status: compactResult.status,
|
|
446
|
-
errorCode: compactResult.errorCode,
|
|
447
|
-
errorType: compactResult.errorType,
|
|
448
|
-
errorParam: compactResult.errorParam,
|
|
449
|
-
errorMessage: compactResult.errorMessage,
|
|
450
|
-
};
|
|
451
|
-
if (protocol === "standalone-compact") writeDebugArtifact("compaction-event", {
|
|
452
|
-
event: "session_before_compact.standalone-compact-failure", protocol, ...remoteFailure, nextStep: "native-summary",
|
|
453
|
-
}, config, ctx);
|
|
454
|
-
return { outcome: "failed", remoteFailure, protocol };
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
let details: NativeCompactionDetails;
|
|
459
|
-
try {
|
|
460
|
-
details = createNativeCompactionDetails({
|
|
461
|
-
strategy: protocol === "standalone-compact" ? LEGACY_NATIVE_COMPACTION_STRATEGY : REMOTE_V2_COMPACTION_STRATEGY,
|
|
462
|
-
provider: consumer.provider,
|
|
463
|
-
api: consumer.api,
|
|
464
|
-
model: consumer.model,
|
|
465
|
-
baseUrl: consumer.baseUrl,
|
|
466
|
-
compactionModel: {
|
|
467
|
-
provider: compactor.provider,
|
|
468
|
-
api: compactor.api,
|
|
469
|
-
model: compactor.model,
|
|
470
|
-
baseUrl: compactor.baseUrl,
|
|
471
|
-
},
|
|
472
|
-
deferredToolCarryover,
|
|
473
|
-
compactedWindow: compactResult.compactedWindow,
|
|
474
|
-
compactResponseId: compactResult.compactResponseId,
|
|
475
|
-
createdAt: compactResult.createdAt,
|
|
476
|
-
requestMeta: buildCompactionRequestMeta(event),
|
|
477
|
-
inputProvenance,
|
|
478
|
-
});
|
|
479
|
-
} catch (error) {
|
|
480
|
-
writeDebugArtifact(
|
|
481
|
-
"compaction-event",
|
|
482
|
-
{
|
|
483
|
-
event: "session_before_compact.invalid-native-details",
|
|
484
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
485
|
-
consumer: {
|
|
486
|
-
provider: consumer.provider,
|
|
487
|
-
api: consumer.api,
|
|
488
|
-
model: consumer.model,
|
|
489
|
-
baseUrl: consumer.baseUrl,
|
|
490
|
-
},
|
|
491
|
-
compactor: {
|
|
492
|
-
provider: compactor.provider,
|
|
493
|
-
api: compactor.api,
|
|
494
|
-
model: compactor.model,
|
|
495
|
-
baseUrl: compactor.baseUrl,
|
|
496
|
-
},
|
|
497
|
-
},
|
|
498
|
-
config,
|
|
499
|
-
ctx,
|
|
500
|
-
);
|
|
501
|
-
return { outcome: "failed", remoteFailure: { reason: "invalid-native-details" }, protocol };
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const compaction = createNativeCompactionResult({
|
|
505
|
-
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
506
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
507
|
-
details,
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
writeDebugArtifact(
|
|
511
|
-
"compaction-event",
|
|
512
|
-
{
|
|
513
|
-
event: `session_before_compact.${protocol}-success`,
|
|
514
|
-
protocol,
|
|
515
|
-
consumer: {
|
|
516
|
-
provider: consumer.provider,
|
|
517
|
-
api: consumer.api,
|
|
518
|
-
model: consumer.model,
|
|
519
|
-
baseUrl: consumer.baseUrl,
|
|
520
|
-
},
|
|
521
|
-
compactor: {
|
|
522
|
-
provider: compactor.provider,
|
|
523
|
-
api: compactor.api,
|
|
524
|
-
model: compactor.model,
|
|
525
|
-
baseUrl: compactor.baseUrl,
|
|
526
|
-
},
|
|
527
|
-
requestSource,
|
|
528
|
-
remoteV2ContextSource: config.remoteV2ContextSource,
|
|
529
|
-
inputProvenance,
|
|
530
|
-
requestInputItems: request.input.length,
|
|
531
|
-
requestExtras: extras ? Object.keys(extras) : [],
|
|
532
|
-
compactResponseId: compactResult.compactResponseId,
|
|
533
|
-
compactedItems: compactResult.compactedWindow.length,
|
|
534
|
-
firstKeptEntryId: compaction.firstKeptEntryId,
|
|
535
|
-
},
|
|
536
|
-
config,
|
|
537
|
-
ctx,
|
|
538
|
-
);
|
|
539
|
-
|
|
540
|
-
if (!isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
|
|
541
|
-
return { outcome: "success", compaction };
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
async function handleSessionBeforeCompact(
|
|
545
|
-
event: SessionBeforeCompactEvent,
|
|
546
|
-
ctx: ExtensionContext,
|
|
547
|
-
dependencies: CompactionDependencies,
|
|
548
|
-
) {
|
|
549
|
-
if (refuseRetiredSession(ctx)) return { cancel: true };
|
|
550
|
-
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
551
|
-
if (resolved.invalidFeatures.some((feature) => ["context", "compatibility", "diagnostics"].includes(feature))) return { cancel: true };
|
|
552
|
-
const config = resolved.config.compaction;
|
|
553
|
-
if (config.enabled) {
|
|
554
|
-
writeDebugArtifact(
|
|
555
|
-
"compaction-event",
|
|
556
|
-
{
|
|
557
|
-
event: "session_before_compact",
|
|
558
|
-
customInstructions: event.customInstructions,
|
|
559
|
-
preparation: {
|
|
560
|
-
tokensBefore: event.preparation.tokensBefore,
|
|
561
|
-
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
562
|
-
previousSummaryPresent: Boolean(event.preparation.previousSummary),
|
|
563
|
-
messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
|
|
564
|
-
turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
config,
|
|
568
|
-
ctx,
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (event.signal.aborted) {
|
|
573
|
-
return { cancel: true };
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const isCurrent = dependencies.isCurrentCompaction ?? (() => true);
|
|
577
|
-
if (!isCurrent()) return { cancel: true };
|
|
578
|
-
if (!config.enabled || resolved.policy.context.mode !== "remote-compaction") return undefined;
|
|
579
|
-
|
|
580
|
-
// Resolve producer protocol policy from this operation's document, not a later disk read.
|
|
581
|
-
if (resolved.format === "v2" && config.remoteCompactModel) {
|
|
582
|
-
const producer = parseModelSpec(config.remoteCompactModel);
|
|
583
|
-
if (producer) {
|
|
584
|
-
const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: producer.provider, id: producer.modelId });
|
|
585
|
-
notifyConfigIssues(ctx, producerPolicy);
|
|
586
|
-
if (producerPolicy.invalidFeatures.includes("compatibility")) return { cancel: true };
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
// Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
|
|
590
|
-
let remoteAttempted = false;
|
|
591
|
-
const resolution = await resolveRemoteCompactionExecution(
|
|
592
|
-
ctx,
|
|
593
|
-
{
|
|
594
|
-
enabled: config.enabled,
|
|
595
|
-
responsesApis: config.responsesApis,
|
|
596
|
-
codexGatewayModels: resolved.gatewayModelKeys,
|
|
597
|
-
},
|
|
598
|
-
config.remoteCompactModel,
|
|
599
|
-
);
|
|
600
|
-
if (!isCurrent() || event.signal.aborted || refuseRetiredSession(ctx)) return { cancel: true };
|
|
601
|
-
if (resolution.ok) {
|
|
602
|
-
remoteAttempted = true;
|
|
603
|
-
const responsesOutcome = await runResponsesNativeCompact(
|
|
604
|
-
event,
|
|
605
|
-
ctx,
|
|
606
|
-
config,
|
|
607
|
-
resolution.execution,
|
|
608
|
-
dependencies.remoteCompact,
|
|
609
|
-
dependencies.projectCompactionContext,
|
|
610
|
-
dependencies.standaloneCompact,
|
|
611
|
-
isCurrent,
|
|
612
|
-
);
|
|
613
|
-
if (!isCurrent() || refuseRetiredSession(ctx)) return { cancel: true };
|
|
614
|
-
if (responsesOutcome.outcome === "success") {
|
|
615
|
-
return { compaction: responsesOutcome.compaction };
|
|
616
|
-
}
|
|
617
|
-
if (responsesOutcome.outcome === "aborted") {
|
|
618
|
-
return { cancel: true };
|
|
619
|
-
}
|
|
620
|
-
if (responsesOutcome.outcome === "unprojected-input") {
|
|
621
|
-
const message = responsesOutcome.reason === COMPACTION_CHECKPOINT_CONTEXT_EDITED
|
|
622
|
-
? "A context edit changed history sealed in the opaque checkpoint. Start a new session or navigate before that checkpoint; its encrypted content cannot be selectively rewritten."
|
|
623
|
-
: responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
|
|
624
|
-
? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
|
|
625
|
-
: responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
|
|
626
|
-
? "Pi did not provide the current session context required by its context-hook projection."
|
|
627
|
-
: responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
|
|
628
|
-
? "Pi's projected context did not include the current compaction summary anchor."
|
|
629
|
-
: "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
|
|
630
|
-
writeDebugArtifact(
|
|
631
|
-
"compaction-event",
|
|
632
|
-
{
|
|
633
|
-
event: "session_before_compact.remote-v2-unprojected-input",
|
|
634
|
-
reason: responsesOutcome.reason,
|
|
635
|
-
contextSource: config.remoteV2ContextSource,
|
|
636
|
-
inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
637
|
-
piContextHookPatch: dependencies.piContextHookPatch,
|
|
638
|
-
message,
|
|
639
|
-
},
|
|
640
|
-
config,
|
|
641
|
-
ctx,
|
|
642
|
-
);
|
|
643
|
-
notifyWarning(
|
|
644
|
-
ctx,
|
|
645
|
-
`Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} The existing checkpoint and session history were left unchanged.`,
|
|
646
|
-
);
|
|
647
|
-
return { cancel: true };
|
|
648
|
-
}
|
|
649
|
-
// Only an actual exhausted remote attempt warns, never an input/routing skip.
|
|
650
|
-
if (event.signal?.aborted) return { cancel: true };
|
|
651
|
-
if (responsesOutcome.remoteFailure) {
|
|
652
|
-
const failure = responsesOutcome.remoteFailure;
|
|
653
|
-
writeDebugArtifact("compaction-event", {
|
|
654
|
-
event: "session_before_compact.remote-to-native-summary",
|
|
655
|
-
protocol: responsesOutcome.protocol,
|
|
656
|
-
...failure,
|
|
657
|
-
nextStep: "native-summary",
|
|
658
|
-
}, config, ctx);
|
|
659
|
-
notifyWarning(ctx, `Remote compaction failed (${failure.reason}${failure.status ? `, HTTP ${failure.status}` : ""}${failure.errorCode ? `, ${failure.errorCode}` : ""}); continuing with a native text summary. No encrypted remote checkpoint was produced.${failure.errorMessage ? ` ${failure.errorMessage}` : ""}`);
|
|
660
|
-
}
|
|
661
|
-
} else {
|
|
662
|
-
writeDebugArtifact(
|
|
663
|
-
"compaction-event",
|
|
664
|
-
{
|
|
665
|
-
event: "session_before_compact.remote-v2-unavailable",
|
|
666
|
-
reason: resolution.reason,
|
|
667
|
-
provider: resolution.provider,
|
|
668
|
-
api: resolution.api,
|
|
669
|
-
model: resolution.model,
|
|
670
|
-
baseUrl: resolution.baseUrl,
|
|
671
|
-
modelSpec: resolution.modelSpec,
|
|
672
|
-
errorMessage: resolution.errorMessage,
|
|
673
|
-
},
|
|
674
|
-
config,
|
|
675
|
-
ctx,
|
|
676
|
-
);
|
|
677
|
-
const expectedConsumerCapabilityMiss =
|
|
678
|
-
resolution.reason === "unsupported-api" && resolution.modelSpec === undefined;
|
|
679
|
-
if (config.remoteCompactModel && !expectedConsumerCapabilityMiss) {
|
|
680
|
-
notifyWarning(
|
|
681
|
-
ctx,
|
|
682
|
-
`remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// Branch 2: run pi's native compaction method. A failed remote request is compacted by the
|
|
688
|
-
// remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
|
|
689
|
-
const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
|
|
690
|
-
if (!isCurrent() || event.signal.aborted || refuseRetiredSession(ctx)) return { cancel: true };
|
|
691
|
-
const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec, isCurrent });
|
|
692
|
-
if (!isCurrent() || refuseRetiredSession(ctx)) return { cancel: true };
|
|
693
|
-
if (fallback.ok) {
|
|
694
|
-
if (ctx.hasUI) {
|
|
695
|
-
ctx.ui.notify(
|
|
696
|
-
`${COMPACTION_EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
|
|
697
|
-
"info",
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
writeDebugArtifact(
|
|
701
|
-
"compaction-event",
|
|
702
|
-
{
|
|
703
|
-
event: "session_before_compact.fallback-success",
|
|
704
|
-
model: fallback.model,
|
|
705
|
-
},
|
|
706
|
-
config,
|
|
707
|
-
ctx,
|
|
708
|
-
);
|
|
709
|
-
return { compaction: fallback.result };
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (fallback.reason === "aborted") {
|
|
713
|
-
return { cancel: true };
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
writeDebugArtifact(
|
|
717
|
-
"compaction-event",
|
|
718
|
-
{
|
|
719
|
-
event: "session_before_compact.fallback-skip",
|
|
720
|
-
reason: fallback.reason,
|
|
721
|
-
modelSpec: fallback.modelSpec,
|
|
722
|
-
errorMessage: fallback.errorMessage,
|
|
723
|
-
},
|
|
724
|
-
config,
|
|
725
|
-
ctx,
|
|
726
|
-
);
|
|
727
|
-
|
|
728
|
-
// Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
|
|
729
|
-
const intentionalSkip =
|
|
730
|
-
fallback.reason === "disabled" ||
|
|
731
|
-
fallback.reason === "no-model-configured" ||
|
|
732
|
-
fallback.reason === "same-as-current-model";
|
|
733
|
-
if (!intentionalSkip) {
|
|
734
|
-
// An explicit summary model is authoritative: the active model may be
|
|
735
|
-
// unable to summarize at all. Preserve context on failure, never reroute.
|
|
736
|
-
notifyWarning(
|
|
737
|
-
ctx,
|
|
738
|
-
`compaction model "${fallback.modelSpec ?? fallbackModelSpec}" failed (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); compaction cancelled, context preserved; not switching to the current model`,
|
|
739
|
-
);
|
|
740
|
-
return { cancel: true };
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
return undefined;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
async function handleContextInternal(
|
|
747
|
-
event: ContextWithSystemEvent,
|
|
748
|
-
ctx: ExtensionContext,
|
|
749
|
-
loadConfig: typeof loadToolkitConfig,
|
|
750
|
-
) {
|
|
751
|
-
if (refuseRetiredSession(ctx, true)) return undefined;
|
|
752
|
-
const resolved = contextOperation(loadConfig, ctx);
|
|
753
|
-
requireContextPolicy(resolved, ctx);
|
|
754
|
-
const config = resolved.config.compaction;
|
|
755
|
-
if (!config.enabled) return undefined;
|
|
756
|
-
const branchEntries = ctx.sessionManager.getBranch();
|
|
757
|
-
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
758
|
-
enabled: config.enabled,
|
|
759
|
-
responsesApis: config.responsesApis,
|
|
760
|
-
codexGatewayModels: resolved.gatewayModelKeys,
|
|
761
|
-
});
|
|
762
|
-
if (refuseRetiredSession(ctx, true)) return undefined;
|
|
763
|
-
if (!resolution.ok) return undefined;
|
|
764
|
-
const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
|
|
765
|
-
if (!latest.ok) return undefined;
|
|
766
|
-
const result = removeNativeCompactionRetainedMessages({
|
|
767
|
-
messages: event.messages,
|
|
768
|
-
branchEntries,
|
|
769
|
-
compactionEntry: latest.entry,
|
|
770
|
-
expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
771
|
-
});
|
|
772
|
-
if (!result.ok) {
|
|
773
|
-
writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
|
|
774
|
-
if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
|
|
775
|
-
reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
|
|
776
|
-
ctx.abort();
|
|
777
|
-
return undefined;
|
|
778
|
-
}
|
|
779
|
-
return result.messages.length === event.messages.length && result.messages.every((message, index) => message === event.messages[index])
|
|
780
|
-
? undefined : { messages: result.messages };
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
|
|
785
|
-
* also report the failure so the compaction projector cannot mistake the
|
|
786
|
-
* caught exception for a successful projection.
|
|
787
|
-
*/
|
|
788
|
-
async function handleContext(
|
|
789
|
-
event: ContextWithSystemEvent,
|
|
790
|
-
ctx: ExtensionContext,
|
|
791
|
-
loadConfig: typeof loadToolkitConfig,
|
|
792
|
-
) {
|
|
793
|
-
try {
|
|
794
|
-
return await handleContextInternal(event, ctx, loadConfig);
|
|
795
|
-
} catch (error) {
|
|
796
|
-
reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
|
|
797
|
-
throw error;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
async function handleBeforeProviderRequest(
|
|
802
|
-
event: BeforeProviderRequestEvent,
|
|
803
|
-
ctx: ExtensionContext,
|
|
804
|
-
loadConfig: typeof loadToolkitConfig,
|
|
805
|
-
) {
|
|
806
|
-
if (refuseRetiredSession(ctx, true)) return undefined;
|
|
807
|
-
const resolved = contextOperation(loadConfig, ctx);
|
|
808
|
-
requireContextPolicy(resolved, ctx);
|
|
809
|
-
const toolkitConfig = resolved.config;
|
|
810
|
-
const config = toolkitConfig.compaction;
|
|
811
|
-
if (!config.enabled) return undefined;
|
|
812
|
-
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
813
|
-
enabled: config.enabled,
|
|
814
|
-
responsesApis: config.responsesApis,
|
|
815
|
-
codexGatewayModels: resolved.gatewayModelKeys,
|
|
816
|
-
}, event.payload);
|
|
817
|
-
if (refuseRetiredSession(ctx, true)) return undefined;
|
|
818
|
-
const branchEntries = ctx.sessionManager.getBranch();
|
|
819
|
-
if (resolution.ok === false) {
|
|
820
|
-
writeDebugArtifact(
|
|
821
|
-
"provider-request",
|
|
822
|
-
{
|
|
823
|
-
event: "before_provider_request.skip",
|
|
824
|
-
reason: resolution.reason,
|
|
825
|
-
provider: resolution.provider,
|
|
826
|
-
api: resolution.api,
|
|
827
|
-
model: resolution.model,
|
|
828
|
-
baseUrl: resolution.baseUrl,
|
|
829
|
-
errorMessage: resolution.errorMessage,
|
|
830
|
-
currentModel: getCurrentModelDebugInfo(ctx),
|
|
831
|
-
payload: event.payload,
|
|
832
|
-
},
|
|
833
|
-
config,
|
|
834
|
-
ctx,
|
|
835
|
-
);
|
|
836
|
-
return undefined;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const runtime = resolution.runtime;
|
|
840
|
-
const payload = runtime.payload;
|
|
841
|
-
if (!payload) {
|
|
842
|
-
return undefined;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
// Capture compact-relevant request fields (tools, reasoning, ...) for the next
|
|
846
|
-
// synthetic compact request using the active consumer's effective runtime identity.
|
|
847
|
-
// This hook runs before the separate Web Search transform, so injected native search
|
|
848
|
-
// tools are not copied into remote_compaction_v2.
|
|
849
|
-
const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
|
|
850
|
-
rememberRequestContext(
|
|
851
|
-
payload,
|
|
852
|
-
{
|
|
853
|
-
provider: runtime.provider,
|
|
854
|
-
api: runtime.api,
|
|
855
|
-
model: runtime.requestModel ?? runtime.model,
|
|
856
|
-
baseUrl: runtime.baseUrl,
|
|
857
|
-
sessionId: getSessionId(ctx),
|
|
858
|
-
},
|
|
859
|
-
{
|
|
860
|
-
excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
|
|
861
|
-
},
|
|
862
|
-
);
|
|
863
|
-
|
|
864
|
-
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
865
|
-
baseUrl: runtime.baseUrl,
|
|
866
|
-
});
|
|
867
|
-
if (!latestNativeCompaction.ok) {
|
|
868
|
-
writeDebugArtifact(
|
|
869
|
-
"provider-request",
|
|
870
|
-
{
|
|
871
|
-
event: "before_provider_request.no-native-compaction",
|
|
872
|
-
reason: latestNativeCompaction.reason,
|
|
873
|
-
provider: runtime.provider,
|
|
874
|
-
api: runtime.api,
|
|
875
|
-
model: runtime.model,
|
|
876
|
-
baseUrl: runtime.baseUrl,
|
|
877
|
-
branchEntries: branchEntries.length,
|
|
878
|
-
latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
|
|
879
|
-
latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
|
|
880
|
-
payload,
|
|
881
|
-
},
|
|
882
|
-
config,
|
|
883
|
-
ctx,
|
|
884
|
-
);
|
|
885
|
-
return undefined;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const latestNativeCompactionEntry = latestNativeCompaction.entry;
|
|
889
|
-
const rewrite = rewriteResponsesPayloadWithNativeReplay({
|
|
890
|
-
model: runtime.currentModel,
|
|
891
|
-
payload,
|
|
892
|
-
branchEntries,
|
|
893
|
-
compactionEntry: latestNativeCompactionEntry,
|
|
894
|
-
expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
895
|
-
});
|
|
896
|
-
if (!rewrite.ok) {
|
|
897
|
-
writeDebugArtifact(
|
|
898
|
-
"provider-request",
|
|
899
|
-
{
|
|
900
|
-
event: "before_provider_request.rewrite-failed",
|
|
901
|
-
reason: rewrite.reason,
|
|
902
|
-
provider: runtime.provider,
|
|
903
|
-
api: runtime.api,
|
|
904
|
-
model: runtime.model,
|
|
905
|
-
baseUrl: runtime.baseUrl,
|
|
906
|
-
compactionEntryId: latestNativeCompactionEntry.id,
|
|
907
|
-
parity: rewrite.parity,
|
|
908
|
-
payload,
|
|
909
|
-
},
|
|
910
|
-
config,
|
|
911
|
-
ctx,
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
// Fail loud instead of letting Pi send the sentinel-only payload: the
|
|
915
|
-
// compacted history would be silently lost while the request still succeeds.
|
|
916
|
-
// A forced redacted failure record is written even when logProviderPayloads
|
|
917
|
-
// is disabled so the incident is diagnosable without leaking content.
|
|
918
|
-
writeReplayFailureArtifact(
|
|
919
|
-
{
|
|
920
|
-
reason: rewrite.reason,
|
|
921
|
-
parity: rewrite.parity,
|
|
922
|
-
compactionEntryId: latestNativeCompactionEntry.id,
|
|
923
|
-
provider: runtime.provider,
|
|
924
|
-
api: runtime.api,
|
|
925
|
-
model: runtime.model,
|
|
926
|
-
},
|
|
927
|
-
config,
|
|
928
|
-
ctx,
|
|
929
|
-
);
|
|
930
|
-
if (ctx.hasUI) {
|
|
931
|
-
ctx.ui.notify(
|
|
932
|
-
`${COMPACTION_EXTENSION_ID}: native compaction replay failed (${rewrite.reason}); provider request aborted`,
|
|
933
|
-
"error",
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
ctx.abort();
|
|
937
|
-
return undefined;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
writeDebugArtifact(
|
|
941
|
-
"provider-request",
|
|
942
|
-
{
|
|
943
|
-
event: "before_provider_request.native-rewrite",
|
|
944
|
-
provider: runtime.provider,
|
|
945
|
-
api: runtime.api,
|
|
946
|
-
model: runtime.model,
|
|
947
|
-
baseUrl: runtime.baseUrl,
|
|
948
|
-
compactionEntryId: latestNativeCompactionEntry.id,
|
|
949
|
-
boundaryIndex: rewrite.segments.boundaryIndex,
|
|
950
|
-
firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
|
|
951
|
-
originalInputItems: payload.input.length,
|
|
952
|
-
rewrittenInputItems: rewrite.rewrittenPayload.input.length,
|
|
953
|
-
leadingItems: rewrite.segments.leading.length,
|
|
954
|
-
compactionSummaryItems: rewrite.segments.compactionSummary.length,
|
|
955
|
-
compactedItems: rewrite.segments.compactedWindow.length,
|
|
956
|
-
postItems: rewrite.segments.post.length,
|
|
957
|
-
payload: rewrite.rewrittenPayload,
|
|
958
|
-
originalPayload: payload,
|
|
959
|
-
},
|
|
960
|
-
config,
|
|
961
|
-
ctx,
|
|
962
|
-
);
|
|
963
|
-
|
|
964
|
-
return rewrite.rewrittenPayload;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
export default function registerCompactionExtension(
|
|
968
|
-
pi: ExtensionAPI,
|
|
969
|
-
overrides: Partial<CompactionDependencies> = {},
|
|
970
|
-
) {
|
|
971
|
-
const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
|
|
972
|
-
registerToolkitConfigCommand(pi, loadConfig);
|
|
973
|
-
registerPhysicalModelObserver(pi);
|
|
974
|
-
const piContextHookPatch = installPiContextHookPatch();
|
|
975
|
-
let compactionGeneration = 0;
|
|
976
|
-
const invalidateCompaction = () => { compactionGeneration++; };
|
|
977
|
-
const dependencies: CompactionDependencies = {
|
|
978
|
-
remoteCompact: executeRemoteV2Compaction,
|
|
979
|
-
standaloneCompact: executeNativeCompaction,
|
|
980
|
-
nativeFallback: runNativeFallbackCompaction,
|
|
981
|
-
...overrides,
|
|
982
|
-
loadConfig,
|
|
983
|
-
piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
|
|
984
|
-
};
|
|
985
|
-
pi.on("session_start", (_event, ctx) => {
|
|
986
|
-
invalidateCompaction();
|
|
987
|
-
if (refuseRetiredSession(ctx)) return;
|
|
988
|
-
const resolved = contextOperation(loadConfig, ctx);
|
|
989
|
-
const config = resolved.config.compaction;
|
|
990
|
-
if (resolved.invalidFeatures.length || !config.enabled) return;
|
|
991
|
-
const artifactPath = writeDebugArtifact("lifecycle", {
|
|
992
|
-
event: "session_start", config, configSource: resolved.source,
|
|
993
|
-
warnings: resolved.issues.map((issue) => `${issue.path}: ${issue.code}`),
|
|
994
|
-
piContextHookPatch: dependencies.piContextHookPatch,
|
|
995
|
-
}, config, ctx);
|
|
996
|
-
if (ctx.hasUI && ["info", "debug"].includes(resolved.policy.diagnostics.level) && (config.notifyOnLoad || config.debug)) {
|
|
997
|
-
ctx.ui.notify(artifactPath ? `${COMPACTION_EXTENSION_ID} loaded • debug artifacts → ${artifactPath}` : `${COMPACTION_EXTENSION_ID} loaded`, "info");
|
|
998
|
-
}
|
|
999
|
-
});
|
|
1000
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
1001
|
-
invalidateCompaction();
|
|
1002
|
-
refuseRetiredSession(ctx);
|
|
1003
|
-
});
|
|
1004
|
-
pi.on("context_with_system", (event, ctx) => handleContext(event, ctx, loadConfig));
|
|
1005
|
-
pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, loadConfig));
|
|
1006
|
-
pi.on("session_before_compact", async (event, ctx) => {
|
|
1007
|
-
invalidateCompaction();
|
|
1008
|
-
if (refuseRetiredSession(ctx)) return { cancel: true };
|
|
1009
|
-
try {
|
|
1010
|
-
const generation = compactionGeneration;
|
|
1011
|
-
const branch = structuredClone(ctx.sessionManager.getBranch());
|
|
1012
|
-
const sessionId = getSessionId(ctx);
|
|
1013
|
-
const model = ctx.model ? { ...ctx.model } : undefined;
|
|
1014
|
-
const thinking = ctx.thinkingLevel;
|
|
1015
|
-
// Clients classify aborts. This fence handles stale ownership: a
|
|
1016
|
-
// validated V2 terminal still wins over cleanup-triggered cancellation.
|
|
1017
|
-
const isCurrent = () => {
|
|
1018
|
-
try {
|
|
1019
|
-
return generation === compactionGeneration
|
|
1020
|
-
&& getSessionId(ctx) === sessionId && isDeepStrictEqual(branch, ctx.sessionManager.getBranch())
|
|
1021
|
-
&& isDeepStrictEqual(model, ctx.model) && thinking === ctx.thinkingLevel
|
|
1022
|
-
&& !retirementReason(ctx)
|
|
1023
|
-
&& !(event.reason === "manual" && typeof ctx.hasPendingMessages === "function" && ctx.hasPendingMessages());
|
|
1024
|
-
} catch { return false; }
|
|
1025
|
-
};
|
|
1026
|
-
const result = await handleSessionBeforeCompact(event, ctx, { ...dependencies, isCurrentCompaction: isCurrent });
|
|
1027
|
-
if (!isCurrent()) return { cancel: true };
|
|
1028
|
-
if (result && "compaction" in result && result.compaction) {
|
|
1029
|
-
// Pi consumes results after later awaited observers. Never persist a
|
|
1030
|
-
// stale candidate or drain/consume the host's queues.
|
|
1031
|
-
return { compaction: result.compaction, get cancel() { return !isCurrent(); } };
|
|
1032
|
-
}
|
|
1033
|
-
if (result === undefined) {
|
|
1034
|
-
// Default summarization starts only after later observers finish too.
|
|
1035
|
-
// Keep the host handoff revocable without claiming its compaction.
|
|
1036
|
-
return { get cancel() { return !isCurrent(); } };
|
|
1037
|
-
}
|
|
1038
|
-
return result;
|
|
1039
|
-
} catch {
|
|
1040
|
-
// Pi can treat a rejected hook as absent and run its implicit summary.
|
|
1041
|
-
try { notifyWarning(ctx, "Compaction stopped; existing context preserved. Resume manually."); } catch { /* stale UI */ }
|
|
1042
|
-
return { cancel: true };
|
|
1043
|
-
}
|
|
1044
|
-
});
|
|
1045
|
-
pi.on("session_compact", invalidateCompaction);
|
|
1046
|
-
pi.on("session_compact_failed", invalidateCompaction);
|
|
1047
|
-
pi.on("session_shutdown", invalidateCompaction);
|
|
1048
|
-
pi.on("model_select", invalidateCompaction);
|
|
1049
|
-
pi.on("before_agent_start", invalidateCompaction);
|
|
1050
|
-
pi.on("agent_start", invalidateCompaction);
|
|
1051
|
-
pi.on("input", invalidateCompaction);
|
|
1052
|
-
pi.on("thinking_level_select", invalidateCompaction);
|
|
1053
|
-
pi.on("message_start", invalidateCompaction);
|
|
1054
|
-
pi.on("tool_call", invalidateCompaction);
|
|
1055
|
-
pi.on("session_before_switch", invalidateCompaction);
|
|
1056
|
-
pi.on("session_before_fork", invalidateCompaction);
|
|
1057
|
-
pi.on("session_before_tree", invalidateCompaction);
|
|
1058
|
-
|
|
1059
|
-
// Exact gateway compatibility is shared transport policy, independent of
|
|
1060
|
-
// compaction enablement. No window identities or ingest headers are generated.
|
|
1061
|
-
pi.on("before_provider_headers", async (event, ctx) => {
|
|
1062
|
-
if (refuseRetiredSession(ctx, true)) return;
|
|
1063
|
-
const resolved = contextOperation(loadConfig, ctx);
|
|
1064
|
-
requireContextPolicy(resolved, ctx);
|
|
1065
|
-
if (!resolved.gatewayModelKeys.length) return;
|
|
1066
|
-
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
1067
|
-
responsesApis: ["openai-responses"], codexGatewayModels: resolved.gatewayModelKeys,
|
|
1068
|
-
});
|
|
1069
|
-
if (refuseRetiredSession(ctx, true)) return;
|
|
1070
|
-
if (!resolution.ok) {
|
|
1071
|
-
const selected = isExactModelAllowed(ctx.model, resolved.gatewayModelKeys);
|
|
1072
|
-
const physical = resolution.provider && resolution.model
|
|
1073
|
-
? isExactModelAllowed({ provider: resolution.provider, id: resolution.model }, resolved.gatewayModelKeys) : false;
|
|
1074
|
-
if (selected || physical) {
|
|
1075
|
-
notifyWarning(ctx, "Configured gateway transport is unavailable; provider request aborted.");
|
|
1076
|
-
ctx.abort();
|
|
1077
|
-
}
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
const runtime = resolution.runtime;
|
|
1081
|
-
if (!runtime.codexAffinity) return;
|
|
1082
|
-
const gatewayHeaders = buildResponsesRequestHeaders(runtime, { accept: "text/event-stream" });
|
|
1083
|
-
const allowed = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
|
|
1084
|
-
for (const existing of Object.keys(event.headers)) {
|
|
1085
|
-
if (!allowed.has(existing.toLowerCase())) delete event.headers[existing];
|
|
1086
|
-
}
|
|
1087
|
-
for (const name of ["authorization", "originator", "user-agent", "session-id", "x-client-request-id", "x-codex-affinity-scope", "x-codex-model"]) {
|
|
1088
|
-
for (const existing of Object.keys(event.headers)) {
|
|
1089
|
-
if (existing.toLowerCase() === name) delete event.headers[existing];
|
|
1090
|
-
}
|
|
1091
|
-
const value = gatewayHeaders.get(name);
|
|
1092
|
-
if (value) event.headers[name] = value;
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type {
|
|
4
|
+
BeforeProviderRequestEvent,
|
|
5
|
+
ContextWithSystemEvent,
|
|
6
|
+
CompactionResult,
|
|
7
|
+
ExtensionAPI,
|
|
8
|
+
ExtensionContext,
|
|
9
|
+
SessionBeforeCompactEvent,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { assertConfigValid, loadToolkitConfig, resolveToolkitConfig, type ResolvedToolkitConfig } from "./config";
|
|
12
|
+
import { notifyConfigIssues } from "./config/notifications";
|
|
13
|
+
import { registerToolkitConfigCommand } from "./config-command";
|
|
14
|
+
import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
|
|
15
|
+
import {
|
|
16
|
+
COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
17
|
+
COMPACTION_CHECKPOINT_CONTEXT_EDITED,
|
|
18
|
+
COMPACTION_PROJECTION_UNAVAILABLE,
|
|
19
|
+
COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
|
|
20
|
+
COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
|
|
21
|
+
hasVerifiedCompactionInputProvenance,
|
|
22
|
+
type UnprojectedCompactionReason,
|
|
23
|
+
} from "./compaction-projection";
|
|
24
|
+
import { buildResponsesRequestHeaders, CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
|
|
25
|
+
import { isExactModelAllowed } from "./model-scope";
|
|
26
|
+
import { detectRetiredRemoteWindows, RETIRED_REMOTE_WINDOWS_MESSAGE } from "./retired-remote-windows";
|
|
27
|
+
import { executeNativeCompaction, type NativeCompactionClientResult } from "./compact-client";
|
|
28
|
+
import { qualifiesForStandaloneCompaction, type CompactionErrorInfo } from "./compaction-errors";
|
|
29
|
+
import { resolveLatestNativeCompactionEntry } from "./details-store";
|
|
30
|
+
import {
|
|
31
|
+
getPiContextHookProjector,
|
|
32
|
+
installPiContextHookPatch,
|
|
33
|
+
reportPiContextHookFailure,
|
|
34
|
+
type PiContextHookPatchResult,
|
|
35
|
+
} from "./pi-context-hook";
|
|
36
|
+
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
37
|
+
import {
|
|
38
|
+
rewriteResponsesPayloadWithNativeReplay,
|
|
39
|
+
hasInvalidatedCompactionContext,
|
|
40
|
+
removeNativeCompactionRetainedMessages,
|
|
41
|
+
serializeLiveTailToResponsesInputWithLimits,
|
|
42
|
+
} from "./payload-rewrite";
|
|
43
|
+
import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
|
|
44
|
+
import { resolveWebSearchRoute } from "./web-search/types";
|
|
45
|
+
import { executeRemoteV2Compaction } from "./remote-v2-client";
|
|
46
|
+
import {
|
|
47
|
+
registerPhysicalModelObserver,
|
|
48
|
+
resolveNativeCompactionEnvironment,
|
|
49
|
+
resolveRemoteCompactionExecution,
|
|
50
|
+
isStandaloneCompactionEligible,
|
|
51
|
+
parseModelSpec,
|
|
52
|
+
type RemoteCompactionExecution,
|
|
53
|
+
} from "./runtime";
|
|
54
|
+
import {
|
|
55
|
+
serializeMessagesToCompactRequestWithLimits,
|
|
56
|
+
serializeMessagesToResponsesInputWithLimits,
|
|
57
|
+
type NativeCompactionRequestBody,
|
|
58
|
+
type ResponsesInputItem,
|
|
59
|
+
} from "./serializer";
|
|
60
|
+
import {
|
|
61
|
+
createNativeCompactionDetails,
|
|
62
|
+
createNativeCompactionResult,
|
|
63
|
+
COMPACTION_EXTENSION_ID,
|
|
64
|
+
LEGACY_NATIVE_COMPACTION_STRATEGY,
|
|
65
|
+
REMOTE_V2_COMPACTION_STRATEGY,
|
|
66
|
+
getLatestDeferredToolCarryover,
|
|
67
|
+
getRemoteV2InputProvenance,
|
|
68
|
+
isNativeCompactionDetails,
|
|
69
|
+
type CompactionConfig,
|
|
70
|
+
type NativeCompactionDetails,
|
|
71
|
+
type NativeCompactionRequestMeta,
|
|
72
|
+
} from "./types";
|
|
73
|
+
|
|
74
|
+
type CompactionContextProjection = (
|
|
75
|
+
messages: readonly AgentMessage[],
|
|
76
|
+
ctx: ExtensionContext,
|
|
77
|
+
) => readonly AgentMessage[] | undefined | Promise<readonly AgentMessage[] | undefined>;
|
|
78
|
+
|
|
79
|
+
function contextOperation(loadConfig: typeof loadToolkitConfig, ctx: ExtensionContext, model = ctx.model): ResolvedToolkitConfig {
|
|
80
|
+
const loaded = loadConfig();
|
|
81
|
+
const resolved = resolveToolkitConfig(loaded, model);
|
|
82
|
+
notifyConfigIssues(ctx, resolved);
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function requireContextPolicy(resolved: ResolvedToolkitConfig, ctx: ExtensionContext): void {
|
|
87
|
+
try {
|
|
88
|
+
assertConfigValid(resolved, "context", "compatibility", "diagnostics");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (typeof ctx.abort === "function") ctx.abort();
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type CompactionDependencies = {
|
|
96
|
+
loadConfig: typeof loadToolkitConfig;
|
|
97
|
+
remoteCompact: typeof executeRemoteV2Compaction;
|
|
98
|
+
standaloneCompact: typeof executeNativeCompaction;
|
|
99
|
+
nativeFallback: typeof runNativeFallbackCompaction;
|
|
100
|
+
/** Revalidate operation ownership after awaits and at result consumption. */
|
|
101
|
+
isCurrentCompaction?: () => boolean;
|
|
102
|
+
/** Test seam for the same ordered projection exposed by the Pi host patch. */
|
|
103
|
+
projectCompactionContext?: CompactionContextProjection;
|
|
104
|
+
piContextHookPatch: PiContextHookPatchResult;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Notices are bounded, but every boundary freshly reads the current branch.
|
|
108
|
+
const retirementNotices = new WeakMap<object, string>();
|
|
109
|
+
function retirementReason(ctx: ExtensionContext): string | undefined {
|
|
110
|
+
try { return detectRetiredRemoteWindows(ctx.sessionManager.getBranch()); }
|
|
111
|
+
catch { return "branch-unavailable"; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function refuseRetiredSession(ctx: ExtensionContext, request = false): boolean {
|
|
115
|
+
const reason = retirementReason(ctx);
|
|
116
|
+
const owner = ctx.sessionManager;
|
|
117
|
+
if (!reason) {
|
|
118
|
+
retirementNotices.delete(owner);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const fingerprint = `${getSessionId(ctx) ?? ""}:${reason}`;
|
|
122
|
+
if (ctx.hasUI && retirementNotices.get(owner) !== fingerprint) {
|
|
123
|
+
retirementNotices.set(owner, fingerprint);
|
|
124
|
+
try {
|
|
125
|
+
ctx.ui.notify(
|
|
126
|
+
reason === "branch-unavailable"
|
|
127
|
+
? `${COMPACTION_EXTENSION_ID}: Cannot verify the active session branch; request blocked. Reopen the session before continuing.`
|
|
128
|
+
: `${COMPACTION_EXTENSION_ID}: ${RETIRED_REMOTE_WINDOWS_MESSAGE}`,
|
|
129
|
+
"error",
|
|
130
|
+
);
|
|
131
|
+
} catch {
|
|
132
|
+
// A detached UI must not prevent abort or turn compact refusal into fallback.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (request) {
|
|
136
|
+
reportPiContextHookFailure(ctx, `retired-remote-windows:${reason}`);
|
|
137
|
+
ctx.abort();
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type ResponsesCompactOutcome =
|
|
143
|
+
| { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
|
|
144
|
+
| { outcome: "aborted" }
|
|
145
|
+
| { outcome: "failed"; remoteFailure?: CompactionErrorInfo & { reason: string; status?: number }; protocol?: string }
|
|
146
|
+
| {
|
|
147
|
+
outcome: "unprojected-input";
|
|
148
|
+
reason: UnprojectedCompactionReason;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function buildCompactionRequestMeta(event: SessionBeforeCompactEvent): NativeCompactionRequestMeta {
|
|
152
|
+
return {
|
|
153
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
154
|
+
previousSummaryPresent: Boolean(event.preparation.previousSummary),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getCurrentModelDebugInfo(ctx: ExtensionContext) {
|
|
159
|
+
return ctx.model
|
|
160
|
+
? {
|
|
161
|
+
provider: ctx.model.provider,
|
|
162
|
+
id: ctx.model.id,
|
|
163
|
+
}
|
|
164
|
+
: undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getCompactionIdentityDebugInfo(entry: { details?: unknown } | undefined) {
|
|
168
|
+
return isNativeCompactionDetails(entry?.details)
|
|
169
|
+
? {
|
|
170
|
+
provider: entry.details.provider,
|
|
171
|
+
api: entry.details.api,
|
|
172
|
+
model: entry.details.model,
|
|
173
|
+
baseUrl: entry.details.baseUrl,
|
|
174
|
+
compactionModel: entry.details.compactionModel,
|
|
175
|
+
}
|
|
176
|
+
: undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getSessionId(ctx: ExtensionContext): string | undefined {
|
|
180
|
+
try {
|
|
181
|
+
return ctx.sessionManager.getSessionId();
|
|
182
|
+
} catch {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function notifyWarning(ctx: ExtensionContext, message: string): void {
|
|
188
|
+
if (ctx.hasUI) {
|
|
189
|
+
ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${message}`, "warning");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
|
|
194
|
+
return window.map((item) => structuredClone(item));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildCompactionInstructions(systemPrompt: string, customInstructions?: string): string {
|
|
198
|
+
const guidance = customInstructions?.trim();
|
|
199
|
+
if (!guidance) {
|
|
200
|
+
return systemPrompt;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return `${systemPrompt}\n\nAdditional user guidance for this manual /compact request:\n${guidance}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function findLatestProjectedCompactionSummaryIndex(
|
|
207
|
+
messages: readonly AgentMessage[],
|
|
208
|
+
summary: string,
|
|
209
|
+
): number {
|
|
210
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
211
|
+
const message = messages[index];
|
|
212
|
+
if (message?.role === "compactionSummary" && message.summary === summary) {
|
|
213
|
+
return index;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return -1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readSessionContextMessages(ctx: ExtensionContext): AgentMessage[] | undefined {
|
|
221
|
+
try {
|
|
222
|
+
const projection = ctx.sessionManager.buildSessionProjection();
|
|
223
|
+
if (!projection || !Array.isArray(projection.messages)) return undefined;
|
|
224
|
+
return structuredClone(projection.messages);
|
|
225
|
+
} catch {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
type CompactionProjectionResult =
|
|
231
|
+
| { ok: true; messages: readonly AgentMessage[] }
|
|
232
|
+
| { ok: false; reason: typeof COMPACTION_PROJECTION_UNAVAILABLE | typeof COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
|
|
233
|
+
|
|
234
|
+
async function projectSessionContextForCompaction(
|
|
235
|
+
ctx: ExtensionContext,
|
|
236
|
+
projectCompactionContext: CompactionContextProjection | undefined,
|
|
237
|
+
): Promise<CompactionProjectionResult> {
|
|
238
|
+
const messages = readSessionContextMessages(ctx);
|
|
239
|
+
if (!messages) {
|
|
240
|
+
return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const projector = projectCompactionContext ?? getPiContextHookProjector(ctx);
|
|
244
|
+
if (!projector) {
|
|
245
|
+
return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const projected = await projector(messages, ctx);
|
|
250
|
+
return Array.isArray(projected)
|
|
251
|
+
? { ok: true, messages: projected }
|
|
252
|
+
: { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
253
|
+
} catch {
|
|
254
|
+
return { ok: false, reason: COMPACTION_PROJECTION_UNAVAILABLE };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getLegacySessionContextMessages(
|
|
259
|
+
event: SessionBeforeCompactEvent,
|
|
260
|
+
ctx: ExtensionContext,
|
|
261
|
+
): AgentMessage[] {
|
|
262
|
+
return readSessionContextMessages(ctx) ?? [
|
|
263
|
+
...event.preparation.messagesToSummarize,
|
|
264
|
+
...event.preparation.turnPrefixMessages,
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function runResponsesNativeCompact(
|
|
269
|
+
event: SessionBeforeCompactEvent,
|
|
270
|
+
ctx: ExtensionContext,
|
|
271
|
+
config: CompactionConfig,
|
|
272
|
+
execution: RemoteCompactionExecution,
|
|
273
|
+
remoteCompact: typeof executeRemoteV2Compaction,
|
|
274
|
+
projectCompactionContext: CompactionContextProjection | undefined,
|
|
275
|
+
standaloneCompact: typeof executeNativeCompaction,
|
|
276
|
+
isCurrent: () => boolean,
|
|
277
|
+
): Promise<ResponsesCompactOutcome> {
|
|
278
|
+
const { consumer, compactor } = execution;
|
|
279
|
+
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
280
|
+
const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
|
|
281
|
+
const deferredToolCarryover = getLatestDeferredToolCarryover(branchEntries);
|
|
282
|
+
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
283
|
+
baseUrl: consumer.baseUrl,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
|
|
287
|
+
let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
|
|
288
|
+
let request: NativeCompactionRequestBody;
|
|
289
|
+
if (latestNativeCompaction.ok) {
|
|
290
|
+
if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
|
|
291
|
+
return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
|
|
292
|
+
}
|
|
293
|
+
const details = latestNativeCompaction.entry.details;
|
|
294
|
+
if (!details) {
|
|
295
|
+
return { outcome: "failed" };
|
|
296
|
+
}
|
|
297
|
+
if (!hasVerifiedCompactionInputProvenance(details, inputProvenance)) {
|
|
298
|
+
return {
|
|
299
|
+
outcome: "unprojected-input",
|
|
300
|
+
reason: COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
requestSource = "latest-native-replay";
|
|
304
|
+
|
|
305
|
+
if (config.remoteV2ContextSource === "pi-context-hook") {
|
|
306
|
+
const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
|
|
307
|
+
if (!projection.ok) {
|
|
308
|
+
return { outcome: "unprojected-input", reason: projection.reason };
|
|
309
|
+
}
|
|
310
|
+
const summaryIndex = findLatestProjectedCompactionSummaryIndex(
|
|
311
|
+
projection.messages,
|
|
312
|
+
latestNativeCompaction.entry.summary,
|
|
313
|
+
);
|
|
314
|
+
if (summaryIndex < 0) {
|
|
315
|
+
return {
|
|
316
|
+
outcome: "unprojected-input",
|
|
317
|
+
reason: COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const input: ResponsesInputItem[] = [
|
|
321
|
+
...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
|
|
322
|
+
...(await serializeMessagesToResponsesInputWithLimits(
|
|
323
|
+
compactor.currentModel,
|
|
324
|
+
[...projection.messages.slice(summaryIndex + 1)],
|
|
325
|
+
{ firstSystemMessageIsUpdate: true },
|
|
326
|
+
)),
|
|
327
|
+
];
|
|
328
|
+
request = {
|
|
329
|
+
model: compactor.requestModel ?? compactor.model,
|
|
330
|
+
input,
|
|
331
|
+
instructions,
|
|
332
|
+
};
|
|
333
|
+
} else {
|
|
334
|
+
const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
|
|
335
|
+
const input: ResponsesInputItem[] = [
|
|
336
|
+
...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
|
|
337
|
+
...(await serializeLiveTailToResponsesInputWithLimits({ model: compactor.currentModel, entries: liveTailEntries })),
|
|
338
|
+
];
|
|
339
|
+
request = {
|
|
340
|
+
model: compactor.requestModel ?? compactor.model,
|
|
341
|
+
input,
|
|
342
|
+
instructions,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
} else if (
|
|
346
|
+
latestNativeCompaction.reason === "no-compaction" ||
|
|
347
|
+
(latestNativeCompaction.reason === "latest-compaction-not-native" &&
|
|
348
|
+
config.allowCompactionContinuityBreak)
|
|
349
|
+
) {
|
|
350
|
+
requestSource =
|
|
351
|
+
latestNativeCompaction.reason === "no-compaction" ? "session-context" : "non-native-session-context";
|
|
352
|
+
if (config.remoteV2ContextSource === "pi-context-hook") {
|
|
353
|
+
const projection = await projectSessionContextForCompaction(ctx, projectCompactionContext);
|
|
354
|
+
if (!projection.ok) {
|
|
355
|
+
return { outcome: "unprojected-input", reason: projection.reason };
|
|
356
|
+
}
|
|
357
|
+
const serialized = await serializeMessagesToCompactRequestWithLimits({
|
|
358
|
+
model: compactor.currentModel,
|
|
359
|
+
messages: [...projection.messages],
|
|
360
|
+
instructions,
|
|
361
|
+
});
|
|
362
|
+
request = { ...serialized, model: compactor.requestModel ?? serialized.model };
|
|
363
|
+
} else {
|
|
364
|
+
const serialized = await serializeMessagesToCompactRequestWithLimits({
|
|
365
|
+
model: compactor.currentModel,
|
|
366
|
+
messages: getLegacySessionContextMessages(event, ctx),
|
|
367
|
+
instructions,
|
|
368
|
+
});
|
|
369
|
+
request = { ...serialized, model: compactor.requestModel ?? serialized.model };
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
writeDebugArtifact(
|
|
373
|
+
"compaction-event",
|
|
374
|
+
{
|
|
375
|
+
event: "session_before_compact.remote-v2-skip",
|
|
376
|
+
reason: latestNativeCompaction.reason,
|
|
377
|
+
consumer: {
|
|
378
|
+
provider: consumer.provider,
|
|
379
|
+
api: consumer.api,
|
|
380
|
+
model: consumer.model,
|
|
381
|
+
baseUrl: consumer.baseUrl,
|
|
382
|
+
},
|
|
383
|
+
compactor: {
|
|
384
|
+
provider: compactor.provider,
|
|
385
|
+
api: compactor.api,
|
|
386
|
+
model: compactor.model,
|
|
387
|
+
baseUrl: compactor.baseUrl,
|
|
388
|
+
},
|
|
389
|
+
latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
|
|
390
|
+
latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
|
|
391
|
+
},
|
|
392
|
+
config,
|
|
393
|
+
ctx,
|
|
394
|
+
);
|
|
395
|
+
return { outcome: "failed" };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Mirror the latest codex_rs CompactionInput fields captured from the most
|
|
399
|
+
// recent live provider request for this model (tools, reasoning, etc.).
|
|
400
|
+
const extras = getCompactionRequestExtras({
|
|
401
|
+
provider: consumer.provider,
|
|
402
|
+
api: consumer.api,
|
|
403
|
+
model: consumer.requestModel ?? consumer.model,
|
|
404
|
+
baseUrl: consumer.baseUrl,
|
|
405
|
+
sessionId: getSessionId(ctx),
|
|
406
|
+
}, compactor.currentModel);
|
|
407
|
+
if (extras) {
|
|
408
|
+
request = { ...request, ...extras };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (event.signal?.aborted || !isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
|
|
412
|
+
let protocol: "remote-v2" | "standalone-compact" = "remote-v2";
|
|
413
|
+
let compactResult: Awaited<ReturnType<typeof executeRemoteV2Compaction>> | NativeCompactionClientResult = await remoteCompact({
|
|
414
|
+
runtime: compactor,
|
|
415
|
+
request,
|
|
416
|
+
signal: event.signal,
|
|
417
|
+
settings: config,
|
|
418
|
+
context: ctx,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (!isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
|
|
422
|
+
if (!compactResult.ok) {
|
|
423
|
+
if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
|
|
424
|
+
const tryStandalone = !!standaloneCompact && isStandaloneCompactionEligible(compactor) && qualifiesForStandaloneCompaction(compactResult);
|
|
425
|
+
writeDebugArtifact("compaction-event", {
|
|
426
|
+
event: "session_before_compact.remote-v2-failure",
|
|
427
|
+
protocol,
|
|
428
|
+
reason: compactResult.reason,
|
|
429
|
+
status: compactResult.status,
|
|
430
|
+
errorCode: compactResult.errorCode,
|
|
431
|
+
errorType: compactResult.errorType,
|
|
432
|
+
errorParam: compactResult.errorParam,
|
|
433
|
+
errorMessage: compactResult.errorMessage,
|
|
434
|
+
nextStep: tryStandalone ? "standalone-compact" : "native-summary",
|
|
435
|
+
}, config, ctx);
|
|
436
|
+
if (tryStandalone && standaloneCompact) {
|
|
437
|
+
if (event.signal?.aborted || !isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
|
|
438
|
+
protocol = "standalone-compact";
|
|
439
|
+
compactResult = await standaloneCompact({ runtime: compactor, request, signal: event.signal, settings: config, context: ctx });
|
|
440
|
+
}
|
|
441
|
+
if (!compactResult.ok) {
|
|
442
|
+
if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
|
|
443
|
+
const remoteFailure = {
|
|
444
|
+
reason: compactResult.reason,
|
|
445
|
+
status: compactResult.status,
|
|
446
|
+
errorCode: compactResult.errorCode,
|
|
447
|
+
errorType: compactResult.errorType,
|
|
448
|
+
errorParam: compactResult.errorParam,
|
|
449
|
+
errorMessage: compactResult.errorMessage,
|
|
450
|
+
};
|
|
451
|
+
if (protocol === "standalone-compact") writeDebugArtifact("compaction-event", {
|
|
452
|
+
event: "session_before_compact.standalone-compact-failure", protocol, ...remoteFailure, nextStep: "native-summary",
|
|
453
|
+
}, config, ctx);
|
|
454
|
+
return { outcome: "failed", remoteFailure, protocol };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let details: NativeCompactionDetails;
|
|
459
|
+
try {
|
|
460
|
+
details = createNativeCompactionDetails({
|
|
461
|
+
strategy: protocol === "standalone-compact" ? LEGACY_NATIVE_COMPACTION_STRATEGY : REMOTE_V2_COMPACTION_STRATEGY,
|
|
462
|
+
provider: consumer.provider,
|
|
463
|
+
api: consumer.api,
|
|
464
|
+
model: consumer.model,
|
|
465
|
+
baseUrl: consumer.baseUrl,
|
|
466
|
+
compactionModel: {
|
|
467
|
+
provider: compactor.provider,
|
|
468
|
+
api: compactor.api,
|
|
469
|
+
model: compactor.model,
|
|
470
|
+
baseUrl: compactor.baseUrl,
|
|
471
|
+
},
|
|
472
|
+
deferredToolCarryover,
|
|
473
|
+
compactedWindow: compactResult.compactedWindow,
|
|
474
|
+
compactResponseId: compactResult.compactResponseId,
|
|
475
|
+
createdAt: compactResult.createdAt,
|
|
476
|
+
requestMeta: buildCompactionRequestMeta(event),
|
|
477
|
+
inputProvenance,
|
|
478
|
+
});
|
|
479
|
+
} catch (error) {
|
|
480
|
+
writeDebugArtifact(
|
|
481
|
+
"compaction-event",
|
|
482
|
+
{
|
|
483
|
+
event: "session_before_compact.invalid-native-details",
|
|
484
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
485
|
+
consumer: {
|
|
486
|
+
provider: consumer.provider,
|
|
487
|
+
api: consumer.api,
|
|
488
|
+
model: consumer.model,
|
|
489
|
+
baseUrl: consumer.baseUrl,
|
|
490
|
+
},
|
|
491
|
+
compactor: {
|
|
492
|
+
provider: compactor.provider,
|
|
493
|
+
api: compactor.api,
|
|
494
|
+
model: compactor.model,
|
|
495
|
+
baseUrl: compactor.baseUrl,
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
config,
|
|
499
|
+
ctx,
|
|
500
|
+
);
|
|
501
|
+
return { outcome: "failed", remoteFailure: { reason: "invalid-native-details" }, protocol };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const compaction = createNativeCompactionResult({
|
|
505
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
506
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
507
|
+
details,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
writeDebugArtifact(
|
|
511
|
+
"compaction-event",
|
|
512
|
+
{
|
|
513
|
+
event: `session_before_compact.${protocol}-success`,
|
|
514
|
+
protocol,
|
|
515
|
+
consumer: {
|
|
516
|
+
provider: consumer.provider,
|
|
517
|
+
api: consumer.api,
|
|
518
|
+
model: consumer.model,
|
|
519
|
+
baseUrl: consumer.baseUrl,
|
|
520
|
+
},
|
|
521
|
+
compactor: {
|
|
522
|
+
provider: compactor.provider,
|
|
523
|
+
api: compactor.api,
|
|
524
|
+
model: compactor.model,
|
|
525
|
+
baseUrl: compactor.baseUrl,
|
|
526
|
+
},
|
|
527
|
+
requestSource,
|
|
528
|
+
remoteV2ContextSource: config.remoteV2ContextSource,
|
|
529
|
+
inputProvenance,
|
|
530
|
+
requestInputItems: request.input.length,
|
|
531
|
+
requestExtras: extras ? Object.keys(extras) : [],
|
|
532
|
+
compactResponseId: compactResult.compactResponseId,
|
|
533
|
+
compactedItems: compactResult.compactedWindow.length,
|
|
534
|
+
firstKeptEntryId: compaction.firstKeptEntryId,
|
|
535
|
+
},
|
|
536
|
+
config,
|
|
537
|
+
ctx,
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
if (!isCurrent() || refuseRetiredSession(ctx)) return { outcome: "aborted" };
|
|
541
|
+
return { outcome: "success", compaction };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function handleSessionBeforeCompact(
|
|
545
|
+
event: SessionBeforeCompactEvent,
|
|
546
|
+
ctx: ExtensionContext,
|
|
547
|
+
dependencies: CompactionDependencies,
|
|
548
|
+
) {
|
|
549
|
+
if (refuseRetiredSession(ctx)) return { cancel: true };
|
|
550
|
+
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
551
|
+
if (resolved.invalidFeatures.some((feature) => ["context", "compatibility", "diagnostics"].includes(feature))) return { cancel: true };
|
|
552
|
+
const config = resolved.config.compaction;
|
|
553
|
+
if (config.enabled) {
|
|
554
|
+
writeDebugArtifact(
|
|
555
|
+
"compaction-event",
|
|
556
|
+
{
|
|
557
|
+
event: "session_before_compact",
|
|
558
|
+
customInstructions: event.customInstructions,
|
|
559
|
+
preparation: {
|
|
560
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
561
|
+
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
562
|
+
previousSummaryPresent: Boolean(event.preparation.previousSummary),
|
|
563
|
+
messagesToSummarizeCount: event.preparation.messagesToSummarize.length,
|
|
564
|
+
turnPrefixMessagesCount: event.preparation.turnPrefixMessages.length,
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
config,
|
|
568
|
+
ctx,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (event.signal.aborted) {
|
|
573
|
+
return { cancel: true };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const isCurrent = dependencies.isCurrentCompaction ?? (() => true);
|
|
577
|
+
if (!isCurrent()) return { cancel: true };
|
|
578
|
+
if (!config.enabled || resolved.policy.context.mode !== "remote-compaction") return undefined;
|
|
579
|
+
|
|
580
|
+
// Resolve producer protocol policy from this operation's document, not a later disk read.
|
|
581
|
+
if (resolved.format === "v2" && config.remoteCompactModel) {
|
|
582
|
+
const producer = parseModelSpec(config.remoteCompactModel);
|
|
583
|
+
if (producer) {
|
|
584
|
+
const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: producer.provider, id: producer.modelId });
|
|
585
|
+
notifyConfigIssues(ctx, producerPolicy);
|
|
586
|
+
if (producerPolicy.invalidFeatures.includes("compatibility")) return { cancel: true };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
|
|
590
|
+
let remoteAttempted = false;
|
|
591
|
+
const resolution = await resolveRemoteCompactionExecution(
|
|
592
|
+
ctx,
|
|
593
|
+
{
|
|
594
|
+
enabled: config.enabled,
|
|
595
|
+
responsesApis: config.responsesApis,
|
|
596
|
+
codexGatewayModels: resolved.gatewayModelKeys,
|
|
597
|
+
},
|
|
598
|
+
config.remoteCompactModel,
|
|
599
|
+
);
|
|
600
|
+
if (!isCurrent() || event.signal.aborted || refuseRetiredSession(ctx)) return { cancel: true };
|
|
601
|
+
if (resolution.ok) {
|
|
602
|
+
remoteAttempted = true;
|
|
603
|
+
const responsesOutcome = await runResponsesNativeCompact(
|
|
604
|
+
event,
|
|
605
|
+
ctx,
|
|
606
|
+
config,
|
|
607
|
+
resolution.execution,
|
|
608
|
+
dependencies.remoteCompact,
|
|
609
|
+
dependencies.projectCompactionContext,
|
|
610
|
+
dependencies.standaloneCompact,
|
|
611
|
+
isCurrent,
|
|
612
|
+
);
|
|
613
|
+
if (!isCurrent() || refuseRetiredSession(ctx)) return { cancel: true };
|
|
614
|
+
if (responsesOutcome.outcome === "success") {
|
|
615
|
+
return { compaction: responsesOutcome.compaction };
|
|
616
|
+
}
|
|
617
|
+
if (responsesOutcome.outcome === "aborted") {
|
|
618
|
+
return { cancel: true };
|
|
619
|
+
}
|
|
620
|
+
if (responsesOutcome.outcome === "unprojected-input") {
|
|
621
|
+
const message = responsesOutcome.reason === COMPACTION_CHECKPOINT_CONTEXT_EDITED
|
|
622
|
+
? "A context edit changed history sealed in the opaque checkpoint. Start a new session or navigate before that checkpoint; its encrypted content cannot be selectively rewritten."
|
|
623
|
+
: responsesOutcome.reason === COMPACTION_PROJECTION_UNAVAILABLE
|
|
624
|
+
? "Pi's ordered context-hook projection is unavailable; Remote V2 was not sent raw session history."
|
|
625
|
+
: responsesOutcome.reason === COMPACTION_SESSION_CONTEXT_UNAVAILABLE
|
|
626
|
+
? "Pi did not provide the current session context required by its context-hook projection."
|
|
627
|
+
: responsesOutcome.reason === COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE
|
|
628
|
+
? "Pi's projected context did not include the current compaction summary anchor."
|
|
629
|
+
: "The latest opaque checkpoint has no marker for the configured Remote V2 context source, or was created in the other mode.";
|
|
630
|
+
writeDebugArtifact(
|
|
631
|
+
"compaction-event",
|
|
632
|
+
{
|
|
633
|
+
event: "session_before_compact.remote-v2-unprojected-input",
|
|
634
|
+
reason: responsesOutcome.reason,
|
|
635
|
+
contextSource: config.remoteV2ContextSource,
|
|
636
|
+
inputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
637
|
+
piContextHookPatch: dependencies.piContextHookPatch,
|
|
638
|
+
message,
|
|
639
|
+
},
|
|
640
|
+
config,
|
|
641
|
+
ctx,
|
|
642
|
+
);
|
|
643
|
+
notifyWarning(
|
|
644
|
+
ctx,
|
|
645
|
+
`Remote V2 compaction cancelled (${responsesOutcome.reason}); ${message} The existing checkpoint and session history were left unchanged.`,
|
|
646
|
+
);
|
|
647
|
+
return { cancel: true };
|
|
648
|
+
}
|
|
649
|
+
// Only an actual exhausted remote attempt warns, never an input/routing skip.
|
|
650
|
+
if (event.signal?.aborted) return { cancel: true };
|
|
651
|
+
if (responsesOutcome.remoteFailure) {
|
|
652
|
+
const failure = responsesOutcome.remoteFailure;
|
|
653
|
+
writeDebugArtifact("compaction-event", {
|
|
654
|
+
event: "session_before_compact.remote-to-native-summary",
|
|
655
|
+
protocol: responsesOutcome.protocol,
|
|
656
|
+
...failure,
|
|
657
|
+
nextStep: "native-summary",
|
|
658
|
+
}, config, ctx);
|
|
659
|
+
notifyWarning(ctx, `Remote compaction failed (${failure.reason}${failure.status ? `, HTTP ${failure.status}` : ""}${failure.errorCode ? `, ${failure.errorCode}` : ""}); continuing with a native text summary. No encrypted remote checkpoint was produced.${failure.errorMessage ? ` ${failure.errorMessage}` : ""}`);
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
writeDebugArtifact(
|
|
663
|
+
"compaction-event",
|
|
664
|
+
{
|
|
665
|
+
event: "session_before_compact.remote-v2-unavailable",
|
|
666
|
+
reason: resolution.reason,
|
|
667
|
+
provider: resolution.provider,
|
|
668
|
+
api: resolution.api,
|
|
669
|
+
model: resolution.model,
|
|
670
|
+
baseUrl: resolution.baseUrl,
|
|
671
|
+
modelSpec: resolution.modelSpec,
|
|
672
|
+
errorMessage: resolution.errorMessage,
|
|
673
|
+
},
|
|
674
|
+
config,
|
|
675
|
+
ctx,
|
|
676
|
+
);
|
|
677
|
+
const expectedConsumerCapabilityMiss =
|
|
678
|
+
resolution.reason === "unsupported-api" && resolution.modelSpec === undefined;
|
|
679
|
+
if (config.remoteCompactModel && !expectedConsumerCapabilityMiss) {
|
|
680
|
+
notifyWarning(
|
|
681
|
+
ctx,
|
|
682
|
+
`remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Branch 2: run pi's native compaction method. A failed remote request is compacted by the
|
|
688
|
+
// remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
|
|
689
|
+
const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
|
|
690
|
+
if (!isCurrent() || event.signal.aborted || refuseRetiredSession(ctx)) return { cancel: true };
|
|
691
|
+
const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec, isCurrent });
|
|
692
|
+
if (!isCurrent() || refuseRetiredSession(ctx)) return { cancel: true };
|
|
693
|
+
if (fallback.ok) {
|
|
694
|
+
if (ctx.hasUI) {
|
|
695
|
+
ctx.ui.notify(
|
|
696
|
+
`${COMPACTION_EXTENSION_ID}: compacted with ${fallback.model.provider}/${fallback.model.id} (native method)`,
|
|
697
|
+
"info",
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
writeDebugArtifact(
|
|
701
|
+
"compaction-event",
|
|
702
|
+
{
|
|
703
|
+
event: "session_before_compact.fallback-success",
|
|
704
|
+
model: fallback.model,
|
|
705
|
+
},
|
|
706
|
+
config,
|
|
707
|
+
ctx,
|
|
708
|
+
);
|
|
709
|
+
return { compaction: fallback.result };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (fallback.reason === "aborted") {
|
|
713
|
+
return { cancel: true };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
writeDebugArtifact(
|
|
717
|
+
"compaction-event",
|
|
718
|
+
{
|
|
719
|
+
event: "session_before_compact.fallback-skip",
|
|
720
|
+
reason: fallback.reason,
|
|
721
|
+
modelSpec: fallback.modelSpec,
|
|
722
|
+
errorMessage: fallback.errorMessage,
|
|
723
|
+
},
|
|
724
|
+
config,
|
|
725
|
+
ctx,
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
// Intentional pi-default paths: feature disabled, nothing configured, or it matches the current one.
|
|
729
|
+
const intentionalSkip =
|
|
730
|
+
fallback.reason === "disabled" ||
|
|
731
|
+
fallback.reason === "no-model-configured" ||
|
|
732
|
+
fallback.reason === "same-as-current-model";
|
|
733
|
+
if (!intentionalSkip) {
|
|
734
|
+
// An explicit summary model is authoritative: the active model may be
|
|
735
|
+
// unable to summarize at all. Preserve context on failure, never reroute.
|
|
736
|
+
notifyWarning(
|
|
737
|
+
ctx,
|
|
738
|
+
`compaction model "${fallback.modelSpec ?? fallbackModelSpec}" failed (${fallback.reason}${fallback.errorMessage ? `: ${fallback.errorMessage}` : ""}); compaction cancelled, context preserved; not switching to the current model`,
|
|
739
|
+
);
|
|
740
|
+
return { cancel: true };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function handleContextInternal(
|
|
747
|
+
event: ContextWithSystemEvent,
|
|
748
|
+
ctx: ExtensionContext,
|
|
749
|
+
loadConfig: typeof loadToolkitConfig,
|
|
750
|
+
) {
|
|
751
|
+
if (refuseRetiredSession(ctx, true)) return undefined;
|
|
752
|
+
const resolved = contextOperation(loadConfig, ctx);
|
|
753
|
+
requireContextPolicy(resolved, ctx);
|
|
754
|
+
const config = resolved.config.compaction;
|
|
755
|
+
if (!config.enabled) return undefined;
|
|
756
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
757
|
+
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
758
|
+
enabled: config.enabled,
|
|
759
|
+
responsesApis: config.responsesApis,
|
|
760
|
+
codexGatewayModels: resolved.gatewayModelKeys,
|
|
761
|
+
});
|
|
762
|
+
if (refuseRetiredSession(ctx, true)) return undefined;
|
|
763
|
+
if (!resolution.ok) return undefined;
|
|
764
|
+
const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
|
|
765
|
+
if (!latest.ok) return undefined;
|
|
766
|
+
const result = removeNativeCompactionRetainedMessages({
|
|
767
|
+
messages: event.messages,
|
|
768
|
+
branchEntries,
|
|
769
|
+
compactionEntry: latest.entry,
|
|
770
|
+
expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
771
|
+
});
|
|
772
|
+
if (!result.ok) {
|
|
773
|
+
writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
|
|
774
|
+
if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
|
|
775
|
+
reportPiContextHookFailure(ctx, `replay-failed:${result.reason}`);
|
|
776
|
+
ctx.abort();
|
|
777
|
+
return undefined;
|
|
778
|
+
}
|
|
779
|
+
return result.messages.length === event.messages.length && result.messages.every((message, index) => message === event.messages[index])
|
|
780
|
+
? undefined : { messages: result.messages };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Pi's emitContext() catches handler exceptions. Re-throw as usual for Pi, but
|
|
785
|
+
* also report the failure so the compaction projector cannot mistake the
|
|
786
|
+
* caught exception for a successful projection.
|
|
787
|
+
*/
|
|
788
|
+
async function handleContext(
|
|
789
|
+
event: ContextWithSystemEvent,
|
|
790
|
+
ctx: ExtensionContext,
|
|
791
|
+
loadConfig: typeof loadToolkitConfig,
|
|
792
|
+
) {
|
|
793
|
+
try {
|
|
794
|
+
return await handleContextInternal(event, ctx, loadConfig);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
|
|
797
|
+
throw error;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async function handleBeforeProviderRequest(
|
|
802
|
+
event: BeforeProviderRequestEvent,
|
|
803
|
+
ctx: ExtensionContext,
|
|
804
|
+
loadConfig: typeof loadToolkitConfig,
|
|
805
|
+
) {
|
|
806
|
+
if (refuseRetiredSession(ctx, true)) return undefined;
|
|
807
|
+
const resolved = contextOperation(loadConfig, ctx);
|
|
808
|
+
requireContextPolicy(resolved, ctx);
|
|
809
|
+
const toolkitConfig = resolved.config;
|
|
810
|
+
const config = toolkitConfig.compaction;
|
|
811
|
+
if (!config.enabled) return undefined;
|
|
812
|
+
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
813
|
+
enabled: config.enabled,
|
|
814
|
+
responsesApis: config.responsesApis,
|
|
815
|
+
codexGatewayModels: resolved.gatewayModelKeys,
|
|
816
|
+
}, event.payload);
|
|
817
|
+
if (refuseRetiredSession(ctx, true)) return undefined;
|
|
818
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
819
|
+
if (resolution.ok === false) {
|
|
820
|
+
writeDebugArtifact(
|
|
821
|
+
"provider-request",
|
|
822
|
+
{
|
|
823
|
+
event: "before_provider_request.skip",
|
|
824
|
+
reason: resolution.reason,
|
|
825
|
+
provider: resolution.provider,
|
|
826
|
+
api: resolution.api,
|
|
827
|
+
model: resolution.model,
|
|
828
|
+
baseUrl: resolution.baseUrl,
|
|
829
|
+
errorMessage: resolution.errorMessage,
|
|
830
|
+
currentModel: getCurrentModelDebugInfo(ctx),
|
|
831
|
+
payload: event.payload,
|
|
832
|
+
},
|
|
833
|
+
config,
|
|
834
|
+
ctx,
|
|
835
|
+
);
|
|
836
|
+
return undefined;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const runtime = resolution.runtime;
|
|
840
|
+
const payload = runtime.payload;
|
|
841
|
+
if (!payload) {
|
|
842
|
+
return undefined;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Capture compact-relevant request fields (tools, reasoning, ...) for the next
|
|
846
|
+
// synthetic compact request using the active consumer's effective runtime identity.
|
|
847
|
+
// This hook runs before the separate Web Search transform, so injected native search
|
|
848
|
+
// tools are not copied into remote_compaction_v2.
|
|
849
|
+
const webSearchRoute = resolveWebSearchRoute({ model: ctx.model, config: toolkitConfig.webSearch });
|
|
850
|
+
rememberRequestContext(
|
|
851
|
+
payload,
|
|
852
|
+
{
|
|
853
|
+
provider: runtime.provider,
|
|
854
|
+
api: runtime.api,
|
|
855
|
+
model: runtime.requestModel ?? runtime.model,
|
|
856
|
+
baseUrl: runtime.baseUrl,
|
|
857
|
+
sessionId: getSessionId(ctx),
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
excludeWebSearchTools: webSearchRoute.route === "standalone-alpha",
|
|
861
|
+
},
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
865
|
+
baseUrl: runtime.baseUrl,
|
|
866
|
+
});
|
|
867
|
+
if (!latestNativeCompaction.ok) {
|
|
868
|
+
writeDebugArtifact(
|
|
869
|
+
"provider-request",
|
|
870
|
+
{
|
|
871
|
+
event: "before_provider_request.no-native-compaction",
|
|
872
|
+
reason: latestNativeCompaction.reason,
|
|
873
|
+
provider: runtime.provider,
|
|
874
|
+
api: runtime.api,
|
|
875
|
+
model: runtime.model,
|
|
876
|
+
baseUrl: runtime.baseUrl,
|
|
877
|
+
branchEntries: branchEntries.length,
|
|
878
|
+
latestCompactionIndex: latestNativeCompaction.latestCompactionIndex,
|
|
879
|
+
latestCompactionIdentity: getCompactionIdentityDebugInfo(latestNativeCompaction.latestCompaction),
|
|
880
|
+
payload,
|
|
881
|
+
},
|
|
882
|
+
config,
|
|
883
|
+
ctx,
|
|
884
|
+
);
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const latestNativeCompactionEntry = latestNativeCompaction.entry;
|
|
889
|
+
const rewrite = rewriteResponsesPayloadWithNativeReplay({
|
|
890
|
+
model: runtime.currentModel,
|
|
891
|
+
payload,
|
|
892
|
+
branchEntries,
|
|
893
|
+
compactionEntry: latestNativeCompactionEntry,
|
|
894
|
+
expectedInputProvenance: getRemoteV2InputProvenance(config.remoteV2ContextSource),
|
|
895
|
+
});
|
|
896
|
+
if (!rewrite.ok) {
|
|
897
|
+
writeDebugArtifact(
|
|
898
|
+
"provider-request",
|
|
899
|
+
{
|
|
900
|
+
event: "before_provider_request.rewrite-failed",
|
|
901
|
+
reason: rewrite.reason,
|
|
902
|
+
provider: runtime.provider,
|
|
903
|
+
api: runtime.api,
|
|
904
|
+
model: runtime.model,
|
|
905
|
+
baseUrl: runtime.baseUrl,
|
|
906
|
+
compactionEntryId: latestNativeCompactionEntry.id,
|
|
907
|
+
parity: rewrite.parity,
|
|
908
|
+
payload,
|
|
909
|
+
},
|
|
910
|
+
config,
|
|
911
|
+
ctx,
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
// Fail loud instead of letting Pi send the sentinel-only payload: the
|
|
915
|
+
// compacted history would be silently lost while the request still succeeds.
|
|
916
|
+
// A forced redacted failure record is written even when logProviderPayloads
|
|
917
|
+
// is disabled so the incident is diagnosable without leaking content.
|
|
918
|
+
writeReplayFailureArtifact(
|
|
919
|
+
{
|
|
920
|
+
reason: rewrite.reason,
|
|
921
|
+
parity: rewrite.parity,
|
|
922
|
+
compactionEntryId: latestNativeCompactionEntry.id,
|
|
923
|
+
provider: runtime.provider,
|
|
924
|
+
api: runtime.api,
|
|
925
|
+
model: runtime.model,
|
|
926
|
+
},
|
|
927
|
+
config,
|
|
928
|
+
ctx,
|
|
929
|
+
);
|
|
930
|
+
if (ctx.hasUI) {
|
|
931
|
+
ctx.ui.notify(
|
|
932
|
+
`${COMPACTION_EXTENSION_ID}: native compaction replay failed (${rewrite.reason}); provider request aborted`,
|
|
933
|
+
"error",
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
ctx.abort();
|
|
937
|
+
return undefined;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
writeDebugArtifact(
|
|
941
|
+
"provider-request",
|
|
942
|
+
{
|
|
943
|
+
event: "before_provider_request.native-rewrite",
|
|
944
|
+
provider: runtime.provider,
|
|
945
|
+
api: runtime.api,
|
|
946
|
+
model: runtime.model,
|
|
947
|
+
baseUrl: runtime.baseUrl,
|
|
948
|
+
compactionEntryId: latestNativeCompactionEntry.id,
|
|
949
|
+
boundaryIndex: rewrite.segments.boundaryIndex,
|
|
950
|
+
firstKeptEntryIndex: rewrite.segments.firstKeptEntryIndex,
|
|
951
|
+
originalInputItems: payload.input.length,
|
|
952
|
+
rewrittenInputItems: rewrite.rewrittenPayload.input.length,
|
|
953
|
+
leadingItems: rewrite.segments.leading.length,
|
|
954
|
+
compactionSummaryItems: rewrite.segments.compactionSummary.length,
|
|
955
|
+
compactedItems: rewrite.segments.compactedWindow.length,
|
|
956
|
+
postItems: rewrite.segments.post.length,
|
|
957
|
+
payload: rewrite.rewrittenPayload,
|
|
958
|
+
originalPayload: payload,
|
|
959
|
+
},
|
|
960
|
+
config,
|
|
961
|
+
ctx,
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
return rewrite.rewrittenPayload;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export default function registerCompactionExtension(
|
|
968
|
+
pi: ExtensionAPI,
|
|
969
|
+
overrides: Partial<CompactionDependencies> = {},
|
|
970
|
+
) {
|
|
971
|
+
const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
|
|
972
|
+
registerToolkitConfigCommand(pi, loadConfig);
|
|
973
|
+
registerPhysicalModelObserver(pi);
|
|
974
|
+
const piContextHookPatch = installPiContextHookPatch();
|
|
975
|
+
let compactionGeneration = 0;
|
|
976
|
+
const invalidateCompaction = () => { compactionGeneration++; };
|
|
977
|
+
const dependencies: CompactionDependencies = {
|
|
978
|
+
remoteCompact: executeRemoteV2Compaction,
|
|
979
|
+
standaloneCompact: executeNativeCompaction,
|
|
980
|
+
nativeFallback: runNativeFallbackCompaction,
|
|
981
|
+
...overrides,
|
|
982
|
+
loadConfig,
|
|
983
|
+
piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
|
|
984
|
+
};
|
|
985
|
+
pi.on("session_start", (_event, ctx) => {
|
|
986
|
+
invalidateCompaction();
|
|
987
|
+
if (refuseRetiredSession(ctx)) return;
|
|
988
|
+
const resolved = contextOperation(loadConfig, ctx);
|
|
989
|
+
const config = resolved.config.compaction;
|
|
990
|
+
if (resolved.invalidFeatures.length || !config.enabled) return;
|
|
991
|
+
const artifactPath = writeDebugArtifact("lifecycle", {
|
|
992
|
+
event: "session_start", config, configSource: resolved.source,
|
|
993
|
+
warnings: resolved.issues.map((issue) => `${issue.path}: ${issue.code}`),
|
|
994
|
+
piContextHookPatch: dependencies.piContextHookPatch,
|
|
995
|
+
}, config, ctx);
|
|
996
|
+
if (ctx.hasUI && ["info", "debug"].includes(resolved.policy.diagnostics.level) && (config.notifyOnLoad || config.debug)) {
|
|
997
|
+
ctx.ui.notify(artifactPath ? `${COMPACTION_EXTENSION_ID} loaded • debug artifacts → ${artifactPath}` : `${COMPACTION_EXTENSION_ID} loaded`, "info");
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
1001
|
+
invalidateCompaction();
|
|
1002
|
+
refuseRetiredSession(ctx);
|
|
1003
|
+
});
|
|
1004
|
+
pi.on("context_with_system", (event, ctx) => handleContext(event, ctx, loadConfig));
|
|
1005
|
+
pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, loadConfig));
|
|
1006
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
1007
|
+
invalidateCompaction();
|
|
1008
|
+
if (refuseRetiredSession(ctx)) return { cancel: true };
|
|
1009
|
+
try {
|
|
1010
|
+
const generation = compactionGeneration;
|
|
1011
|
+
const branch = structuredClone(ctx.sessionManager.getBranch());
|
|
1012
|
+
const sessionId = getSessionId(ctx);
|
|
1013
|
+
const model = ctx.model ? { ...ctx.model } : undefined;
|
|
1014
|
+
const thinking = ctx.thinkingLevel;
|
|
1015
|
+
// Clients classify aborts. This fence handles stale ownership: a
|
|
1016
|
+
// validated V2 terminal still wins over cleanup-triggered cancellation.
|
|
1017
|
+
const isCurrent = () => {
|
|
1018
|
+
try {
|
|
1019
|
+
return generation === compactionGeneration
|
|
1020
|
+
&& getSessionId(ctx) === sessionId && isDeepStrictEqual(branch, ctx.sessionManager.getBranch())
|
|
1021
|
+
&& isDeepStrictEqual(model, ctx.model) && thinking === ctx.thinkingLevel
|
|
1022
|
+
&& !retirementReason(ctx)
|
|
1023
|
+
&& !(event.reason === "manual" && typeof ctx.hasPendingMessages === "function" && ctx.hasPendingMessages());
|
|
1024
|
+
} catch { return false; }
|
|
1025
|
+
};
|
|
1026
|
+
const result = await handleSessionBeforeCompact(event, ctx, { ...dependencies, isCurrentCompaction: isCurrent });
|
|
1027
|
+
if (!isCurrent()) return { cancel: true };
|
|
1028
|
+
if (result && "compaction" in result && result.compaction) {
|
|
1029
|
+
// Pi consumes results after later awaited observers. Never persist a
|
|
1030
|
+
// stale candidate or drain/consume the host's queues.
|
|
1031
|
+
return { compaction: result.compaction, get cancel() { return !isCurrent(); } };
|
|
1032
|
+
}
|
|
1033
|
+
if (result === undefined) {
|
|
1034
|
+
// Default summarization starts only after later observers finish too.
|
|
1035
|
+
// Keep the host handoff revocable without claiming its compaction.
|
|
1036
|
+
return { get cancel() { return !isCurrent(); } };
|
|
1037
|
+
}
|
|
1038
|
+
return result;
|
|
1039
|
+
} catch {
|
|
1040
|
+
// Pi can treat a rejected hook as absent and run its implicit summary.
|
|
1041
|
+
try { notifyWarning(ctx, "Compaction stopped; existing context preserved. Resume manually."); } catch { /* stale UI */ }
|
|
1042
|
+
return { cancel: true };
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
pi.on("session_compact", invalidateCompaction);
|
|
1046
|
+
pi.on("session_compact_failed", invalidateCompaction);
|
|
1047
|
+
pi.on("session_shutdown", invalidateCompaction);
|
|
1048
|
+
pi.on("model_select", invalidateCompaction);
|
|
1049
|
+
pi.on("before_agent_start", invalidateCompaction);
|
|
1050
|
+
pi.on("agent_start", invalidateCompaction);
|
|
1051
|
+
pi.on("input", invalidateCompaction);
|
|
1052
|
+
pi.on("thinking_level_select", invalidateCompaction);
|
|
1053
|
+
pi.on("message_start", invalidateCompaction);
|
|
1054
|
+
pi.on("tool_call", invalidateCompaction);
|
|
1055
|
+
pi.on("session_before_switch", invalidateCompaction);
|
|
1056
|
+
pi.on("session_before_fork", invalidateCompaction);
|
|
1057
|
+
pi.on("session_before_tree", invalidateCompaction);
|
|
1058
|
+
|
|
1059
|
+
// Exact gateway compatibility is shared transport policy, independent of
|
|
1060
|
+
// compaction enablement. No window identities or ingest headers are generated.
|
|
1061
|
+
pi.on("before_provider_headers", async (event, ctx) => {
|
|
1062
|
+
if (refuseRetiredSession(ctx, true)) return;
|
|
1063
|
+
const resolved = contextOperation(loadConfig, ctx);
|
|
1064
|
+
requireContextPolicy(resolved, ctx);
|
|
1065
|
+
if (!resolved.gatewayModelKeys.length) return;
|
|
1066
|
+
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
1067
|
+
responsesApis: ["openai-responses"], codexGatewayModels: resolved.gatewayModelKeys,
|
|
1068
|
+
});
|
|
1069
|
+
if (refuseRetiredSession(ctx, true)) return;
|
|
1070
|
+
if (!resolution.ok) {
|
|
1071
|
+
const selected = isExactModelAllowed(ctx.model, resolved.gatewayModelKeys);
|
|
1072
|
+
const physical = resolution.provider && resolution.model
|
|
1073
|
+
? isExactModelAllowed({ provider: resolution.provider, id: resolution.model }, resolved.gatewayModelKeys) : false;
|
|
1074
|
+
if (selected || physical) {
|
|
1075
|
+
notifyWarning(ctx, "Configured gateway transport is unavailable; provider request aborted.");
|
|
1076
|
+
ctx.abort();
|
|
1077
|
+
}
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const runtime = resolution.runtime;
|
|
1081
|
+
if (!runtime.codexAffinity) return;
|
|
1082
|
+
const gatewayHeaders = buildResponsesRequestHeaders(runtime, { accept: "text/event-stream" });
|
|
1083
|
+
const allowed = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
|
|
1084
|
+
for (const existing of Object.keys(event.headers)) {
|
|
1085
|
+
if (!allowed.has(existing.toLowerCase())) delete event.headers[existing];
|
|
1086
|
+
}
|
|
1087
|
+
for (const name of ["authorization", "originator", "user-agent", "session-id", "x-client-request-id", "x-codex-affinity-scope", "x-codex-model"]) {
|
|
1088
|
+
for (const existing of Object.keys(event.headers)) {
|
|
1089
|
+
if (existing.toLowerCase() === name) delete event.headers[existing];
|
|
1090
|
+
}
|
|
1091
|
+
const value = gatewayHeaders.get(name);
|
|
1092
|
+
if (value) event.headers[name] = value;
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
}
|