pi-openai-toolkit 0.12.0 → 0.13.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.
@@ -0,0 +1,427 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
+ import type { ProviderHeaders } from "@earendil-works/pi-ai";
4
+ import type {
5
+ CompactionResult,
6
+ ExtensionAPI,
7
+ ExtensionContext,
8
+ SessionBeforeCompactEvent,
9
+ SessionEntry,
10
+ } from "@earendil-works/pi-coding-agent";
11
+ import { ContextWindowBudget, type ContextRemaining } from "./window-budget";
12
+ import {
13
+ rewriteEncryptedToolOutputs,
14
+ rewriteWindowHeaders,
15
+ rewriteWindowPayload,
16
+ } from "./window-request";
17
+ import {
18
+ CONTEXT_WINDOW_COMPACTION_STRATEGY,
19
+ CONTEXT_WINDOW_COMPACTION_SUMMARY,
20
+ isContextWindowBoundary,
21
+ renderContextWindowMessage,
22
+ sendContextWindowMessage,
23
+ } from "./messages";
24
+ import {
25
+ CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
26
+ CONTEXT_MANAGEMENT_PROTOCOL,
27
+ type CodexContextManagementMessageDetails,
28
+ type ContextWindowCompactionDetails,
29
+ type ContextWindowIdentity,
30
+ isCodexContextManagementMessageDetails,
31
+ isNonEmptyString,
32
+ isRecord,
33
+ isContextWindowCompactionDetails,
34
+ } from "./types";
35
+ import { encodeEncryptedOutputForContext, loadHistoryNotesThreadHint } from "./history-notes";
36
+ import { rewriteContextNamespaceTools } from "./namespace-tools";
37
+
38
+ interface StartContextWindowOptions {
39
+ triggerTurn: boolean;
40
+ signal?: AbortSignal;
41
+ trimPreviousWindow: boolean;
42
+ }
43
+
44
+ type ThreadHintLoader = (
45
+ ctx: ExtensionContext,
46
+ signal?: AbortSignal,
47
+ ) => Promise<string | undefined>;
48
+
49
+ type WindowBoundaryEntry = Extract<SessionEntry, { type: "custom_message" }> & {
50
+ details: CodexContextManagementMessageDetails;
51
+ };
52
+
53
+ export class CodexContextWindowManager {
54
+ private identity: ContextWindowIdentity | undefined;
55
+ private sessionId: string | undefined;
56
+ private restoredMarkerId: string | undefined;
57
+ private readonly budget = new ContextWindowBudget();
58
+ private rolloverPending = false;
59
+ private trimPendingWindowId: string | undefined;
60
+ private readonly loadThreadHint: ThreadHintLoader;
61
+ private readonly getGatewayAllowlist: () => readonly string[];
62
+
63
+ constructor(
64
+ loadThreadHint?: ThreadHintLoader,
65
+ gatewayAllowlist: readonly string[] | (() => readonly string[]) = [],
66
+ ) {
67
+ this.getGatewayAllowlist = typeof gatewayAllowlist === "function" ? gatewayAllowlist : () => gatewayAllowlist;
68
+ this.loadThreadHint = loadThreadHint ?? ((ctx, signal) =>
69
+ loadHistoryNotesThreadHint(ctx, signal, this.getGatewayAllowlist()));
70
+ }
71
+
72
+ reset(): void {
73
+ this.identity = undefined;
74
+ this.sessionId = undefined;
75
+ this.restoredMarkerId = undefined;
76
+ this.budget.reset();
77
+ this.rolloverPending = false;
78
+ this.trimPendingWindowId = undefined;
79
+ }
80
+
81
+ currentIdentity(): ContextWindowIdentity | undefined {
82
+ return this.identity ? { ...this.identity } : undefined;
83
+ }
84
+
85
+ restore(entries: readonly SessionEntry[], sessionId?: string): void {
86
+ this.reset();
87
+ this.sessionId = sessionId;
88
+ for (const entry of entries) {
89
+ if (entry.type === "compaction") {
90
+ this.recordCompaction(entry.details);
91
+ continue;
92
+ }
93
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
94
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
95
+ if (!isCodexContextManagementMessageDetails(entry.details)) {
96
+ throw new Error("Malformed persisted Codex context-window message");
97
+ }
98
+ if (!matchesSession(entry.details.sessionId, sessionId)) continue;
99
+ const details = entry.details.contextManagement;
100
+ this.restoredMarkerId = entry.id;
101
+ if (details.kind === "window") {
102
+ this.identity = identityFromDetails(entry.details);
103
+ this.trimPendingWindowId = details.trimPreviousWindow ? details.currentWindowId : undefined;
104
+ }
105
+ this.budget.restore(details.kind, details.currentWindowId);
106
+ }
107
+ }
108
+
109
+ /** Rebuild state when Pi navigates to a different session/branch. */
110
+ synchronize(ctx: Pick<ExtensionContext, "sessionManager">): void {
111
+ const entries = ctx.sessionManager.getBranch();
112
+ const sessionId = ctx.sessionManager.getSessionId();
113
+ const latestMarkerId = findLatestContextMarkerId(entries, sessionId);
114
+ if (this.sessionId !== sessionId || this.restoredMarkerId !== latestMarkerId) {
115
+ this.restore(entries, sessionId);
116
+ }
117
+ }
118
+
119
+ ensureInitialized(pi: ExtensionAPI, ctx: ExtensionContext, active: boolean): void {
120
+ if (!active) return;
121
+ this.restore(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId());
122
+ if (this.identity) return;
123
+ const windowId = randomUUID();
124
+ this.sendWindowMessage(
125
+ pi,
126
+ ctx,
127
+ { firstWindowId: windowId, currentWindowId: windowId, windowNumber: 0 },
128
+ { triggerTurn: false, trimPreviousWindow: false },
129
+ );
130
+ }
131
+
132
+ project(
133
+ messages: readonly AgentMessage[],
134
+ mode: "off" | "remote",
135
+ ): AgentMessage[] {
136
+ if (mode === "off") {
137
+ return messages.filter(
138
+ (message) => message.role !== "custom" || message.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
139
+ );
140
+ }
141
+ let boundaryIndex = -1;
142
+ for (let index = 0; index < messages.length; index += 1) {
143
+ const message = messages[index]!;
144
+ if (
145
+ message.role === "custom" &&
146
+ message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE
147
+ ) {
148
+ if (!couldBelongToSession(message.details, this.sessionId)) continue;
149
+ if (!isCodexContextManagementMessageDetails(message.details)) {
150
+ throw new Error("Malformed persisted Codex context-window message");
151
+ }
152
+ if (!matchesSession(message.details.sessionId, this.sessionId)) continue;
153
+ }
154
+ if (!isContextWindowBoundary(message)) continue;
155
+ boundaryIndex = index;
156
+ this.identity = identityFromDetails(message.details);
157
+ }
158
+ if (boundaryIndex < 0) {
159
+ this.identity = undefined;
160
+ this.restoredMarkerId = undefined;
161
+ this.budget.reset();
162
+ this.trimPendingWindowId = undefined;
163
+ }
164
+ this.rolloverPending = false;
165
+ const projected = boundaryIndex < 0 ? [...messages] : [...messages.slice(boundaryIndex)];
166
+ return mode === "remote" ? projectEncryptedToolResults(projected) : projected;
167
+ }
168
+
169
+ async startNewWindow(
170
+ pi: ExtensionAPI,
171
+ ctx: ExtensionContext,
172
+ options: StartContextWindowOptions,
173
+ ): Promise<boolean> {
174
+ this.synchronize(ctx);
175
+ if (this.rolloverPending) return false;
176
+ if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
177
+ this.rolloverPending = true;
178
+ try {
179
+ const current = this.identity;
180
+ let threadHint: string | undefined;
181
+ if (current) {
182
+ try {
183
+ threadHint = await this.loadThreadHint(ctx, options.signal);
184
+ } catch (error) {
185
+ if (options.signal?.aborted) throw error;
186
+ }
187
+ }
188
+ if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
189
+ const currentWindowId = randomUUID();
190
+ const next: ContextWindowIdentity = current
191
+ ? {
192
+ firstWindowId: current.firstWindowId,
193
+ currentWindowId,
194
+ previousWindowId: current.currentWindowId,
195
+ windowNumber: current.windowNumber + 1,
196
+ }
197
+ : { firstWindowId: currentWindowId, currentWindowId, windowNumber: 0 };
198
+ this.sendWindowMessage(pi, ctx, next, options, threadHint);
199
+ return true;
200
+ } catch (error) {
201
+ this.rolloverPending = false;
202
+ throw error;
203
+ }
204
+ }
205
+
206
+ recordBudget(
207
+ pi: ExtensionAPI,
208
+ ctx: ExtensionContext,
209
+ active: boolean,
210
+ contextReminderThresholdPercent: number,
211
+ contextTokens?: number,
212
+ ): void {
213
+ if (!active || !this.identity || contextReminderThresholdPercent <= 0) return;
214
+ const reminder = this.budget.record(ctx, this.identity, contextTokens, contextReminderThresholdPercent);
215
+ if (!reminder) return;
216
+ sendContextWindowMessage(
217
+ pi,
218
+ reminder.content,
219
+ reminder.kind,
220
+ this.identity,
221
+ { triggerTurn: reminder.kind === "fallback", sessionId: ctx.sessionManager.getSessionId() },
222
+ );
223
+ }
224
+
225
+ remaining(ctx: ExtensionContext, contextTokens?: number): ContextRemaining {
226
+ return this.budget.remaining(ctx, this.identity, contextTokens);
227
+ }
228
+
229
+ /** True when a notes checkpoint succeeded in the current window (after the latest boundary). */
230
+ hasNotesCheckpointSinceBoundary(
231
+ ctx: Pick<ExtensionContext, "sessionManager">,
232
+ ): boolean {
233
+ return findNotesCheckpointSinceBoundary(ctx.sessionManager.getBranch(), this.sessionId);
234
+ }
235
+
236
+ prepareCompaction(
237
+ event: SessionBeforeCompactEvent,
238
+ ): { cancel: true } | { compaction: CompactionResult<ContextWindowCompactionDetails> } {
239
+ if (event.reason === "threshold") {
240
+ const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
241
+ if (!boundary || boundary.details.contextManagement.currentWindowId !== this.trimPendingWindowId) {
242
+ return { cancel: true };
243
+ }
244
+ }
245
+ return { compaction: this.createCompaction(event) };
246
+ }
247
+
248
+ recordCompaction(details: unknown): void {
249
+ if (!isContextWindowCompactionDetails(details)) return;
250
+ if (details.windowId === this.trimPendingWindowId) this.trimPendingWindowId = undefined;
251
+ }
252
+
253
+ createCompaction(event: SessionBeforeCompactEvent): CompactionResult<ContextWindowCompactionDetails> {
254
+ const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
255
+ return {
256
+ summary: CONTEXT_WINDOW_COMPACTION_SUMMARY,
257
+ firstKeptEntryId: boundary?.id ?? event.preparation.firstKeptEntryId,
258
+ tokensBefore: event.preparation.tokensBefore,
259
+ details: {
260
+ protocol: CONTEXT_MANAGEMENT_PROTOCOL,
261
+ strategy: CONTEXT_WINDOW_COMPACTION_STRATEGY,
262
+ ...(this.identity ? { windowId: this.identity.currentWindowId } : {}),
263
+ },
264
+ };
265
+ }
266
+
267
+ rewritePayload(payload: unknown, ctx: ExtensionContext): unknown {
268
+ const withMetadata = rewriteWindowPayload(payload, ctx, this.identity);
269
+ return rewriteContextNamespaceTools(withMetadata, { encrypted: true });
270
+ }
271
+
272
+ rewriteHeaders(headers: ProviderHeaders, ctx: ExtensionContext): void {
273
+ rewriteWindowHeaders(headers, ctx, this.identity);
274
+ }
275
+
276
+ private sendWindowMessage(
277
+ pi: ExtensionAPI,
278
+ ctx: ExtensionContext,
279
+ identity: ContextWindowIdentity,
280
+ options: StartContextWindowOptions,
281
+ threadHint?: string,
282
+ ): void {
283
+ sendContextWindowMessage(
284
+ pi,
285
+ renderContextWindowMessage(identity, threadHint),
286
+ "window",
287
+ identity,
288
+ { triggerTurn: options.triggerTurn, sessionId: ctx.sessionManager.getSessionId() },
289
+ options.trimPreviousWindow,
290
+ );
291
+ this.identity = identity;
292
+ this.sessionId = ctx.sessionManager.getSessionId();
293
+ this.restoredMarkerId = undefined;
294
+ this.trimPendingWindowId = options.trimPreviousWindow ? identity.currentWindowId : undefined;
295
+ // sendMessage has accepted the marker synchronously; clear only the
296
+ // in-flight guard so a later turn can roll over again.
297
+ this.rolloverPending = false;
298
+ }
299
+ }
300
+
301
+ function identityFromDetails(details: CodexContextManagementMessageDetails): ContextWindowIdentity {
302
+ const context = details.contextManagement;
303
+ return {
304
+ firstWindowId: context.firstWindowId,
305
+ currentWindowId: context.currentWindowId,
306
+ ...(context.previousWindowId ? { previousWindowId: context.previousWindowId } : {}),
307
+ windowNumber: context.windowNumber,
308
+ };
309
+ }
310
+
311
+ const NOTES_CHECKPOINT_ACTIONS: ReadonlySet<string> = new Set(["append_to_file", "write_file"]);
312
+
313
+ /**
314
+ * Whether the branch contains a successful notes append/write result after the
315
+ * latest window boundary. Reads and failed writes never count as checkpoints.
316
+ */
317
+ export function findNotesCheckpointSinceBoundary(
318
+ entries: readonly SessionEntry[],
319
+ sessionId?: string,
320
+ ): boolean {
321
+ let boundaryIndex = -1;
322
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
323
+ const entry = entries[index]!;
324
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
325
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
326
+ if (!isCodexContextManagementMessageDetails(entry.details)) {
327
+ throw new Error("Malformed persisted Codex context-window message");
328
+ }
329
+ if (!matchesSession(entry.details.sessionId, sessionId)) continue;
330
+ if (entry.details.contextManagement.kind === "window") {
331
+ boundaryIndex = index;
332
+ break;
333
+ }
334
+ }
335
+ if (boundaryIndex < 0) return false;
336
+ const checkpointCalls = new Set<string>();
337
+ for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
338
+ const entry = entries[index]!;
339
+ if (entry.type !== "message") continue;
340
+ const message = entry.message;
341
+ if (message.role === "assistant") {
342
+ const parts = Array.isArray(message.content) ? message.content : [];
343
+ for (const part of parts) {
344
+ if (!isRecord(part) || part.type !== "toolCall") continue;
345
+ // Match by call id only; the provider-side namespace rewrite never
346
+ // affects the names persisted in the Pi session branch.
347
+ if (part.name !== "notes") continue;
348
+ const args = isRecord(part.arguments) ? part.arguments : undefined;
349
+ const action = typeof args?.action === "string" ? args.action : "";
350
+ if (typeof part.id === "string" && NOTES_CHECKPOINT_ACTIONS.has(action)) {
351
+ checkpointCalls.add(part.id);
352
+ }
353
+ }
354
+ continue;
355
+ }
356
+ if (message.role === "toolResult" && checkpointCalls.has(message.toolCallId)) {
357
+ if (message.isError) {
358
+ checkpointCalls.delete(message.toolCallId);
359
+ continue;
360
+ }
361
+ const details = message.details;
362
+ if (isRecord(details) && isRecord(details.codexHistoryNotes)) return true;
363
+ }
364
+ }
365
+ return false;
366
+ }
367
+
368
+ export function findLatestWindowBoundaryEntry(
369
+ entries: readonly SessionEntry[],
370
+ sessionId?: string,
371
+ ): WindowBoundaryEntry | undefined {
372
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
373
+ const entry = entries[index]!;
374
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
375
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
376
+ if (!isCodexContextManagementMessageDetails(entry.details)) {
377
+ throw new Error("Malformed persisted Codex context-window message");
378
+ }
379
+ if (!matchesSession(entry.details.sessionId, sessionId)) continue;
380
+ if (entry.details.contextManagement.kind === "window") return entry as WindowBoundaryEntry;
381
+ }
382
+ return undefined;
383
+ }
384
+
385
+ function findLatestContextMarkerId(
386
+ entries: readonly SessionEntry[],
387
+ sessionId?: string,
388
+ ): string | undefined {
389
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
390
+ const entry = entries[index]!;
391
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
392
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
393
+ if (!isCodexContextManagementMessageDetails(entry.details)) {
394
+ throw new Error("Malformed persisted Codex context-window message");
395
+ }
396
+ if (matchesSession(entry.details.sessionId, sessionId)) return entry.id;
397
+ }
398
+ return undefined;
399
+ }
400
+
401
+ function couldBelongToSession(details: unknown, sessionId: string | undefined): boolean {
402
+ if (sessionId === undefined || !isRecord(details)) return true;
403
+ const markerSessionId = details.sessionId;
404
+ return !isNonEmptyString(markerSessionId) || markerSessionId === sessionId;
405
+ }
406
+
407
+ function matchesSession(markerSessionId: string | undefined, sessionId: string | undefined): boolean {
408
+ return sessionId === undefined || markerSessionId === sessionId;
409
+ }
410
+
411
+ function projectEncryptedToolResults(messages: readonly AgentMessage[]): AgentMessage[] {
412
+ let changed = false;
413
+ const projected = messages.map((message) => {
414
+ if (message.role !== "toolResult" || !isRecord(message.details)) return message;
415
+ const historyNotes = message.details.codexHistoryNotes;
416
+ if (!isRecord(historyNotes) || typeof historyNotes.encrypted_output !== "string") return message;
417
+ changed = true;
418
+ return {
419
+ ...message,
420
+ content: [
421
+ { type: "text" as const, text: encodeEncryptedOutputForContext(historyNotes.encrypted_output) },
422
+ ...message.content.filter((item) => item.type === "image"),
423
+ ],
424
+ };
425
+ });
426
+ return changed ? projected : [...messages];
427
+ }
@@ -0,0 +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
+ }
package/src/debug.ts CHANGED
@@ -14,11 +14,11 @@ import {
14
14
  type RedactOptions,
15
15
  } from "./types";
16
16
 
17
- const CRITICAL_KEY_RE = /(authorization|api[-_]?key|token|credential|oauth|auth|encrypted[_-]?content)/i;
18
- const SENSITIVE_KEY_RE = /(authorization|api[-_]?key|token|secret|password|cookie|set-cookie|signature|credential|oauth|auth|encrypted[_-]?content)/i;
17
+ const CRITICAL_KEY_RE = /(authorization|api[-_]?key|token|credential|oauth|auth|account[-_]?id|encrypted[_-]?(content|output))/i;
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;
21
- const HEADER_TOKEN_RE = /\b(x-api-key|api-key|authorization)\b\s*[:=]\s*[^\s,;]+/gi;
21
+ const HEADER_TOKEN_RE = /\b(x-api-key|api-key|authorization|chatgpt-account-id)\b\s*[:=]\s*[^\s,;]+/gi;
22
22
  const SENSITIVE_QUERY_RE = /([?&](?:api[-_]?key|access[-_]?token|token|secret|signature|credential|password)=)[^&#\s]*/gi;
23
23
 
24
24
  function ensureDir(dirPath: string) {