pi-openai-toolkit 0.14.2 → 0.14.3

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.
@@ -1,421 +1,479 @@
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
-
62
- constructor(loadThreadHint?: ThreadHintLoader) {
63
- this.loadThreadHint = loadThreadHint ?? ((ctx, signal) => loadHistoryNotesThreadHint(ctx, signal));
64
- }
65
-
66
- reset(): void {
67
- this.identity = undefined;
68
- this.sessionId = undefined;
69
- this.restoredMarkerId = undefined;
70
- this.budget.reset();
71
- this.rolloverPending = false;
72
- this.trimPendingWindowId = undefined;
73
- }
74
-
75
- currentIdentity(): ContextWindowIdentity | undefined {
76
- return this.identity ? { ...this.identity } : undefined;
77
- }
78
-
79
- restore(entries: readonly SessionEntry[], sessionId?: string): void {
80
- this.reset();
81
- this.sessionId = sessionId;
82
- for (const entry of entries) {
83
- if (entry.type === "compaction") {
84
- this.recordCompaction(entry.details);
85
- continue;
86
- }
87
- if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
88
- if (!couldBelongToSession(entry.details, sessionId)) continue;
89
- if (!isCodexContextManagementMessageDetails(entry.details)) {
90
- throw new Error("Malformed persisted Codex context-window message");
91
- }
92
- if (!matchesSession(entry.details.sessionId, sessionId)) continue;
93
- const details = entry.details.contextManagement;
94
- this.restoredMarkerId = entry.id;
95
- if (details.kind === "window") {
96
- this.identity = identityFromDetails(entry.details);
97
- this.trimPendingWindowId = details.trimPreviousWindow ? details.currentWindowId : undefined;
98
- }
99
- this.budget.restore(details.kind, details.currentWindowId);
100
- }
101
- }
102
-
103
- /** Rebuild state when Pi navigates to a different session/branch. */
104
- synchronize(ctx: Pick<ExtensionContext, "sessionManager">): void {
105
- const entries = ctx.sessionManager.getBranch();
106
- const sessionId = ctx.sessionManager.getSessionId();
107
- const latestMarkerId = findLatestContextMarkerId(entries, sessionId);
108
- if (this.sessionId !== sessionId || this.restoredMarkerId !== latestMarkerId) {
109
- this.restore(entries, sessionId);
110
- }
111
- }
112
-
113
- ensureInitialized(pi: ExtensionAPI, ctx: ExtensionContext, active: boolean): void {
114
- if (!active) return;
115
- this.restore(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId());
116
- if (this.identity) return;
117
- const windowId = randomUUID();
118
- this.sendWindowMessage(
119
- pi,
120
- ctx,
121
- { firstWindowId: windowId, currentWindowId: windowId, windowNumber: 0 },
122
- { triggerTurn: false, trimPreviousWindow: false },
123
- );
124
- }
125
-
126
- project(
127
- messages: readonly AgentMessage[],
128
- mode: "off" | "remote",
129
- ): AgentMessage[] {
130
- if (mode === "off") {
131
- return messages.filter(
132
- (message) => message.role !== "custom" || message.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
133
- );
134
- }
135
- let boundaryIndex = -1;
136
- for (let index = 0; index < messages.length; index += 1) {
137
- const message = messages[index]!;
138
- if (
139
- message.role === "custom" &&
140
- message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE
141
- ) {
142
- if (!couldBelongToSession(message.details, this.sessionId)) continue;
143
- if (!isCodexContextManagementMessageDetails(message.details)) {
144
- throw new Error("Malformed persisted Codex context-window message");
145
- }
146
- if (!matchesSession(message.details.sessionId, this.sessionId)) continue;
147
- }
148
- if (!isContextWindowBoundary(message)) continue;
149
- boundaryIndex = index;
150
- this.identity = identityFromDetails(message.details);
151
- }
152
- if (boundaryIndex < 0) {
153
- this.identity = undefined;
154
- this.restoredMarkerId = undefined;
155
- this.budget.reset();
156
- this.trimPendingWindowId = undefined;
157
- }
158
- this.rolloverPending = false;
159
- const projected = boundaryIndex < 0 ? [...messages] : [...messages.slice(boundaryIndex)];
160
- return mode === "remote" ? projectEncryptedToolResults(projected) : projected;
161
- }
162
-
163
- async startNewWindow(
164
- pi: ExtensionAPI,
165
- ctx: ExtensionContext,
166
- options: StartContextWindowOptions,
167
- ): Promise<boolean> {
168
- this.synchronize(ctx);
169
- if (this.rolloverPending) return false;
170
- if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
171
- this.rolloverPending = true;
172
- try {
173
- const current = this.identity;
174
- let threadHint: string | undefined;
175
- if (current) {
176
- try {
177
- threadHint = await this.loadThreadHint(ctx, options.signal);
178
- } catch (error) {
179
- if (options.signal?.aborted) throw error;
180
- }
181
- }
182
- if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
183
- const currentWindowId = randomUUID();
184
- const next: ContextWindowIdentity = current
185
- ? {
186
- firstWindowId: current.firstWindowId,
187
- currentWindowId,
188
- previousWindowId: current.currentWindowId,
189
- windowNumber: current.windowNumber + 1,
190
- }
191
- : { firstWindowId: currentWindowId, currentWindowId, windowNumber: 0 };
192
- this.sendWindowMessage(pi, ctx, next, options, threadHint);
193
- return true;
194
- } catch (error) {
195
- this.rolloverPending = false;
196
- throw error;
197
- }
198
- }
199
-
200
- recordBudget(
201
- pi: ExtensionAPI,
202
- ctx: ExtensionContext,
203
- active: boolean,
204
- contextReminderThresholdPercent: number,
205
- contextTokens?: number,
206
- ): void {
207
- if (!active || !this.identity || contextReminderThresholdPercent <= 0) return;
208
- const reminder = this.budget.record(ctx, this.identity, contextTokens, contextReminderThresholdPercent);
209
- if (!reminder) return;
210
- sendContextWindowMessage(
211
- pi,
212
- reminder.content,
213
- reminder.kind,
214
- this.identity,
215
- { triggerTurn: reminder.kind === "fallback", sessionId: ctx.sessionManager.getSessionId() },
216
- );
217
- }
218
-
219
- remaining(ctx: ExtensionContext, contextTokens?: number): ContextRemaining {
220
- return this.budget.remaining(ctx, this.identity, contextTokens);
221
- }
222
-
223
- /** True when a notes checkpoint succeeded in the current window (after the latest boundary). */
224
- hasNotesCheckpointSinceBoundary(
225
- ctx: Pick<ExtensionContext, "sessionManager">,
226
- ): boolean {
227
- return findNotesCheckpointSinceBoundary(ctx.sessionManager.getBranch(), this.sessionId);
228
- }
229
-
230
- prepareCompaction(
231
- event: SessionBeforeCompactEvent,
232
- ): { cancel: true } | { compaction: CompactionResult<ContextWindowCompactionDetails> } {
233
- if (event.reason === "threshold") {
234
- const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
235
- if (!boundary || boundary.details.contextManagement.currentWindowId !== this.trimPendingWindowId) {
236
- return { cancel: true };
237
- }
238
- }
239
- return { compaction: this.createCompaction(event) };
240
- }
241
-
242
- recordCompaction(details: unknown): void {
243
- if (!isContextWindowCompactionDetails(details)) return;
244
- if (details.windowId === this.trimPendingWindowId) this.trimPendingWindowId = undefined;
245
- }
246
-
247
- createCompaction(event: SessionBeforeCompactEvent): CompactionResult<ContextWindowCompactionDetails> {
248
- const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
249
- return {
250
- summary: CONTEXT_WINDOW_COMPACTION_SUMMARY,
251
- firstKeptEntryId: boundary?.id ?? event.preparation.firstKeptEntryId,
252
- tokensBefore: event.preparation.tokensBefore,
253
- details: {
254
- protocol: CONTEXT_MANAGEMENT_PROTOCOL,
255
- strategy: CONTEXT_WINDOW_COMPACTION_STRATEGY,
256
- ...(this.identity ? { windowId: this.identity.currentWindowId } : {}),
257
- },
258
- };
259
- }
260
-
261
- rewritePayload(payload: unknown, ctx: ExtensionContext): unknown {
262
- const withMetadata = rewriteWindowPayload(payload, ctx, this.identity);
263
- return rewriteContextNamespaceTools(withMetadata, { encrypted: true });
264
- }
265
-
266
- rewriteHeaders(headers: ProviderHeaders, ctx: ExtensionContext): void {
267
- rewriteWindowHeaders(headers, ctx, this.identity);
268
- }
269
-
270
- private sendWindowMessage(
271
- pi: ExtensionAPI,
272
- ctx: ExtensionContext,
273
- identity: ContextWindowIdentity,
274
- options: StartContextWindowOptions,
275
- threadHint?: string,
276
- ): void {
277
- sendContextWindowMessage(
278
- pi,
279
- renderContextWindowMessage(identity, threadHint),
280
- "window",
281
- identity,
282
- { triggerTurn: options.triggerTurn, sessionId: ctx.sessionManager.getSessionId() },
283
- options.trimPreviousWindow,
284
- );
285
- this.identity = identity;
286
- this.sessionId = ctx.sessionManager.getSessionId();
287
- this.restoredMarkerId = undefined;
288
- this.trimPendingWindowId = options.trimPreviousWindow ? identity.currentWindowId : undefined;
289
- // sendMessage has accepted the marker synchronously; clear only the
290
- // in-flight guard so a later turn can roll over again.
291
- this.rolloverPending = false;
292
- }
293
- }
294
-
295
- function identityFromDetails(details: CodexContextManagementMessageDetails): ContextWindowIdentity {
296
- const context = details.contextManagement;
297
- return {
298
- firstWindowId: context.firstWindowId,
299
- currentWindowId: context.currentWindowId,
300
- ...(context.previousWindowId ? { previousWindowId: context.previousWindowId } : {}),
301
- windowNumber: context.windowNumber,
302
- };
303
- }
304
-
305
- const NOTES_CHECKPOINT_ACTIONS: ReadonlySet<string> = new Set(["append_to_file", "write_file"]);
306
-
307
- /**
308
- * Whether the branch contains a successful notes append/write result after the
309
- * latest window boundary. Reads and failed writes never count as checkpoints.
310
- */
311
- export function findNotesCheckpointSinceBoundary(
312
- entries: readonly SessionEntry[],
313
- sessionId?: string,
314
- ): boolean {
315
- let boundaryIndex = -1;
316
- for (let index = entries.length - 1; index >= 0; index -= 1) {
317
- const entry = entries[index]!;
318
- if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
319
- if (!couldBelongToSession(entry.details, sessionId)) continue;
320
- if (!isCodexContextManagementMessageDetails(entry.details)) {
321
- throw new Error("Malformed persisted Codex context-window message");
322
- }
323
- if (!matchesSession(entry.details.sessionId, sessionId)) continue;
324
- if (entry.details.contextManagement.kind === "window") {
325
- boundaryIndex = index;
326
- break;
327
- }
328
- }
329
- if (boundaryIndex < 0) return false;
330
- const checkpointCalls = new Set<string>();
331
- for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
332
- const entry = entries[index]!;
333
- if (entry.type !== "message") continue;
334
- const message = entry.message;
335
- if (message.role === "assistant") {
336
- const parts = Array.isArray(message.content) ? message.content : [];
337
- for (const part of parts) {
338
- if (!isRecord(part) || part.type !== "toolCall") continue;
339
- // Match by call id only; the provider-side namespace rewrite never
340
- // affects the names persisted in the Pi session branch.
341
- if (part.name !== "notes") continue;
342
- const args = isRecord(part.arguments) ? part.arguments : undefined;
343
- const action = typeof args?.action === "string" ? args.action : "";
344
- if (typeof part.id === "string" && NOTES_CHECKPOINT_ACTIONS.has(action)) {
345
- checkpointCalls.add(part.id);
346
- }
347
- }
348
- continue;
349
- }
350
- if (message.role === "toolResult" && checkpointCalls.has(message.toolCallId)) {
351
- if (message.isError) {
352
- checkpointCalls.delete(message.toolCallId);
353
- continue;
354
- }
355
- const details = message.details;
356
- if (isRecord(details) && isRecord(details.codexHistoryNotes)) return true;
357
- }
358
- }
359
- return false;
360
- }
361
-
362
- export function findLatestWindowBoundaryEntry(
363
- entries: readonly SessionEntry[],
364
- sessionId?: string,
365
- ): WindowBoundaryEntry | undefined {
366
- for (let index = entries.length - 1; index >= 0; index -= 1) {
367
- const entry = entries[index]!;
368
- if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
369
- if (!couldBelongToSession(entry.details, sessionId)) continue;
370
- if (!isCodexContextManagementMessageDetails(entry.details)) {
371
- throw new Error("Malformed persisted Codex context-window message");
372
- }
373
- if (!matchesSession(entry.details.sessionId, sessionId)) continue;
374
- if (entry.details.contextManagement.kind === "window") return entry as WindowBoundaryEntry;
375
- }
376
- return undefined;
377
- }
378
-
379
- function findLatestContextMarkerId(
380
- entries: readonly SessionEntry[],
381
- sessionId?: string,
382
- ): string | undefined {
383
- for (let index = entries.length - 1; index >= 0; index -= 1) {
384
- const entry = entries[index]!;
385
- if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
386
- if (!couldBelongToSession(entry.details, sessionId)) continue;
387
- if (!isCodexContextManagementMessageDetails(entry.details)) {
388
- throw new Error("Malformed persisted Codex context-window message");
389
- }
390
- if (matchesSession(entry.details.sessionId, sessionId)) return entry.id;
391
- }
392
- return undefined;
393
- }
394
-
395
- function couldBelongToSession(details: unknown, sessionId: string | undefined): boolean {
396
- if (sessionId === undefined || !isRecord(details)) return true;
397
- const markerSessionId = details.sessionId;
398
- return !isNonEmptyString(markerSessionId) || markerSessionId === sessionId;
399
- }
400
-
401
- function matchesSession(markerSessionId: string | undefined, sessionId: string | undefined): boolean {
402
- return sessionId === undefined || markerSessionId === sessionId;
403
- }
404
-
405
- function projectEncryptedToolResults(messages: readonly AgentMessage[]): AgentMessage[] {
406
- let changed = false;
407
- const projected = messages.map((message) => {
408
- if (message.role !== "toolResult" || !isRecord(message.details)) return message;
409
- const historyNotes = message.details.codexHistoryNotes;
410
- if (!isRecord(historyNotes) || typeof historyNotes.encrypted_output !== "string") return message;
411
- changed = true;
412
- return {
413
- ...message,
414
- content: [
415
- { type: "text" as const, text: encodeEncryptedOutputForContext(historyNotes.encrypted_output) },
416
- ...message.content.filter((item) => item.type === "image"),
417
- ],
418
- };
419
- });
420
- return changed ? projected : [...messages];
421
- }
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
+
62
+ constructor(loadThreadHint?: ThreadHintLoader) {
63
+ this.loadThreadHint = loadThreadHint ?? ((ctx, signal) => loadHistoryNotesThreadHint(ctx, signal));
64
+ }
65
+
66
+ reset(): void {
67
+ this.identity = undefined;
68
+ this.sessionId = undefined;
69
+ this.restoredMarkerId = undefined;
70
+ this.budget.reset();
71
+ this.rolloverPending = false;
72
+ this.trimPendingWindowId = undefined;
73
+ }
74
+
75
+ currentIdentity(): ContextWindowIdentity | undefined {
76
+ return this.identity ? { ...this.identity } : undefined;
77
+ }
78
+
79
+ restore(entries: readonly SessionEntry[], sessionId?: string): void {
80
+ this.reset();
81
+ this.sessionId = sessionId;
82
+ for (const entry of entries) {
83
+ if (entry.type === "compaction") {
84
+ this.recordCompaction(entry.details);
85
+ continue;
86
+ }
87
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
88
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
89
+ if (!isCodexContextManagementMessageDetails(entry.details)) {
90
+ throw new Error("Malformed persisted Codex context-window message");
91
+ }
92
+ if (!matchesSession(entry.details.sessionId, sessionId)) continue;
93
+ const details = entry.details.contextManagement;
94
+ this.restoredMarkerId = entry.id;
95
+ if (details.kind === "window") {
96
+ this.identity = identityFromDetails(entry.details);
97
+ this.trimPendingWindowId = details.trimPreviousWindow ? details.currentWindowId : undefined;
98
+ }
99
+ this.budget.restore(details.kind, details.currentWindowId);
100
+ }
101
+ }
102
+
103
+ /** Rebuild state when Pi navigates to a different session/branch. */
104
+ synchronize(ctx: Pick<ExtensionContext, "sessionManager">): void {
105
+ const entries = ctx.sessionManager.getBranch();
106
+ const sessionId = ctx.sessionManager.getSessionId();
107
+ const latestMarkerId = findLatestContextMarkerId(entries, sessionId);
108
+ if (this.sessionId !== sessionId || this.restoredMarkerId !== latestMarkerId) {
109
+ this.restore(entries, sessionId);
110
+ }
111
+ }
112
+
113
+ ensureInitialized(pi: ExtensionAPI, ctx: ExtensionContext, active: boolean): void {
114
+ if (!active) return;
115
+ this.restore(ctx.sessionManager.getBranch(), ctx.sessionManager.getSessionId());
116
+ if (this.identity) return;
117
+ const windowId = randomUUID();
118
+ this.sendWindowMessage(
119
+ pi,
120
+ ctx,
121
+ { firstWindowId: windowId, currentWindowId: windowId, windowNumber: 0 },
122
+ { triggerTurn: false, trimPreviousWindow: false },
123
+ );
124
+ }
125
+
126
+ project(
127
+ messages: readonly AgentMessage[],
128
+ mode: "off" | "remote",
129
+ ): AgentMessage[] {
130
+ if (mode === "off") {
131
+ return messages.filter(
132
+ (message) => message.role !== "custom" || message.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
133
+ );
134
+ }
135
+ let boundaryIndex = -1;
136
+ for (let index = 0; index < messages.length; index += 1) {
137
+ const message = messages[index]!;
138
+ if (
139
+ message.role === "custom" &&
140
+ message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE
141
+ ) {
142
+ if (!couldBelongToSession(message.details, this.sessionId)) continue;
143
+ if (!isCodexContextManagementMessageDetails(message.details)) {
144
+ throw new Error("Malformed persisted Codex context-window message");
145
+ }
146
+ if (!matchesSession(message.details.sessionId, this.sessionId)) continue;
147
+ }
148
+ if (!isContextWindowBoundary(message)) continue;
149
+ boundaryIndex = index;
150
+ this.identity = identityFromDetails(message.details);
151
+ }
152
+ if (boundaryIndex < 0) {
153
+ this.identity = undefined;
154
+ this.restoredMarkerId = undefined;
155
+ this.budget.reset();
156
+ this.trimPendingWindowId = undefined;
157
+ }
158
+ this.rolloverPending = false;
159
+ const projected = boundaryIndex < 0 ? [...messages] : [...messages.slice(boundaryIndex)];
160
+ return mode === "remote" ? projectEncryptedToolResults(projected) : projected;
161
+ }
162
+
163
+ async startNewWindow(
164
+ pi: ExtensionAPI,
165
+ ctx: ExtensionContext,
166
+ options: StartContextWindowOptions,
167
+ ): Promise<boolean> {
168
+ this.synchronize(ctx);
169
+ if (this.rolloverPending) return false;
170
+ if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
171
+ this.rolloverPending = true;
172
+ try {
173
+ const current = this.identity;
174
+ let threadHint: string | undefined;
175
+ if (current) {
176
+ try {
177
+ threadHint = await this.loadThreadHint(ctx, options.signal);
178
+ } catch (error) {
179
+ if (options.signal?.aborted) throw error;
180
+ }
181
+ }
182
+ if (options.signal?.aborted) throw new Error("Remote context rollover was aborted");
183
+ const currentWindowId = randomUUID();
184
+ const next: ContextWindowIdentity = current
185
+ ? {
186
+ firstWindowId: current.firstWindowId,
187
+ currentWindowId,
188
+ previousWindowId: current.currentWindowId,
189
+ windowNumber: current.windowNumber + 1,
190
+ }
191
+ : { firstWindowId: currentWindowId, currentWindowId, windowNumber: 0 };
192
+ this.sendWindowMessage(pi, ctx, next, options, threadHint);
193
+ return true;
194
+ } catch (error) {
195
+ this.rolloverPending = false;
196
+ throw error;
197
+ }
198
+ }
199
+
200
+ recordBudget(
201
+ pi: ExtensionAPI,
202
+ ctx: ExtensionContext,
203
+ active: boolean,
204
+ contextReminderThresholdPercent: number,
205
+ contextTokens?: number,
206
+ ): void {
207
+ if (!active || !this.identity || contextReminderThresholdPercent <= 0) return;
208
+ // Right after a rollover, Pi's usage anchor still reports the previous
209
+ // window's last request until the new window's first request completes.
210
+ // Budget decisions taken in that gap act on stale numbers: they burn the
211
+ // once-per-window reminder on a false alarm seconds after a successful
212
+ // rollover, leaving the window silent for the rest of its life.
213
+ if (!hasAssistantUsageSinceWindowBoundary(ctx.sessionManager.getBranch(), this.sessionId)) 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
+ // Only the compaction that consumes a scheduled rollover may write a
240
+ // boundary. Every other path — threshold, manual /compact, overflow —
241
+ // stays cancelled: a boundary written without a pending trim does not
242
+ // shrink anything, becomes Pi's latest-compaction anchor, and blinds
243
+ // getContextUsage (null tokens) until the next assistant usage lands —
244
+ // which is exactly how the exhausted-window fallback can go silent.
245
+ const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
246
+ if (!boundary || boundary.details.contextManagement.currentWindowId !== this.trimPendingWindowId) {
247
+ return { cancel: true };
248
+ }
249
+ const compaction = this.createCompaction(event);
250
+ // Consume the trim synchronously. Pi does not re-fire session_compact
251
+ // for hook-written entries, so relying on that event leaks the pending
252
+ // id and lets every later compaction pass this gate again, writing an
253
+ // endless run of no-op boundaries for the same window.
254
+ this.trimPendingWindowId = undefined;
255
+ return { compaction };
256
+ }
257
+
258
+ recordCompaction(details: unknown): void {
259
+ if (!isContextWindowCompactionDetails(details)) return;
260
+ if (details.windowId === this.trimPendingWindowId) this.trimPendingWindowId = undefined;
261
+ }
262
+
263
+ createCompaction(event: SessionBeforeCompactEvent): CompactionResult<ContextWindowCompactionDetails> {
264
+ const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
265
+ return {
266
+ summary: CONTEXT_WINDOW_COMPACTION_SUMMARY,
267
+ firstKeptEntryId: boundary?.id ?? event.preparation.firstKeptEntryId,
268
+ tokensBefore: event.preparation.tokensBefore,
269
+ details: {
270
+ protocol: CONTEXT_MANAGEMENT_PROTOCOL,
271
+ strategy: CONTEXT_WINDOW_COMPACTION_STRATEGY,
272
+ ...(this.identity ? { windowId: this.identity.currentWindowId } : {}),
273
+ },
274
+ };
275
+ }
276
+
277
+ rewritePayload(payload: unknown, ctx: ExtensionContext): unknown {
278
+ const withMetadata = rewriteWindowPayload(payload, ctx, this.identity);
279
+ return rewriteContextNamespaceTools(withMetadata, { encrypted: true });
280
+ }
281
+
282
+ rewriteHeaders(headers: ProviderHeaders, ctx: ExtensionContext): void {
283
+ rewriteWindowHeaders(headers, ctx, this.identity);
284
+ }
285
+
286
+ private sendWindowMessage(
287
+ pi: ExtensionAPI,
288
+ ctx: ExtensionContext,
289
+ identity: ContextWindowIdentity,
290
+ options: StartContextWindowOptions,
291
+ threadHint?: string,
292
+ ): void {
293
+ sendContextWindowMessage(
294
+ pi,
295
+ renderContextWindowMessage(identity, threadHint),
296
+ "window",
297
+ identity,
298
+ { triggerTurn: options.triggerTurn, sessionId: ctx.sessionManager.getSessionId() },
299
+ options.trimPreviousWindow,
300
+ );
301
+ this.identity = identity;
302
+ this.sessionId = ctx.sessionManager.getSessionId();
303
+ this.restoredMarkerId = undefined;
304
+ this.trimPendingWindowId = options.trimPreviousWindow ? identity.currentWindowId : undefined;
305
+ // sendMessage has accepted the marker synchronously; clear only the
306
+ // in-flight guard so a later turn can roll over again.
307
+ this.rolloverPending = false;
308
+ }
309
+ }
310
+
311
+ function identityFromDetails(details: CodexContextManagementMessageDetails): ContextWindowIdentity {
312
+ const context = details.contextManagement;
313
+ return {
314
+ firstWindowId: context.firstWindowId,
315
+ currentWindowId: context.currentWindowId,
316
+ ...(context.previousWindowId ? { previousWindowId: context.previousWindowId } : {}),
317
+ windowNumber: context.windowNumber,
318
+ };
319
+ }
320
+
321
+ const NOTES_CHECKPOINT_ACTIONS: ReadonlySet<string> = new Set(["append_to_file", "write_file"]);
322
+
323
+ /**
324
+ * Whether the branch contains a successful assistant usage after the latest
325
+ * window boundary. Before that first usage lands, any context measurement is
326
+ * still anchored to the previous window's last request.
327
+ */
328
+ export function hasAssistantUsageSinceWindowBoundary(
329
+ entries: readonly SessionEntry[],
330
+ sessionId?: string,
331
+ ): boolean {
332
+ let boundaryIndex = -1;
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
+ }
346
+ if (boundaryIndex < 0) return false;
347
+ for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
348
+ const entry = entries[index]!;
349
+ if (entry.type !== "message") continue;
350
+ const message = entry.message;
351
+ if (message.role !== "assistant") continue;
352
+ const candidate = message as unknown as {
353
+ stopReason?: string;
354
+ usage?: { totalTokens?: number; input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
355
+ };
356
+ if (candidate.stopReason === "aborted" || candidate.stopReason === "error") continue;
357
+ const usage = candidate.usage;
358
+ if (!usage) continue;
359
+ const tokens = usage.totalTokens ?? (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
360
+ if (tokens > 0) return true;
361
+ }
362
+ return false;
363
+ }
364
+
365
+ /**
366
+ * Whether the branch contains a successful notes append/write result after the
367
+ * latest window boundary. Reads and failed writes never count as checkpoints.
368
+ */
369
+ export function findNotesCheckpointSinceBoundary(
370
+ entries: readonly SessionEntry[],
371
+ sessionId?: string,
372
+ ): boolean {
373
+ let boundaryIndex = -1;
374
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
375
+ const entry = entries[index]!;
376
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
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>();
389
+ for (let index = boundaryIndex + 1; index < entries.length; index += 1) {
390
+ const entry = entries[index]!;
391
+ if (entry.type !== "message") continue;
392
+ const message = entry.message;
393
+ if (message.role === "assistant") {
394
+ const parts = Array.isArray(message.content) ? message.content : [];
395
+ 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;
400
+ const args = isRecord(part.arguments) ? part.arguments : undefined;
401
+ const action = typeof args?.action === "string" ? args.action : "";
402
+ if (typeof part.id === "string" && NOTES_CHECKPOINT_ACTIONS.has(action)) {
403
+ checkpointCalls.add(part.id);
404
+ }
405
+ }
406
+ continue;
407
+ }
408
+ if (message.role === "toolResult" && checkpointCalls.has(message.toolCallId)) {
409
+ if (message.isError) {
410
+ checkpointCalls.delete(message.toolCallId);
411
+ continue;
412
+ }
413
+ const details = message.details;
414
+ if (isRecord(details) && isRecord(details.codexHistoryNotes)) return true;
415
+ }
416
+ }
417
+ return false;
418
+ }
419
+
420
+ export function findLatestWindowBoundaryEntry(
421
+ entries: readonly SessionEntry[],
422
+ sessionId?: string,
423
+ ): WindowBoundaryEntry | undefined {
424
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
425
+ const entry = entries[index]!;
426
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
427
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
428
+ if (!isCodexContextManagementMessageDetails(entry.details)) {
429
+ throw new Error("Malformed persisted Codex context-window message");
430
+ }
431
+ if (!matchesSession(entry.details.sessionId, sessionId)) continue;
432
+ if (entry.details.contextManagement.kind === "window") return entry as WindowBoundaryEntry;
433
+ }
434
+ return undefined;
435
+ }
436
+
437
+ function findLatestContextMarkerId(
438
+ entries: readonly SessionEntry[],
439
+ sessionId?: string,
440
+ ): string | undefined {
441
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
442
+ const entry = entries[index]!;
443
+ if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
444
+ if (!couldBelongToSession(entry.details, sessionId)) continue;
445
+ if (!isCodexContextManagementMessageDetails(entry.details)) {
446
+ throw new Error("Malformed persisted Codex context-window message");
447
+ }
448
+ if (matchesSession(entry.details.sessionId, sessionId)) return entry.id;
449
+ }
450
+ return undefined;
451
+ }
452
+
453
+ function couldBelongToSession(details: unknown, sessionId: string | undefined): boolean {
454
+ if (sessionId === undefined || !isRecord(details)) return true;
455
+ const markerSessionId = details.sessionId;
456
+ return !isNonEmptyString(markerSessionId) || markerSessionId === sessionId;
457
+ }
458
+
459
+ function matchesSession(markerSessionId: string | undefined, sessionId: string | undefined): boolean {
460
+ return sessionId === undefined || markerSessionId === sessionId;
461
+ }
462
+
463
+ function projectEncryptedToolResults(messages: readonly AgentMessage[]): AgentMessage[] {
464
+ let changed = false;
465
+ const projected = messages.map((message) => {
466
+ if (message.role !== "toolResult" || !isRecord(message.details)) return message;
467
+ const historyNotes = message.details.codexHistoryNotes;
468
+ if (!isRecord(historyNotes) || typeof historyNotes.encrypted_output !== "string") return message;
469
+ changed = true;
470
+ return {
471
+ ...message,
472
+ content: [
473
+ { type: "text" as const, text: encodeEncryptedOutputForContext(historyNotes.encrypted_output) },
474
+ ...message.content.filter((item) => item.type === "image"),
475
+ ],
476
+ };
477
+ });
478
+ return changed ? projected : [...messages];
479
+ }