pi-openai-toolkit 0.14.4 → 0.14.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -147
- package/README.zh.md +119 -151
- package/docs/internals.md +42 -21
- package/package.json +127 -127
- package/src/auto-mode/classifier.ts +165 -165
- package/src/auto-mode/policy.ts +146 -146
- package/src/auto-mode/prompt.ts +279 -279
- package/src/auto-mode/reviewer.ts +325 -325
- package/src/auto-mode/transcript.ts +133 -133
- package/src/auto-mode/types.ts +137 -137
- package/src/codex-astra/effort-planner.ts +175 -175
- package/src/codex-astra/extension.ts +122 -122
- package/src/config.ts +621 -592
- package/src/context-management/codex-provider.ts +260 -260
- package/src/context-management/history-notes.ts +357 -325
- package/src/context-management/messages.ts +112 -106
- package/src/context-management/namespace-tools.ts +426 -426
- package/src/context-management/tool-contract.ts +23 -23
- package/src/context-management/tools.ts +306 -311
- package/src/context-management/types.ts +168 -161
- package/src/context-management/window-budget.ts +83 -83
- package/src/context-management/window-manager.ts +60 -51
- package/src/context-management/window-request.ts +125 -125
- package/src/debug.ts +1 -1
- package/src/extension-runtime.ts +34 -9
- package/src/image-generation/artifacts.ts +237 -237
- package/src/image-generation/client.ts +248 -248
- package/src/image-generation/eligibility.ts +18 -18
- package/src/image-generation/extension.ts +19 -2
- package/src/image-generation/protocol.ts +473 -405
- package/src/image-generation/references.ts +194 -194
- package/src/image-generation/render.ts +252 -252
- package/src/image-generation/service.ts +249 -243
- package/src/image-generation/types.ts +189 -184
- package/src/model-scope.ts +17 -17
- package/src/provider-headers.ts +23 -23
- package/src/responses-headers.ts +173 -108
- package/src/runtime.ts +513 -477
- package/src/types.ts +11 -0
- package/src/web-search/extension.ts +80 -80
- package/src/web-search/payload.ts +96 -96
- package/src/web-search/prompt.ts +20 -20
- package/src/web-search/types.ts +45 -45
|
@@ -31,8 +31,13 @@ import {
|
|
|
31
31
|
isNonEmptyString,
|
|
32
32
|
isRecord,
|
|
33
33
|
isContextWindowCompactionDetails,
|
|
34
|
+
type NotesCheckpointReceipt,
|
|
34
35
|
} from "./types";
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
encodeEncryptedOutputForContext,
|
|
38
|
+
isSuccessfulHistoryNotesToolResult,
|
|
39
|
+
loadHistoryNotesThreadHint,
|
|
40
|
+
} from "./history-notes";
|
|
36
41
|
import { rewriteContextNamespaceTools } from "./namespace-tools";
|
|
37
42
|
|
|
38
43
|
interface StartContextWindowOptions {
|
|
@@ -171,6 +176,9 @@ export class CodexContextWindowManager {
|
|
|
171
176
|
this.rolloverPending = true;
|
|
172
177
|
try {
|
|
173
178
|
const current = this.identity;
|
|
179
|
+
const checkpoint = current
|
|
180
|
+
? findLatestNotesCheckpointSinceBoundary(ctx.sessionManager.getBranch(), this.sessionId)
|
|
181
|
+
: undefined;
|
|
174
182
|
let threadHint: string | undefined;
|
|
175
183
|
if (current) {
|
|
176
184
|
try {
|
|
@@ -189,7 +197,7 @@ export class CodexContextWindowManager {
|
|
|
189
197
|
windowNumber: current.windowNumber + 1,
|
|
190
198
|
}
|
|
191
199
|
: { firstWindowId: currentWindowId, currentWindowId, windowNumber: 0 };
|
|
192
|
-
this.sendWindowMessage(pi, ctx, next, options, threadHint);
|
|
200
|
+
this.sendWindowMessage(pi, ctx, next, options, threadHint, checkpoint);
|
|
193
201
|
return true;
|
|
194
202
|
} catch (error) {
|
|
195
203
|
this.rolloverPending = false;
|
|
@@ -289,10 +297,11 @@ export class CodexContextWindowManager {
|
|
|
289
297
|
identity: ContextWindowIdentity,
|
|
290
298
|
options: StartContextWindowOptions,
|
|
291
299
|
threadHint?: string,
|
|
300
|
+
checkpoint?: NotesCheckpointReceipt,
|
|
292
301
|
): void {
|
|
293
302
|
sendContextWindowMessage(
|
|
294
303
|
pi,
|
|
295
|
-
renderContextWindowMessage(identity, threadHint),
|
|
304
|
+
renderContextWindowMessage(identity, threadHint, checkpoint),
|
|
296
305
|
"window",
|
|
297
306
|
identity,
|
|
298
307
|
{ triggerTurn: options.triggerTurn, sessionId: ctx.sessionManager.getSessionId() },
|
|
@@ -329,20 +338,7 @@ export function hasAssistantUsageSinceWindowBoundary(
|
|
|
329
338
|
entries: readonly SessionEntry[],
|
|
330
339
|
sessionId?: string,
|
|
331
340
|
): boolean {
|
|
332
|
-
|
|
333
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
334
|
-
const entry = entries[index]!;
|
|
335
|
-
if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
|
|
336
|
-
if (!couldBelongToSession(entry.details, sessionId)) continue;
|
|
337
|
-
if (!isCodexContextManagementMessageDetails(entry.details)) {
|
|
338
|
-
throw new Error("Malformed persisted Codex context-window message");
|
|
339
|
-
}
|
|
340
|
-
if (!matchesSession(entry.details.sessionId, sessionId)) continue;
|
|
341
|
-
if (entry.details.contextManagement.kind === "window") {
|
|
342
|
-
boundaryIndex = index;
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
341
|
+
const boundaryIndex = findLatestWindowBoundaryIndex(entries, sessionId);
|
|
346
342
|
if (boundaryIndex < 0) return false;
|
|
347
343
|
for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
|
|
348
344
|
const entry = entries[index]!;
|
|
@@ -363,29 +359,18 @@ export function hasAssistantUsageSinceWindowBoundary(
|
|
|
363
359
|
}
|
|
364
360
|
|
|
365
361
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
362
|
+
* Return the most recent successful notes write after the latest window
|
|
363
|
+
* boundary. The receipt is derived from persisted session entries so it
|
|
364
|
+
* survives restarts and can be used for an explicit rollover handoff.
|
|
368
365
|
*/
|
|
369
|
-
export function
|
|
366
|
+
export function findLatestNotesCheckpointSinceBoundary(
|
|
370
367
|
entries: readonly SessionEntry[],
|
|
371
368
|
sessionId?: string,
|
|
372
|
-
):
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (!couldBelongToSession(entry.details, sessionId)) continue;
|
|
378
|
-
if (!isCodexContextManagementMessageDetails(entry.details)) {
|
|
379
|
-
throw new Error("Malformed persisted Codex context-window message");
|
|
380
|
-
}
|
|
381
|
-
if (!matchesSession(entry.details.sessionId, sessionId)) continue;
|
|
382
|
-
if (entry.details.contextManagement.kind === "window") {
|
|
383
|
-
boundaryIndex = index;
|
|
384
|
-
break;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (boundaryIndex < 0) return false;
|
|
388
|
-
const checkpointCalls = new Set<string>();
|
|
369
|
+
): NotesCheckpointReceipt | undefined {
|
|
370
|
+
const boundaryIndex = findLatestWindowBoundaryIndex(entries, sessionId);
|
|
371
|
+
if (boundaryIndex < 0) return undefined;
|
|
372
|
+
const checkpointCalls = new Map<string, NotesCheckpointReceipt>();
|
|
373
|
+
let latest: NotesCheckpointReceipt | undefined;
|
|
389
374
|
for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
|
|
390
375
|
const entry = entries[index]!;
|
|
391
376
|
if (entry.type !== "message") continue;
|
|
@@ -393,28 +378,52 @@ export function findNotesCheckpointSinceBoundary(
|
|
|
393
378
|
if (message.role === "assistant") {
|
|
394
379
|
const parts = Array.isArray(message.content) ? message.content : [];
|
|
395
380
|
for (const part of parts) {
|
|
396
|
-
if (!isRecord(part) || part.type !== "toolCall") continue;
|
|
397
|
-
// Match by call id only; the provider-side namespace rewrite never
|
|
398
|
-
// affects the names persisted in the Pi session branch.
|
|
399
|
-
if (part.name !== "notes") continue;
|
|
381
|
+
if (!isRecord(part) || part.type !== "toolCall" || part.name !== "notes") continue;
|
|
400
382
|
const args = isRecord(part.arguments) ? part.arguments : undefined;
|
|
401
383
|
const action = typeof args?.action === "string" ? args.action : "";
|
|
402
|
-
|
|
403
|
-
|
|
384
|
+
const path = isNonEmptyString(args?.path) ? args.path : undefined;
|
|
385
|
+
if (isNonEmptyString(part.id) && path !== undefined && NOTES_CHECKPOINT_ACTIONS.has(action)) {
|
|
386
|
+
checkpointCalls.set(part.id, { path, toolCallId: part.id });
|
|
404
387
|
}
|
|
405
388
|
}
|
|
406
389
|
continue;
|
|
407
390
|
}
|
|
408
|
-
if (message.role
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
391
|
+
if (message.role !== "toolResult" || message.toolName !== "notes") continue;
|
|
392
|
+
const receipt = checkpointCalls.get(message.toolCallId);
|
|
393
|
+
if (!receipt) continue;
|
|
394
|
+
checkpointCalls.delete(message.toolCallId);
|
|
395
|
+
if (message.isError !== false || !isSuccessfulHistoryNotesToolResult(message.details)) continue;
|
|
396
|
+
latest = receipt;
|
|
397
|
+
}
|
|
398
|
+
return latest;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Whether the branch contains a successful notes append/write result after the
|
|
403
|
+
* latest window boundary. Reads and failed writes never count as checkpoints.
|
|
404
|
+
*/
|
|
405
|
+
export function findNotesCheckpointSinceBoundary(
|
|
406
|
+
entries: readonly SessionEntry[],
|
|
407
|
+
sessionId?: string,
|
|
408
|
+
): boolean {
|
|
409
|
+
return findLatestNotesCheckpointSinceBoundary(entries, sessionId) !== undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function findLatestWindowBoundaryIndex(
|
|
413
|
+
entries: readonly SessionEntry[],
|
|
414
|
+
sessionId?: string,
|
|
415
|
+
): number {
|
|
416
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
417
|
+
const entry = entries[index]!;
|
|
418
|
+
if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
|
|
419
|
+
if (!couldBelongToSession(entry.details, sessionId)) continue;
|
|
420
|
+
if (!isCodexContextManagementMessageDetails(entry.details)) {
|
|
421
|
+
throw new Error("Malformed persisted Codex context-window message");
|
|
415
422
|
}
|
|
423
|
+
if (!matchesSession(entry.details.sessionId, sessionId)) continue;
|
|
424
|
+
if (entry.details.contextManagement.kind === "window") return index;
|
|
416
425
|
}
|
|
417
|
-
return
|
|
426
|
+
return -1;
|
|
418
427
|
}
|
|
419
428
|
|
|
420
429
|
export function findLatestWindowBoundaryEntry(
|
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import type { ContextWindowIdentity } from "./types";
|
|
4
|
-
import { CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER } from "./history-notes";
|
|
5
|
-
import { rewriteContextNamespaceCalls } from "./namespace-tools";
|
|
6
|
-
|
|
7
|
-
export type ContextWindowRequestMetadata = {
|
|
8
|
-
session_id: string;
|
|
9
|
-
thread_id: string;
|
|
10
|
-
agent_name: "/root";
|
|
11
|
-
window_id: string;
|
|
12
|
-
window_number: number;
|
|
13
|
-
context_window_id: string;
|
|
14
|
-
request_kind: "turn";
|
|
15
|
-
history_ingest_requested: true;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export function buildContextWindowRequestMetadata(
|
|
19
|
-
ctx: Pick<ExtensionContext, "sessionManager">,
|
|
20
|
-
identity: ContextWindowIdentity,
|
|
21
|
-
): ContextWindowRequestMetadata {
|
|
22
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
23
|
-
return {
|
|
24
|
-
session_id: sessionId,
|
|
25
|
-
thread_id: sessionId,
|
|
26
|
-
agent_name: "/root",
|
|
27
|
-
window_id: `${sessionId}:${identity.windowNumber}`,
|
|
28
|
-
window_number: identity.windowNumber,
|
|
29
|
-
context_window_id: identity.currentWindowId,
|
|
30
|
-
request_kind: "turn",
|
|
31
|
-
history_ingest_requested: true,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function rewriteWindowPayload(
|
|
36
|
-
payload: unknown,
|
|
37
|
-
ctx: Pick<ExtensionContext, "sessionManager">,
|
|
38
|
-
identity: ContextWindowIdentity | undefined,
|
|
39
|
-
): unknown {
|
|
40
|
-
if (!identity || !isResponsesPayload(payload)) return payload;
|
|
41
|
-
const clientMetadata = payload.client_metadata;
|
|
42
|
-
if (clientMetadata !== undefined && !isRecord(clientMetadata)) return payload;
|
|
43
|
-
const existingTurnMetadata = parseTurnMetadata(clientMetadata?.["x-codex-turn-metadata"]);
|
|
44
|
-
if (clientMetadata?.["x-codex-turn-metadata"] !== undefined && !existingTurnMetadata) return payload;
|
|
45
|
-
const metadata = buildContextWindowRequestMetadata(ctx, identity);
|
|
46
|
-
const turnMetadata = { ...existingTurnMetadata, ...metadata };
|
|
47
|
-
const rewritten: Record<string, unknown> = {
|
|
48
|
-
...payload,
|
|
49
|
-
client_metadata: {
|
|
50
|
-
...(clientMetadata ?? {}),
|
|
51
|
-
"x-codex-window-id": metadata.window_id,
|
|
52
|
-
"x-codex-turn-metadata": JSON.stringify(turnMetadata),
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
return rewriteEncryptedToolOutputs(rewriteContextNamespaceCalls(rewritten));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function rewriteEncryptedToolOutputs(payload: unknown): unknown {
|
|
59
|
-
if (!isRecord(payload) || !Array.isArray(payload.input)) return payload;
|
|
60
|
-
let changed = false;
|
|
61
|
-
const input = payload.input.map((item) => {
|
|
62
|
-
if (!isRecord(item) || item.type !== "function_call_output") return item;
|
|
63
|
-
const output = item.output;
|
|
64
|
-
if (typeof output === "string") {
|
|
65
|
-
const encrypted = decodeEncryptedOutputContext(output);
|
|
66
|
-
if (encrypted === undefined) return item;
|
|
67
|
-
changed = true;
|
|
68
|
-
return { ...item, output: [{ type: "encrypted_content", encrypted_content: encrypted }] };
|
|
69
|
-
}
|
|
70
|
-
if (!Array.isArray(output)) return item;
|
|
71
|
-
let outputChanged = false;
|
|
72
|
-
const rewrittenOutput = output.map((part) => {
|
|
73
|
-
if (!isRecord(part) || part.type !== "input_text" || typeof part.text !== "string") return part;
|
|
74
|
-
const encrypted = decodeEncryptedOutputContext(part.text);
|
|
75
|
-
if (encrypted === undefined) return part;
|
|
76
|
-
outputChanged = true;
|
|
77
|
-
return { type: "encrypted_content", encrypted_content: encrypted };
|
|
78
|
-
});
|
|
79
|
-
if (!outputChanged) return item;
|
|
80
|
-
changed = true;
|
|
81
|
-
return { ...item, output: rewrittenOutput };
|
|
82
|
-
});
|
|
83
|
-
return changed ? { ...payload, input } : payload;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function decodeEncryptedOutputContext(value: string): string | undefined {
|
|
87
|
-
if (!value.startsWith(CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER)) return undefined;
|
|
88
|
-
try {
|
|
89
|
-
const decoded: unknown = JSON.parse(value.slice(CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER.length));
|
|
90
|
-
if (typeof decoded !== "string" || decoded.length === 0) throw new Error("invalid encrypted output marker");
|
|
91
|
-
return decoded;
|
|
92
|
-
} catch {
|
|
93
|
-
throw new Error("Malformed encrypted history/notes output marker");
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function parseTurnMetadata(value: unknown): Record<string, unknown> | undefined {
|
|
98
|
-
if (value === undefined) return {};
|
|
99
|
-
if (typeof value !== "string") return undefined;
|
|
100
|
-
try {
|
|
101
|
-
const parsed: unknown = JSON.parse(value);
|
|
102
|
-
return isRecord(parsed) ? parsed : undefined;
|
|
103
|
-
} catch {
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isResponsesPayload(value: unknown): value is Record<string, unknown> & { model: string; input: unknown[] } {
|
|
109
|
-
return isRecord(value) && typeof value.model === "string" && Array.isArray(value.input);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function rewriteWindowHeaders(
|
|
113
|
-
headers: ProviderHeaders,
|
|
114
|
-
ctx: Pick<ExtensionContext, "sessionManager">,
|
|
115
|
-
identity: ContextWindowIdentity | undefined,
|
|
116
|
-
): void {
|
|
117
|
-
if (!identity) return;
|
|
118
|
-
const metadata = buildContextWindowRequestMetadata(ctx, identity);
|
|
119
|
-
headers["x-codex-window-id"] = metadata.window_id;
|
|
120
|
-
headers["x-codex-turn-metadata"] = JSON.stringify(metadata);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
124
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
125
|
-
}
|
|
1
|
+
import type { ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { ContextWindowIdentity } from "./types";
|
|
4
|
+
import { CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER } from "./history-notes";
|
|
5
|
+
import { rewriteContextNamespaceCalls } from "./namespace-tools";
|
|
6
|
+
|
|
7
|
+
export type ContextWindowRequestMetadata = {
|
|
8
|
+
session_id: string;
|
|
9
|
+
thread_id: string;
|
|
10
|
+
agent_name: "/root";
|
|
11
|
+
window_id: string;
|
|
12
|
+
window_number: number;
|
|
13
|
+
context_window_id: string;
|
|
14
|
+
request_kind: "turn";
|
|
15
|
+
history_ingest_requested: true;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function buildContextWindowRequestMetadata(
|
|
19
|
+
ctx: Pick<ExtensionContext, "sessionManager">,
|
|
20
|
+
identity: ContextWindowIdentity,
|
|
21
|
+
): ContextWindowRequestMetadata {
|
|
22
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
23
|
+
return {
|
|
24
|
+
session_id: sessionId,
|
|
25
|
+
thread_id: sessionId,
|
|
26
|
+
agent_name: "/root",
|
|
27
|
+
window_id: `${sessionId}:${identity.windowNumber}`,
|
|
28
|
+
window_number: identity.windowNumber,
|
|
29
|
+
context_window_id: identity.currentWindowId,
|
|
30
|
+
request_kind: "turn",
|
|
31
|
+
history_ingest_requested: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function rewriteWindowPayload(
|
|
36
|
+
payload: unknown,
|
|
37
|
+
ctx: Pick<ExtensionContext, "sessionManager">,
|
|
38
|
+
identity: ContextWindowIdentity | undefined,
|
|
39
|
+
): unknown {
|
|
40
|
+
if (!identity || !isResponsesPayload(payload)) return payload;
|
|
41
|
+
const clientMetadata = payload.client_metadata;
|
|
42
|
+
if (clientMetadata !== undefined && !isRecord(clientMetadata)) return payload;
|
|
43
|
+
const existingTurnMetadata = parseTurnMetadata(clientMetadata?.["x-codex-turn-metadata"]);
|
|
44
|
+
if (clientMetadata?.["x-codex-turn-metadata"] !== undefined && !existingTurnMetadata) return payload;
|
|
45
|
+
const metadata = buildContextWindowRequestMetadata(ctx, identity);
|
|
46
|
+
const turnMetadata = { ...existingTurnMetadata, ...metadata };
|
|
47
|
+
const rewritten: Record<string, unknown> = {
|
|
48
|
+
...payload,
|
|
49
|
+
client_metadata: {
|
|
50
|
+
...(clientMetadata ?? {}),
|
|
51
|
+
"x-codex-window-id": metadata.window_id,
|
|
52
|
+
"x-codex-turn-metadata": JSON.stringify(turnMetadata),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
return rewriteEncryptedToolOutputs(rewriteContextNamespaceCalls(rewritten));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function rewriteEncryptedToolOutputs(payload: unknown): unknown {
|
|
59
|
+
if (!isRecord(payload) || !Array.isArray(payload.input)) return payload;
|
|
60
|
+
let changed = false;
|
|
61
|
+
const input = payload.input.map((item) => {
|
|
62
|
+
if (!isRecord(item) || item.type !== "function_call_output") return item;
|
|
63
|
+
const output = item.output;
|
|
64
|
+
if (typeof output === "string") {
|
|
65
|
+
const encrypted = decodeEncryptedOutputContext(output);
|
|
66
|
+
if (encrypted === undefined) return item;
|
|
67
|
+
changed = true;
|
|
68
|
+
return { ...item, output: [{ type: "encrypted_content", encrypted_content: encrypted }] };
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(output)) return item;
|
|
71
|
+
let outputChanged = false;
|
|
72
|
+
const rewrittenOutput = output.map((part) => {
|
|
73
|
+
if (!isRecord(part) || part.type !== "input_text" || typeof part.text !== "string") return part;
|
|
74
|
+
const encrypted = decodeEncryptedOutputContext(part.text);
|
|
75
|
+
if (encrypted === undefined) return part;
|
|
76
|
+
outputChanged = true;
|
|
77
|
+
return { type: "encrypted_content", encrypted_content: encrypted };
|
|
78
|
+
});
|
|
79
|
+
if (!outputChanged) return item;
|
|
80
|
+
changed = true;
|
|
81
|
+
return { ...item, output: rewrittenOutput };
|
|
82
|
+
});
|
|
83
|
+
return changed ? { ...payload, input } : payload;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function decodeEncryptedOutputContext(value: string): string | undefined {
|
|
87
|
+
if (!value.startsWith(CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER)) return undefined;
|
|
88
|
+
try {
|
|
89
|
+
const decoded: unknown = JSON.parse(value.slice(CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER.length));
|
|
90
|
+
if (typeof decoded !== "string" || decoded.length === 0) throw new Error("invalid encrypted output marker");
|
|
91
|
+
return decoded;
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error("Malformed encrypted history/notes output marker");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseTurnMetadata(value: unknown): Record<string, unknown> | undefined {
|
|
98
|
+
if (value === undefined) return {};
|
|
99
|
+
if (typeof value !== "string") return undefined;
|
|
100
|
+
try {
|
|
101
|
+
const parsed: unknown = JSON.parse(value);
|
|
102
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isResponsesPayload(value: unknown): value is Record<string, unknown> & { model: string; input: unknown[] } {
|
|
109
|
+
return isRecord(value) && typeof value.model === "string" && Array.isArray(value.input);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function rewriteWindowHeaders(
|
|
113
|
+
headers: ProviderHeaders,
|
|
114
|
+
ctx: Pick<ExtensionContext, "sessionManager">,
|
|
115
|
+
identity: ContextWindowIdentity | undefined,
|
|
116
|
+
): void {
|
|
117
|
+
if (!identity) return;
|
|
118
|
+
const metadata = buildContextWindowRequestMetadata(ctx, identity);
|
|
119
|
+
headers["x-codex-window-id"] = metadata.window_id;
|
|
120
|
+
headers["x-codex-turn-metadata"] = JSON.stringify(metadata);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
124
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
125
|
+
}
|
package/src/debug.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type RedactOptions,
|
|
15
15
|
} from "./types";
|
|
16
16
|
|
|
17
|
-
const CRITICAL_KEY_RE = /(authorization|api[-_]?key|token|credential|oauth|auth|account[-_]?id|encrypted[_-]?(content|output))/i;
|
|
17
|
+
const CRITICAL_KEY_RE = /(authorization|api[-_]?key|token|credential|oauth|auth|account[-_]?id|cookie|set[-_]?cookie|response(?:text)?|completed|encrypted[_-]?(content|output))/i;
|
|
18
18
|
const SENSITIVE_KEY_RE = /(authorization|api[-_]?key|token|secret|password|cookie|set-cookie|signature|credential|oauth|auth|account[-_]?id|encrypted[_-]?(content|output))/i;
|
|
19
19
|
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi;
|
|
20
20
|
const OPENAI_KEY_RE = /\bsk-[A-Za-z0-9\-_]+\b/g;
|
package/src/extension-runtime.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
|
|
|
18
18
|
import { CodexContextWindowManager } from "./context-management/window-manager";
|
|
19
19
|
import { registerContextManagementTools } from "./context-management/tools";
|
|
20
20
|
import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
|
|
21
|
+
import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
|
|
21
22
|
import { resolveLatestNativeCompactionEntry } from "./details-store";
|
|
22
23
|
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
23
24
|
import {
|
|
@@ -398,6 +399,7 @@ async function handleSessionBeforeCompact(
|
|
|
398
399
|
{
|
|
399
400
|
enabled: config.enabled,
|
|
400
401
|
responsesApis: config.responsesApis,
|
|
402
|
+
codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
|
|
401
403
|
},
|
|
402
404
|
config.remoteCompactModel,
|
|
403
405
|
);
|
|
@@ -536,9 +538,15 @@ async function handleContext(
|
|
|
536
538
|
}
|
|
537
539
|
|
|
538
540
|
// An eligible Codex Remote session is never allowed to fall through to the
|
|
539
|
-
// older compaction-replay path.
|
|
540
|
-
//
|
|
541
|
+
// older compaction-replay path. Gateway traffic must also fail closed when
|
|
542
|
+
// its transport capability is unavailable; otherwise Pi could send an
|
|
543
|
+
// ordinary unscoped request and lose CPA OAuth/session affinity.
|
|
541
544
|
if (isCodexContextModel(ctx.model, config)) {
|
|
545
|
+
if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
|
|
546
|
+
notifyRemoteContextFailure(ctx, "codex-context-unavailable");
|
|
547
|
+
ctx.abort();
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
542
550
|
const visibleMessages = contextWindows.project(event.messages, "off");
|
|
543
551
|
return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
|
|
544
552
|
? undefined
|
|
@@ -554,6 +562,7 @@ async function handleContext(
|
|
|
554
562
|
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
555
563
|
enabled: config.enabled,
|
|
556
564
|
responsesApis: config.responsesApis,
|
|
565
|
+
codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
|
|
557
566
|
});
|
|
558
567
|
if (!resolution.ok) return undefined;
|
|
559
568
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
@@ -601,8 +610,14 @@ async function handleBeforeProviderRequest(
|
|
|
601
610
|
}
|
|
602
611
|
}
|
|
603
612
|
if (isCodexContextModel(ctx.model, config)) {
|
|
604
|
-
// Keep Codex Remote mutually exclusive with the legacy replay
|
|
605
|
-
//
|
|
613
|
+
// Keep Codex Remote mutually exclusive with the legacy replay pipeline
|
|
614
|
+
// when authentication or tool ownership is unavailable. Gateway traffic
|
|
615
|
+
// cannot continue as an ordinary Responses request because that would
|
|
616
|
+
// silently lose the required CPA session/account selection.
|
|
617
|
+
if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
|
|
618
|
+
notifyRemoteContextFailure(ctx, "codex-context-unavailable");
|
|
619
|
+
ctx.abort();
|
|
620
|
+
}
|
|
606
621
|
return undefined;
|
|
607
622
|
}
|
|
608
623
|
|
|
@@ -611,6 +626,7 @@ async function handleBeforeProviderRequest(
|
|
|
611
626
|
{
|
|
612
627
|
enabled: config.enabled,
|
|
613
628
|
responsesApis: config.responsesApis,
|
|
629
|
+
codexGatewayModels: config.contextManagement === "remote" ? config.gatewayContextModels : [],
|
|
614
630
|
},
|
|
615
631
|
event.payload,
|
|
616
632
|
);
|
|
@@ -877,7 +893,14 @@ export default function registerCompactionExtension(
|
|
|
877
893
|
pi.on("before_provider_headers", async (event, ctx) => {
|
|
878
894
|
const config = dependencies.loadConfig().config.compaction;
|
|
879
895
|
if (!isCodexContextModel(ctx.model, config)) return;
|
|
880
|
-
|
|
896
|
+
const active = await remoteContextActive(ctx, config);
|
|
897
|
+
if (!active) {
|
|
898
|
+
if (isCodexGatewayModel(ctx.model, config.gatewayContextModels)) {
|
|
899
|
+
notifyRemoteContextFailure(ctx, "codex-context-unavailable");
|
|
900
|
+
ctx.abort();
|
|
901
|
+
}
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
881
904
|
const provider = await resolveCodexContextProvider(ctx, ctx.model, config.gatewayContextModels);
|
|
882
905
|
if (provider.ok && provider.provider.kind === "codex-gateway") {
|
|
883
906
|
const sessionId = getSessionId(ctx);
|
|
@@ -885,6 +908,10 @@ export default function registerCompactionExtension(
|
|
|
885
908
|
sessionId,
|
|
886
909
|
clientRequestId: sessionId,
|
|
887
910
|
});
|
|
911
|
+
const allowedGatewayHeaders = new Set<string>(CODEX_GATEWAY_FORWARD_HEADERS);
|
|
912
|
+
for (const existing of Object.keys(event.headers)) {
|
|
913
|
+
if (!allowedGatewayHeaders.has(existing.toLowerCase())) delete event.headers[existing];
|
|
914
|
+
}
|
|
888
915
|
for (const name of [
|
|
889
916
|
"authorization",
|
|
890
917
|
"originator",
|
|
@@ -901,15 +928,13 @@ export default function registerCompactionExtension(
|
|
|
901
928
|
const value = gatewayHeaders.get(name);
|
|
902
929
|
if (value) event.headers[name] = value;
|
|
903
930
|
}
|
|
904
|
-
for (const existing of Object.keys(event.headers)) {
|
|
905
|
-
if (["cookie", "chatgpt-account-id", "x-api-key"].includes(existing.toLowerCase())) delete event.headers[existing];
|
|
906
|
-
}
|
|
907
931
|
}
|
|
908
932
|
contextWindows.rewriteHeaders(event.headers, ctx);
|
|
909
933
|
});
|
|
910
|
-
pi.on("message_end", (event, ctx) => {
|
|
934
|
+
pi.on("message_end", async (event, ctx) => {
|
|
911
935
|
const config = dependencies.loadConfig().config.compaction;
|
|
912
936
|
if (!isCodexContextModel(ctx.model, config) || !tools.isRegistered) return undefined;
|
|
937
|
+
if (!(await remoteContextActive(ctx, config))) return undefined;
|
|
913
938
|
const message = routeContextNamespaceToolMessage(event.message);
|
|
914
939
|
return message === event.message ? undefined : { message };
|
|
915
940
|
});
|