pi-openai-toolkit 0.14.7 → 0.14.9

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.
Files changed (47) hide show
  1. package/README.md +37 -8
  2. package/README.zh.md +37 -8
  3. package/docs/internals.md +27 -1
  4. package/extensions/auto-mode.ts +1 -1
  5. package/extensions/codex-astra.ts +1 -1
  6. package/extensions/compaction.ts +1 -1
  7. package/extensions/image-generation.ts +1 -1
  8. package/extensions/web-search.ts +1 -1
  9. package/package.json +6 -2
  10. package/src/auto-mode/classifier.ts +165 -165
  11. package/src/auto-mode/extension.ts +129 -20
  12. package/src/auto-mode/policy.ts +146 -146
  13. package/src/auto-mode/prompt.ts +279 -279
  14. package/src/auto-mode/reviewer.ts +325 -325
  15. package/src/auto-mode/tool-review-tui.ts +211 -0
  16. package/src/auto-mode/transcript.ts +133 -133
  17. package/src/auto-mode/types.ts +137 -137
  18. package/src/codex-astra/effort-planner.ts +175 -175
  19. package/src/codex-astra/extension.ts +122 -122
  20. package/src/compaction-projection.ts +31 -0
  21. package/src/config.ts +100 -1
  22. package/src/context-management/messages.ts +112 -112
  23. package/src/context-management/namespace-tools.ts +426 -426
  24. package/src/context-management/tool-contract.ts +23 -23
  25. package/src/context-management/tools.ts +291 -279
  26. package/src/context-management/window-budget.ts +83 -83
  27. package/src/context-management/window-manager.ts +110 -28
  28. package/src/context-management/window-request.ts +125 -125
  29. package/src/extension-runtime.ts +263 -34
  30. package/src/image-generation/artifacts.ts +237 -237
  31. package/src/image-generation/client.ts +248 -248
  32. package/src/image-generation/eligibility.ts +18 -18
  33. package/src/image-generation/references.ts +194 -194
  34. package/src/image-generation/render.ts +252 -252
  35. package/src/model-scope.ts +17 -17
  36. package/src/payload-rewrite.ts +55 -10
  37. package/src/pi-context-hook.ts +116 -0
  38. package/src/provider-headers.ts +23 -23
  39. package/src/request-context-cache.ts +16 -1
  40. package/src/runtime.ts +52 -0
  41. package/src/types.ts +46 -0
  42. package/src/web-search/alpha-client.ts +875 -0
  43. package/src/web-search/extension.ts +523 -80
  44. package/src/web-search/payload.ts +295 -96
  45. package/src/web-search/prompt.ts +59 -20
  46. package/src/web-search/types.ts +144 -45
  47. package/tsconfig.check.json +2 -0
@@ -1,83 +1,83 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import {
3
- CONTEXT_WINDOW_FALLBACK_BUFFER,
4
- CONTEXT_WINDOW_FALLBACK_MESSAGE,
5
- renderContextWindowReminder,
6
- type ContextManagementMessageKind,
7
- type ContextWindowIdentity,
8
- } from "./messages";
9
-
10
- export interface ContextRemaining {
11
- remainingTokens: number | undefined;
12
- windowId: string | undefined;
13
- contextWindow: number;
14
- }
15
-
16
- export class ContextWindowBudget {
17
- private readonly remindedWindows = new Set<string>();
18
- private readonly exhaustedWindows = new Set<string>();
19
-
20
- reset(): void {
21
- this.remindedWindows.clear();
22
- this.exhaustedWindows.clear();
23
- }
24
-
25
- restore(kind: ContextManagementMessageKind, windowId: string): void {
26
- if (kind === "reminder") this.remindedWindows.add(windowId);
27
- if (kind === "fallback") this.exhaustedWindows.add(windowId);
28
- }
29
-
30
- record(
31
- ctx: ExtensionContext,
32
- identity: ContextWindowIdentity,
33
- contextTokens: number | undefined,
34
- contextReminderThresholdPercent: number,
35
- ): { content: string; kind: "fallback" | "reminder" } | undefined {
36
- const remaining = this.remaining(ctx, identity, contextTokens);
37
- if (remaining.remainingTokens === undefined) return undefined;
38
- const windowId = identity.currentWindowId;
39
- if (remaining.remainingTokens <= 0 && !this.exhaustedWindows.has(windowId)) {
40
- this.exhaustedWindows.add(windowId);
41
- return { content: CONTEXT_WINDOW_FALLBACK_MESSAGE, kind: "fallback" };
42
- }
43
- const reminderThreshold =
44
- contextReminderThresholdPercent > 0
45
- ? Math.floor((remaining.contextWindow * contextReminderThresholdPercent) / 100)
46
- : 0;
47
- if (
48
- contextReminderThresholdPercent > 0 &&
49
- remaining.remainingTokens <= reminderThreshold &&
50
- !this.remindedWindows.has(windowId)
51
- ) {
52
- this.remindedWindows.add(windowId);
53
- return { content: renderContextWindowReminder(remaining.remainingTokens), kind: "reminder" };
54
- }
55
- return undefined;
56
- }
57
-
58
- remaining(
59
- ctx: ExtensionContext,
60
- identity: ContextWindowIdentity | undefined,
61
- contextTokens?: number,
62
- ): ContextRemaining {
63
- const usage = ctx.getContextUsage();
64
- const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
65
- const limit = Math.max(0, contextWindow - CONTEXT_WINDOW_FALLBACK_BUFFER);
66
- if (usage === undefined && contextTokens === undefined) {
67
- return {
68
- remainingTokens: undefined,
69
- windowId: identity?.currentWindowId,
70
- contextWindow: limit,
71
- };
72
- }
73
- const used = contextTokens ?? usage?.tokens;
74
- return {
75
- remainingTokens:
76
- used === null || used === undefined
77
- ? undefined
78
- : Math.max(0, limit - used),
79
- windowId: identity?.currentWindowId,
80
- contextWindow: limit,
81
- };
82
- }
83
- }
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ CONTEXT_WINDOW_FALLBACK_BUFFER,
4
+ CONTEXT_WINDOW_FALLBACK_MESSAGE,
5
+ renderContextWindowReminder,
6
+ type ContextManagementMessageKind,
7
+ type ContextWindowIdentity,
8
+ } from "./messages";
9
+
10
+ export interface ContextRemaining {
11
+ remainingTokens: number | undefined;
12
+ windowId: string | undefined;
13
+ contextWindow: number;
14
+ }
15
+
16
+ export class ContextWindowBudget {
17
+ private readonly remindedWindows = new Set<string>();
18
+ private readonly exhaustedWindows = new Set<string>();
19
+
20
+ reset(): void {
21
+ this.remindedWindows.clear();
22
+ this.exhaustedWindows.clear();
23
+ }
24
+
25
+ restore(kind: ContextManagementMessageKind, windowId: string): void {
26
+ if (kind === "reminder") this.remindedWindows.add(windowId);
27
+ if (kind === "fallback") this.exhaustedWindows.add(windowId);
28
+ }
29
+
30
+ record(
31
+ ctx: ExtensionContext,
32
+ identity: ContextWindowIdentity,
33
+ contextTokens: number | undefined,
34
+ contextReminderThresholdPercent: number,
35
+ ): { content: string; kind: "fallback" | "reminder" } | undefined {
36
+ const remaining = this.remaining(ctx, identity, contextTokens);
37
+ if (remaining.remainingTokens === undefined) return undefined;
38
+ const windowId = identity.currentWindowId;
39
+ if (remaining.remainingTokens <= 0 && !this.exhaustedWindows.has(windowId)) {
40
+ this.exhaustedWindows.add(windowId);
41
+ return { content: CONTEXT_WINDOW_FALLBACK_MESSAGE, kind: "fallback" };
42
+ }
43
+ const reminderThreshold =
44
+ contextReminderThresholdPercent > 0
45
+ ? Math.floor((remaining.contextWindow * contextReminderThresholdPercent) / 100)
46
+ : 0;
47
+ if (
48
+ contextReminderThresholdPercent > 0 &&
49
+ remaining.remainingTokens <= reminderThreshold &&
50
+ !this.remindedWindows.has(windowId)
51
+ ) {
52
+ this.remindedWindows.add(windowId);
53
+ return { content: renderContextWindowReminder(remaining.remainingTokens), kind: "reminder" };
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ remaining(
59
+ ctx: ExtensionContext,
60
+ identity: ContextWindowIdentity | undefined,
61
+ contextTokens?: number,
62
+ ): ContextRemaining {
63
+ const usage = ctx.getContextUsage();
64
+ const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
65
+ const limit = Math.max(0, contextWindow - CONTEXT_WINDOW_FALLBACK_BUFFER);
66
+ if (usage === undefined && contextTokens === undefined) {
67
+ return {
68
+ remainingTokens: undefined,
69
+ windowId: identity?.currentWindowId,
70
+ contextWindow: limit,
71
+ };
72
+ }
73
+ const used = contextTokens ?? usage?.tokens;
74
+ return {
75
+ remainingTokens:
76
+ used === null || used === undefined
77
+ ? undefined
78
+ : Math.max(0, limit - used),
79
+ windowId: identity?.currentWindowId,
80
+ contextWindow: limit,
81
+ };
82
+ }
83
+ }
@@ -55,12 +55,23 @@ type WindowBoundaryEntry = Extract<SessionEntry, { type: "custom_message" }> & {
55
55
  details: CodexContextManagementMessageDetails;
56
56
  };
57
57
 
58
+ /**
59
+ * In-process duplicate guard for one scheduled rollover. It is anchored to the
60
+ * session that scheduled it and to the exact target window id, so it can never
61
+ * be satisfied by an older or foreign marker. It is not checkpoint evidence:
62
+ * only a persisted notes result unlocks the gate.
63
+ */
64
+ type PendingRollover = {
65
+ sessionId?: string;
66
+ targetWindowId: string;
67
+ };
68
+
58
69
  export class CodexContextWindowManager {
59
70
  private identity: ContextWindowIdentity | undefined;
60
71
  private sessionId: string | undefined;
61
72
  private restoredMarkerId: string | undefined;
62
73
  private readonly budget = new ContextWindowBudget();
63
- private rolloverPending = false;
74
+ private pendingRollover: PendingRollover | undefined;
64
75
  private trimPendingWindowId: string | undefined;
65
76
  private readonly loadThreadHint: ThreadHintLoader;
66
77
 
@@ -69,20 +80,26 @@ export class CodexContextWindowManager {
69
80
  }
70
81
 
71
82
  reset(): void {
83
+ this.resetWindowState();
84
+ this.pendingRollover = undefined;
85
+ }
86
+
87
+ currentIdentity(): ContextWindowIdentity | undefined {
88
+ return this.identity ? { ...this.identity } : undefined;
89
+ }
90
+
91
+ private resetWindowState(): void {
72
92
  this.identity = undefined;
73
93
  this.sessionId = undefined;
74
94
  this.restoredMarkerId = undefined;
75
95
  this.budget.reset();
76
- this.rolloverPending = false;
77
96
  this.trimPendingWindowId = undefined;
78
97
  }
79
98
 
80
- currentIdentity(): ContextWindowIdentity | undefined {
81
- return this.identity ? { ...this.identity } : undefined;
82
- }
83
-
84
99
  restore(entries: readonly SessionEntry[], sessionId?: string): void {
85
- this.reset();
100
+ // A rebuild of derived state must not drop an outstanding rollover guard;
101
+ // only a changed session or an observed target marker retires it below.
102
+ this.resetWindowState();
86
103
  this.sessionId = sessionId;
87
104
  for (const entry of entries) {
88
105
  if (entry.type === "compaction") {
@@ -103,21 +120,43 @@ export class CodexContextWindowManager {
103
120
  }
104
121
  this.budget.restore(details.kind, details.currentWindowId);
105
122
  }
123
+ this.retireSatisfiedOrStalePending(entries, sessionId);
106
124
  }
107
125
 
108
126
  /** Rebuild state when Pi navigates to a different session/branch. */
109
127
  synchronize(ctx: Pick<ExtensionContext, "sessionManager">): void {
110
128
  const entries = ctx.sessionManager.getBranch();
111
129
  const sessionId = ctx.sessionManager.getSessionId();
130
+ this.retireSatisfiedOrStalePending(entries, sessionId);
112
131
  const latestMarkerId = findLatestContextMarkerId(entries, sessionId);
113
132
  if (this.sessionId !== sessionId || this.restoredMarkerId !== latestMarkerId) {
114
133
  this.restore(entries, sessionId);
115
134
  }
116
135
  }
117
136
 
137
+ /**
138
+ * Drop a pending rollover only for durable reasons: the target window marker
139
+ * is now persisted in this session, or the session changed. A queued marker
140
+ * visible in an in-memory message list is not durable evidence.
141
+ */
142
+ private retireSatisfiedOrStalePending(
143
+ entries: readonly SessionEntry[],
144
+ sessionId: string | undefined,
145
+ ): void {
146
+ const pending = this.pendingRollover;
147
+ if (!pending) return;
148
+ if (pending.sessionId !== undefined && pending.sessionId !== sessionId) {
149
+ this.pendingRollover = undefined;
150
+ return;
151
+ }
152
+ if (hasPersistedWindowBoundary(entries, sessionId, pending.targetWindowId)) {
153
+ this.pendingRollover = undefined;
154
+ }
155
+ }
156
+
118
157
  ensureInitialized(pi: ExtensionAPI, ctx: ExtensionContext, active: boolean): void {
119
158
  if (!active) return;
120
- this.restore(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId());
159
+ this.synchronize(ctx);
121
160
  if (this.identity) return;
122
161
  const windowId = randomUUID();
123
162
  this.sendWindowMessage(
@@ -160,7 +199,8 @@ export class CodexContextWindowManager {
160
199
  this.budget.reset();
161
200
  this.trimPendingWindowId = undefined;
162
201
  }
163
- this.rolloverPending = false;
202
+ // Projection observes the request view, never durable session state. A
203
+ // queued rollover marker must not clear the duplicate guard here.
164
204
  const projected = boundaryIndex < 0 ? [...messages] : [...messages.slice(boundaryIndex)];
165
205
  return mode === "remote" ? projectEncryptedToolResults(projected) : projected;
166
206
  }
@@ -171,13 +211,25 @@ export class CodexContextWindowManager {
171
211
  options: StartContextWindowOptions,
172
212
  ): Promise<boolean> {
173
213
  this.synchronize(ctx);
174
- if (this.rolloverPending) return false;
214
+ // A same-session rollover whose target marker is not persisted yet is
215
+ // still in flight: report it instead of scheduling a second window.
216
+ if (this.hasPendingRollover(ctx)) return false;
175
217
  if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
176
- this.rolloverPending = true;
218
+ const current = this.identity;
219
+ const liveSessionId = ctx.sessionManager.getSessionId();
220
+ const currentWindowId = randomUUID();
221
+ const next: ContextWindowIdentity = current
222
+ ? {
223
+ firstWindowId: current.firstWindowId,
224
+ currentWindowId,
225
+ previousWindowId: current.currentWindowId,
226
+ windowNumber: current.windowNumber + 1,
227
+ }
228
+ : { firstWindowId: currentWindowId, currentWindowId, windowNumber: 0 };
229
+ this.pendingRollover = { sessionId: liveSessionId, targetWindowId: currentWindowId };
177
230
  try {
178
- const current = this.identity;
179
231
  const checkpoint = current
180
- ? findLatestNotesCheckpointSinceBoundary(ctx.sessionManager.getBranch(), this.sessionId)
232
+ ? findLatestNotesCheckpointSinceBoundary(ctx.sessionManager.getBranch(), liveSessionId)
181
233
  : undefined;
182
234
  let threadHint: string | undefined;
183
235
  if (current) {
@@ -188,19 +240,10 @@ export class CodexContextWindowManager {
188
240
  }
189
241
  }
190
242
  if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
191
- const currentWindowId = randomUUID();
192
- const next: ContextWindowIdentity = current
193
- ? {
194
- firstWindowId: current.firstWindowId,
195
- currentWindowId,
196
- previousWindowId: current.currentWindowId,
197
- windowNumber: current.windowNumber + 1,
198
- }
199
- : { firstWindowId: currentWindowId, currentWindowId, windowNumber: 0 };
200
243
  this.sendWindowMessage(pi, ctx, next, options, threadHint, checkpoint);
201
244
  return true;
202
245
  } catch (error) {
203
- this.rolloverPending = false;
246
+ this.pendingRollover = undefined;
204
247
  throw error;
205
248
  }
206
249
  }
@@ -234,11 +277,30 @@ export class CodexContextWindowManager {
234
277
  return this.budget.remaining(ctx, this.identity, contextTokens);
235
278
  }
236
279
 
237
- /** True when a notes checkpoint succeeded in the current window (after the latest boundary). */
280
+ /**
281
+ * True when a notes checkpoint succeeded in the current window (after the
282
+ * latest boundary). The scan always uses the live session id: a cached id can
283
+ * belong to a session Pi already navigated away from, which would filter the
284
+ * current window's marker and result out as foreign.
285
+ */
238
286
  hasNotesCheckpointSinceBoundary(
239
287
  ctx: Pick<ExtensionContext, "sessionManager">,
240
288
  ): boolean {
241
- return findNotesCheckpointSinceBoundary(ctx.sessionManager.getBranch(), this.sessionId);
289
+ return findNotesCheckpointSinceBoundary(
290
+ ctx.sessionManager.getBranch(),
291
+ ctx.sessionManager.getSessionId(),
292
+ );
293
+ }
294
+
295
+ /**
296
+ * True while a rollover scheduled for the current session is awaiting its
297
+ * persisted target marker. Call `synchronize()` first so a marker that has
298
+ * already been persisted or a session switch retires the guard.
299
+ */
300
+ hasPendingRollover(ctx: Pick<ExtensionContext, "sessionManager">): boolean {
301
+ const pending = this.pendingRollover;
302
+ if (!pending) return false;
303
+ return pending.sessionId === undefined || pending.sessionId === ctx.sessionManager.getSessionId();
242
304
  }
243
305
 
244
306
  prepareCompaction(
@@ -311,9 +373,9 @@ export class CodexContextWindowManager {
311
373
  this.sessionId = ctx.sessionManager.getSessionId();
312
374
  this.restoredMarkerId = undefined;
313
375
  this.trimPendingWindowId = options.trimPreviousWindow ? identity.currentWindowId : undefined;
314
- // sendMessage has accepted the marker synchronously; clear only the
315
- // in-flight guard so a later turn can roll over again.
316
- this.rolloverPending = false;
376
+ // sendMessage() acceptance is not persistence. The pending rollover stays
377
+ // armed until synchronize() observes this target window in the persisted
378
+ // branch (or the session changes).
317
379
  }
318
380
  }
319
381
 
@@ -443,6 +505,26 @@ export function findLatestWindowBoundaryEntry(
443
505
  return undefined;
444
506
  }
445
507
 
508
+ /**
509
+ * Whether this exact window boundary is already persisted for the session.
510
+ * Only durable entries count; a queued/visible marker does not.
511
+ */
512
+ function hasPersistedWindowBoundary(
513
+ entries: readonly SessionEntry[],
514
+ sessionId: string | undefined,
515
+ windowId: string,
516
+ ): boolean {
517
+ for (const entry of entries) {
518
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
519
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
520
+ if (!isCodexContextManagementMessageDetails(entry.details)) continue;
521
+ if (!matchesSession(entry.details.sessionId, sessionId)) continue;
522
+ const details = entry.details.contextManagement;
523
+ if (details.kind === "window" && details.currentWindowId === windowId) return true;
524
+ }
525
+ return false;
526
+ }
527
+
446
528
  function findLatestContextMarkerId(
447
529
  entries: readonly SessionEntry[],
448
530
  sessionId?: string,
@@ -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
+ }