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.
- package/README.md +37 -8
- package/README.zh.md +37 -8
- package/docs/internals.md +27 -1
- package/extensions/auto-mode.ts +1 -1
- package/extensions/codex-astra.ts +1 -1
- package/extensions/compaction.ts +1 -1
- package/extensions/image-generation.ts +1 -1
- package/extensions/web-search.ts +1 -1
- package/package.json +6 -2
- package/src/auto-mode/classifier.ts +165 -165
- package/src/auto-mode/extension.ts +129 -20
- 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/tool-review-tui.ts +211 -0
- 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/compaction-projection.ts +31 -0
- package/src/config.ts +100 -1
- package/src/context-management/messages.ts +112 -112
- package/src/context-management/namespace-tools.ts +426 -426
- package/src/context-management/tool-contract.ts +23 -23
- package/src/context-management/tools.ts +291 -279
- package/src/context-management/window-budget.ts +83 -83
- package/src/context-management/window-manager.ts +110 -28
- package/src/context-management/window-request.ts +125 -125
- package/src/extension-runtime.ts +263 -34
- 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/references.ts +194 -194
- package/src/image-generation/render.ts +252 -252
- package/src/model-scope.ts +17 -17
- package/src/payload-rewrite.ts +55 -10
- package/src/pi-context-hook.ts +116 -0
- package/src/provider-headers.ts +23 -23
- package/src/request-context-cache.ts +16 -1
- package/src/runtime.ts +52 -0
- package/src/types.ts +46 -0
- package/src/web-search/alpha-client.ts +875 -0
- package/src/web-search/extension.ts +523 -80
- package/src/web-search/payload.ts +295 -96
- package/src/web-search/prompt.ts +59 -20
- package/src/web-search/types.ts +144 -45
- 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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
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.
|
|
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
|
-
/**
|
|
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(
|
|
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
|
|
315
|
-
//
|
|
316
|
-
|
|
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
|
+
}
|