pi-openai-toolkit 0.20.6 → 0.20.7

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/docs/internals.md CHANGED
@@ -73,11 +73,12 @@ sequenceDiagram
73
73
  Model->>Toolkit: new_context
74
74
  Toolkit->>Toolkit: verify successful notes write in this window
75
75
  Toolkit-->>Model: checkpoint receipt with the successful note path
76
- Toolkit-->>Pi: window marker appended, previous window trim scheduled
76
+ Toolkit-->>Pi: new window marker persisted
77
77
  Note over Model: Context switch is complete; read the receipt once, then resume the active task
78
78
  Model->>Backend: next request with the new x-codex-window-id
79
79
  Pi->>Toolkit: session_before_compact
80
- Toolkit-->>Pi: no-summary boundary consuming the scheduled trim
80
+ Toolkit->>Toolkit: resolve boundary against Pi's effective context
81
+ Toolkit-->>Pi: safe no-summary boundary, or cancel obsolete cleanup
81
82
  ```
82
83
 
83
84
  State invariants the lifecycle code must keep honest:
@@ -90,12 +91,12 @@ State invariants the lifecycle code must keep honest:
90
91
  - When even that rebuild would lose declared tools, the trim is refused and the full message list is sent: a window that still carries the previous turns is usable, a window with no tools silently degrades into a chat the model can only narrate. Each refusal is reported once per window and lost-tool set through `takeProjectionDiagnostics()`, which writes a `context.projection.tool_loadout_shrink` compaction artifact and warns in the UI.
91
92
  - The manager also remembers the head it last anchored. When a boundary compaction retires the transcript entry that declared the tools, the branch itself no longer contains a system message, and the remembered head is re-anchored instead of sending a tool-less window. The cache is request-time only, refreshed by every projection, and cleared by `reset()`.
92
93
  - `new_context` has no force escape hatch: a persisted successful notes checkpoint in the current window is always required before rollover. The model must wait for the notes result to be persisted before retrying.
93
- - After a marker is accepted for sending, the manager keeps a session/window-anchored pending guard until that exact target marker is persisted or the session changes. A duplicate `new_context` during this persistence gap returns `started: false` and sends no second marker; projected/in-memory messages do not retire the guard.
94
+ - After a marker is accepted for sending, the manager keeps a session/window-anchored pending guard until that exact target marker is persisted or the session changes. A duplicate `new_context` during this persistence gap returns `started: false` and sends no second marker; projected/in-memory messages do not retire the guard. The exact queued target can already trim the current Remote request and select its metadata, but this preview is recomputed per request and never authorizes maintenance compaction before persistence.
94
95
  - Budget checks are skipped until the current window has produced its own assistant usage. Acting on the previous window's usage anchor would burn the once-per-window reminder on a false alarm.
95
- - Preparing a boundary never consumes its scheduled trim. The manager reconciles the latest persisted compaction and branch state, so only a durable matching boundary consumes it. Missing/late callbacks, aborted preparation and navigation across branches sharing a marker remain retryable. Automatic threshold/overflow and toolkit-owned manual maintenance only trim a matching scheduled window; a user manual `/compact` follows the managed handoff below.
96
- - A window boundary is persisted as a `codex-context-window` custom message. `session_start` replays boundaries from the branch and rebuilds identity after forks.
97
- - Retiring windows remains a projection-level operation in the persisted branch, but a non-window consumer never receives that retired history. `context_with_system` trims to the latest persisted Remote Context boundary, preserves the current system/tool head, and removes the internal window marker before the local provider request. The legacy `leaveManagedMode` setting is retained only so old config files remain valid; it no longer warns or starts a bulk close-out from `model_select` or `agent_settled`.
98
- - If a non-window consumer reaches `session_before_compact` while a Remote Context boundary exists, the extension returns a no-summary compaction anchored at that boundary. No Remote V2/native compaction producer receives the Remote window, Pi retains the marker and current-window tail, and switching back to a supported model reuses the persisted window identity. A session with no Remote boundary keeps the ordinary compaction path.
96
+ - Pi's canonical session projection is the source of effective history. Toolkit may further restrict that projection, but never reconstruct omitted or summarized messages from the raw branch. Boundary resolution uses projected source entries and the active branch's latest compaction; an older marker retained inside a newer compaction does not authorize dropping that compaction's summary.
97
+ - Window identity and cleanup eligibility are separate. A persisted `codex-context-window` message restores identity after reload/fork, but its historical `trimPreviousWindow` flag is not an outstanding command. A newer native summary or opaque checkpoint supersedes old cleanup without erasing window identity. Missing or edited boundary evidence is never repaired by loading raw history.
98
+ - Maintenance compaction and local-consumer detachment use the same effective-boundary rules. Only a safe current boundary can produce a no-summary compaction; obsolete cleanup must not move the retained start backwards or replace a newer summary/checkpoint. Preparation alone changes no durable state, and cancellation, retry and branch navigation are resolved from current evidence rather than callback acknowledgments. User manual `/compact` still follows the managed handoff below.
99
+ - A non-window consumer receives only the effective current context, with the system/tool head preserved and internal window markers removed. When detachment is needed, no compaction producer receives retired Remote history. Once a later ordinary compaction owns effective context, the historical window does not suppress its summary or valid opaque replay. A genuinely new window can retire that checkpoint. Disabling opaque checkpoint generation through `responsesApis` does not disable required replay of an inherited checkpoint; an unsupported transport, authentication failure, or endpoint mismatch aborts the request instead of exposing its placeholder. The legacy `leaveManagedMode` setting remains compatible but does not start a bulk close-out from `model_select` or `agent_settled`.
99
100
  - The configured native-fallback summary model is still checked against the physical request size (`estimateSummarizationRequest`). The guard remains a provider-safety invariant; the Remote-window fix avoids the oversized request by excluding managed history before any compaction producer can be called, rather than removing the guard or chunking the request.
100
101
 
101
102
  ---
@@ -120,7 +121,7 @@ The underlying native compact promise still rejects its intentional cancellation
120
121
 
121
122
  ### Remote Compaction v2 wire contract
122
123
 
123
- v2 is what an uncovered session gets. Any model that Remote Context declines, such as a gateway without an exact transport opt-in, can still compact through v2 when its API appears in `context.remoteCompaction.apis`. The two strategies never compete for one session.
124
+ v2 is what an uncovered session gets. Any model that Remote Context declines, such as a gateway without an exact transport opt-in, can still compact through v2 when its API appears in `context.remoteCompaction.apis`. Compaction generation follows the active strategy. Across mode changes, an already-persisted checkpoint remains authoritative until a safe newer window or compaction replaces it; a historical window identity alone must not suppress checkpoint replay.
124
125
 
125
126
  A `compaction_trigger` item appended to the live streaming request yields one output item of `type: "compaction"` with non-empty `encrypted_content`, stored in `CompactionEntry.details.compactedWindow`. On later requests the opaque checkpoint is replayed ahead of live turns, with no text summary. Replay fails closed: if the summary anchor cannot be located, the request is aborted with a notification and a content-free failure artifact. The sentinel-only payload is never sent.
126
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-toolkit",
3
- "version": "0.20.6",
3
+ "version": "0.20.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
3
  import type { ProviderHeaders } from "@earendil-works/pi-ai";
4
4
  import {
5
- buildSessionContext,
5
+ buildSessionProjection,
6
6
  calculateContextTokens,
7
7
  estimateTokens,
8
8
  type CompactionResult,
@@ -43,6 +43,7 @@ import {
43
43
  loadHistoryNotesThreadHint,
44
44
  } from "./history-notes";
45
45
  import { rewriteContextNamespaceTools } from "./namespace-tools";
46
+ import { hasInvalidatedCompactionContext } from "../payload-rewrite";
46
47
 
47
48
  interface StartContextWindowOptions {
48
49
  triggerTurn: boolean;
@@ -73,6 +74,8 @@ type WindowBoundaryEntry = Extract<SessionEntry, { type: "custom_message" }> & {
73
74
  type PendingRollover = {
74
75
  sessionId?: string;
75
76
  targetWindowId: string;
77
+ /** The current request already contains this marker; never persistence proof. */
78
+ projected?: boolean;
76
79
  };
77
80
 
78
81
  export class CodexContextWindowManager {
@@ -80,10 +83,9 @@ export class CodexContextWindowManager {
80
83
  private sessionId: string | undefined;
81
84
  private restoredMarkerId: string | undefined;
82
85
  private restoredCompactionId: string | undefined;
83
- private branchStateInvalidated = false;
86
+ private boundary: WindowBoundaryResolution = { kind: "none" };
84
87
  private readonly budget = new ContextWindowBudget();
85
88
  private pendingRollover: PendingRollover | undefined;
86
- private trimPendingWindowId: string | undefined;
87
89
  private readonly projectionDiagnostics = new Set<string>();
88
90
  private lastKnownSystemHead: AgentMessage | undefined;
89
91
  /**
@@ -108,9 +110,21 @@ export class CodexContextWindowManager {
108
110
  currentIdentity(): ContextWindowIdentity | undefined {
109
111
  return this.identity ? { ...this.identity } : undefined;
110
112
  }
111
- /** Whether a rollover trim awaits a matching persisted compaction. */
113
+ /** Derived eligibility, never a replayed marker command. */
112
114
  hasPendingTrim(): boolean {
113
- return this.trimPendingWindowId !== undefined;
115
+ return this.boundary.kind === "effective" && !this.boundary.cleaned &&
116
+ this.boundary.entry.details.contextManagement.trimPreviousWindow === true;
117
+ }
118
+
119
+ /** Historical identity does not own a newer native/opaque checkpoint. */
120
+ hasEffectiveWindow(): boolean {
121
+ if (!this.identity || this.boundary.kind === "ambiguous") return false;
122
+ return this.boundary.kind !== "superseded" ||
123
+ (this.pendingRollover?.projected === true && this.pendingRollover.targetWindowId === this.identity.currentWindowId);
124
+ }
125
+
126
+ hasSupersededWindow(): boolean {
127
+ return this.boundary.kind === "superseded";
114
128
  }
115
129
 
116
130
  private resetWindowState(): void {
@@ -118,9 +132,8 @@ export class CodexContextWindowManager {
118
132
  this.sessionId = undefined;
119
133
  this.restoredMarkerId = undefined;
120
134
  this.restoredCompactionId = undefined;
121
- this.branchStateInvalidated = false;
135
+ this.boundary = { kind: "none" };
122
136
  this.budget.reset();
123
- this.trimPendingWindowId = undefined;
124
137
  this.projectedContextTokens = undefined;
125
138
  }
126
139
 
@@ -132,7 +145,6 @@ export class CodexContextWindowManager {
132
145
  for (const entry of entries) {
133
146
  if (entry.type === "compaction") {
134
147
  this.restoredCompactionId = entry.id;
135
- this.recordCompaction(entry.details);
136
148
  continue;
137
149
  }
138
150
  if (entry.type !== "custom_message" || entry.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE) continue;
@@ -145,13 +157,10 @@ export class CodexContextWindowManager {
145
157
  this.restoredMarkerId = entry.id;
146
158
  if (details.kind === "window") {
147
159
  this.identity = identityFromDetails(entry.details);
148
- this.trimPendingWindowId = details.trimPreviousWindow ? details.currentWindowId : undefined;
149
160
  }
150
161
  this.budget.restore(details.kind, details.currentWindowId);
151
162
  }
152
- // recordCompaction may invalidate the cache while replaying entries; the
153
- // completed replay already reconciled those acknowledgments with this branch.
154
- this.branchStateInvalidated = false;
163
+ this.boundary = resolveWindowBoundary(entries, sessionId);
155
164
  this.retireSatisfiedOrStalePending(entries, sessionId);
156
165
  }
157
166
 
@@ -170,9 +179,11 @@ export class CodexContextWindowManager {
170
179
  }
171
180
  // Hook-provided compactions may have no success callback. The persisted
172
181
  // branch also detects navigation before/after a commit with the same marker.
173
- if (this.branchStateInvalidated || this.sessionId !== sessionId || this.restoredMarkerId !== latestMarkerId || this.restoredCompactionId !== latestCompactionId) {
182
+ if (this.sessionId !== sessionId || this.restoredMarkerId !== latestMarkerId || this.restoredCompactionId !== latestCompactionId) {
174
183
  this.restore(entries, sessionId);
175
184
  }
185
+ // Edits and sibling navigation may leave marker/compaction IDs unchanged.
186
+ this.boundary = resolveWindowBoundary(entries, sessionId);
176
187
  }
177
188
 
178
189
  /**
@@ -211,13 +222,22 @@ export class CodexContextWindowManager {
211
222
  project(
212
223
  messages: readonly AgentMessage[],
213
224
  mode: "off" | "local" | "remote",
225
+ /** Delay encoding until verified retained copies have been removed. */
226
+ encodeEncryptedOutputs = true,
214
227
  ): AgentMessage[] {
228
+ // Recompute the request-only preview on every projection. Observing a
229
+ // queued marker must not authorize maintenance or retire the duplicate guard.
230
+ if (this.pendingRollover) this.pendingRollover.projected = false;
215
231
  if (mode === "off") {
216
232
  this.projectedContextTokens = undefined;
217
233
  return messages.filter(
218
234
  (message) => message.role !== "custom" || message.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
219
235
  );
220
236
  }
237
+ if (this.boundary.kind === "ambiguous") {
238
+ throw new Error("Persisted Codex context-window boundary missing or edited in canonical projection");
239
+ }
240
+ if (this.boundary.kind === "superseded") this.identity = identityFromDetails(this.boundary.entry.details);
221
241
  let boundaryIndex = -1;
222
242
  for (let index = 0; index < messages.length; index += 1) {
223
243
  const message = messages[index]!;
@@ -232,28 +252,24 @@ export class CodexContextWindowManager {
232
252
  if (!matchesSession(message.details.sessionId, this.sessionId)) continue;
233
253
  }
234
254
  if (!isContextWindowBoundary(message)) continue;
255
+ const queuedRollover = mode === "remote" && this.pendingRollover?.targetWindowId === message.details.contextManagement.currentWindowId;
256
+ if (this.boundary.kind === "superseded" && !queuedRollover) continue;
235
257
  // Local consumers follow the synchronized persisted identity, not a
236
258
  // queued marker from the request view. They never own the Remote session.
237
259
  if (mode === "local" && message.details.contextManagement.currentWindowId !== this.identity?.currentWindowId) continue;
238
260
  boundaryIndex = index;
239
- if (mode === "remote") this.identity = identityFromDetails(message.details);
240
- }
241
- if (boundaryIndex < 0) {
242
- if (mode === "local" && this.identity) {
243
- throw new Error("Persisted Codex context-window boundary missing from local projection");
244
- }
245
261
  if (mode === "remote") {
246
- this.identity = undefined;
247
- this.restoredMarkerId = undefined;
248
- this.budget.reset();
249
- this.trimPendingWindowId = undefined;
262
+ this.identity = identityFromDetails(message.details);
263
+ if (this.pendingRollover) this.pendingRollover.projected = queuedRollover;
250
264
  }
251
- this.projectedContextTokens = undefined;
265
+ }
266
+ if (boundaryIndex < 0 && this.boundary.kind === "effective") {
267
+ throw new Error("Persisted Codex context-window boundary missing from effective projection");
252
268
  }
253
269
  // Projection observes the request view, never durable session state. A
254
270
  // queued rollover marker must not clear the duplicate guard here.
255
271
  const trimmed = boundaryIndex < 0 ? [...messages] : this.trimToWindow(messages, boundaryIndex, mode === "local");
256
- const projected = mode === "remote" ? projectEncryptedToolResults(trimmed) : trimmed.filter(
272
+ const projected = mode === "remote" ? (encodeEncryptedOutputs ? projectEncryptedToolResults(trimmed) : trimmed) : trimmed.filter(
257
273
  (message) => message.role !== "custom" || message.customType !== CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
258
274
  );
259
275
  this.projectedContextTokens = mode === "remote" && this.identity
@@ -421,21 +437,12 @@ export class CodexContextWindowManager {
421
437
  prepareCompaction(
422
438
  event: SessionBeforeCompactEvent,
423
439
  ): { cancel: true } | { compaction: CompactionResult<ContextWindowCompactionDetails> } {
424
- // Only the compaction that consumes a scheduled rollover may write a
425
- // boundary. Every other path — threshold, manual /compact, overflow —
426
- // stays cancelled: a boundary written without a pending trim does not
427
- // shrink anything, becomes Pi's latest-compaction anchor, and blinds
428
- // getContextUsage (null tokens) until the next assistant usage lands —
429
- // which is exactly how the exhausted-window fallback can go silent.
430
- const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
431
- if (!boundary || boundary.details.contextManagement.currentWindowId !== this.trimPendingWindowId) {
440
+ const boundary = resolveWindowBoundary(event.branchEntries, this.sessionId);
441
+ if (boundary.kind !== "effective" || boundary.cleaned || !boundary.entry.details.contextManagement.trimPreviousWindow) {
432
442
  return { cancel: true };
433
443
  }
434
- const compaction = this.createCompaction(event);
435
- // Preparation is only a proposal: Pi may abort before appending it.
436
- // synchronize() consumes the trim after observing a durable compaction;
437
- // recordCompaction() also handles hosts that emit a success callback.
438
- return { compaction };
444
+ // An unpersisted/cancelled proposal changes no eligibility.
445
+ return { compaction: this.createCompaction(event, boundary.entry) };
439
446
  }
440
447
 
441
448
  /**
@@ -445,10 +452,12 @@ export class CodexContextWindowManager {
445
452
  */
446
453
  prepareDetachedCompaction(
447
454
  event: SessionBeforeCompactEvent,
448
- ): { compaction: CompactionResult<ContextWindowCompactionDetails> } | undefined {
449
- const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
450
- if (!boundary) return undefined;
451
- const compaction = this.createCompaction(event);
455
+ ): { cancel: true } | { compaction: CompactionResult<ContextWindowCompactionDetails> } | undefined {
456
+ const boundary = resolveWindowBoundary(event.branchEntries, this.sessionId);
457
+ if (boundary.kind === "ambiguous") throw new Error("Persisted Codex context-window boundary missing or edited");
458
+ if (boundary.kind !== "effective") return undefined;
459
+ if (boundary.cleaned) return { cancel: true };
460
+ const compaction = this.createCompaction(event, boundary.entry);
452
461
  // Pi's preparation/tokensBefore describes the durable branch, which includes
453
462
  // Remote-managed history. Detached local accounting must use the same current
454
463
  // window projection that the non-window provider sees instead.
@@ -461,30 +470,19 @@ export class CodexContextWindowManager {
461
470
  }
462
471
 
463
472
  private estimateLocalContextTokens(entries: readonly SessionEntry[]): number {
464
- const messages = buildSessionContext(entries as never[]).messages as unknown as AgentMessage[];
473
+ const messages = buildSessionProjection([...entries]).messages;
465
474
  return this.project(messages, "local").reduce((total, message) => total + estimateTokens(message as never), 0);
466
475
  }
467
476
 
468
- recordCompaction(details: unknown): void {
469
- if (!isContextWindowCompactionDetails(details)) return;
470
- if (this.trimPendingWindowId !== undefined && details.windowId === this.trimPendingWindowId) {
471
- this.trimPendingWindowId = undefined;
472
- // The callback supplies no branch identity. Force durable reconciliation
473
- // even if navigation returns to the cached pre-commit branch before sync.
474
- this.branchStateInvalidated = true;
475
- }
476
- }
477
-
478
- createCompaction(event: SessionBeforeCompactEvent): CompactionResult<ContextWindowCompactionDetails> {
479
- const boundary = findLatestWindowBoundaryEntry(event.branchEntries, this.sessionId);
477
+ private createCompaction(event: SessionBeforeCompactEvent, boundary: WindowBoundaryEntry): CompactionResult<ContextWindowCompactionDetails> {
480
478
  return {
481
479
  summary: CONTEXT_WINDOW_COMPACTION_SUMMARY,
482
- firstKeptEntryId: boundary?.id ?? event.preparation.firstKeptEntryId,
480
+ firstKeptEntryId: boundary.id,
483
481
  tokensBefore: event.preparation.tokensBefore,
484
482
  details: {
485
483
  protocol: CONTEXT_MANAGEMENT_PROTOCOL,
486
484
  strategy: CONTEXT_WINDOW_COMPACTION_STRATEGY,
487
- ...(this.identity ? { windowId: this.identity.currentWindowId } : {}),
485
+ windowId: boundary.details.contextManagement.currentWindowId,
488
486
  },
489
487
  };
490
488
  }
@@ -517,13 +515,41 @@ export class CodexContextWindowManager {
517
515
  this.identity = identity;
518
516
  this.sessionId = ctx.sessionManager.getSessionId();
519
517
  this.restoredMarkerId = undefined;
520
- this.trimPendingWindowId = options.trimPreviousWindow ? identity.currentWindowId : undefined;
521
518
  // sendMessage() acceptance is not persistence. The pending rollover stays
522
519
  // armed until synchronize() observes this target window in the persisted
523
520
  // branch (or the session changes).
524
521
  }
525
522
  }
526
523
 
524
+ type WindowBoundaryResolution =
525
+ | { kind: "none" | "ambiguous" }
526
+ | { kind: "superseded"; entry: WindowBoundaryEntry }
527
+ | { kind: "effective"; entry: WindowBoundaryEntry; cleaned: boolean };
528
+
529
+ /** Pi's projection owns context; raw chronology only establishes window ownership. */
530
+ function resolveWindowBoundary(entries: readonly SessionEntry[], sessionId?: string): WindowBoundaryResolution {
531
+ const boundary = findLatestWindowBoundaryEntry(entries, sessionId);
532
+ if (!boundary) return { kind: "none" };
533
+ const projection = buildSessionProjection([...entries]);
534
+ const latest = projection.entries.find((entry) => entry.sourceEntry.type === "compaction")?.sourceEntry;
535
+ const boundaryIndex = entries.findIndex((entry) => entry.id === boundary.id);
536
+ const laterCompaction = latest?.type === "compaction" && entries.findIndex((entry) => entry.id === latest.id) > boundaryIndex
537
+ ? latest : undefined;
538
+ if (laterCompaction && hasInvalidatedCompactionContext([...entries], laterCompaction.id)) return { kind: "ambiguous" };
539
+ const marker = projection.entries.find((entry) => entry.sourceEntry.id === boundary.id);
540
+ // A surviving edit can replace content without changing custom details.
541
+ const edited = entries.some((entry) => entry.type === "context_edit" && entry.targetId === boundary.id &&
542
+ projection.entries.some((projected) => projected.sourceEntry.id === entry.id));
543
+ if (edited) return { kind: "ambiguous" };
544
+ const cleaned = laterCompaction !== undefined &&
545
+ isContextWindowCompactionDetails(laterCompaction.details) &&
546
+ laterCompaction.details.windowId === boundary.details.contextManagement.currentWindowId &&
547
+ laterCompaction.firstKeptEntryId === boundary.id;
548
+ if (laterCompaction && !cleaned) return { kind: "superseded", entry: boundary };
549
+ if (!marker?.messages.some(isContextWindowBoundary)) return { kind: "ambiguous" };
550
+ return { kind: "effective", entry: boundary, cleaned };
551
+ }
552
+
527
553
  function identityFromDetails(details: CodexContextManagementMessageDetails): ContextWindowIdentity {
528
554
  const context = details.contextManagement;
529
555
  return {
@@ -957,6 +957,19 @@ async function handleSessionBeforeCompact(
957
957
  return undefined;
958
958
  }
959
959
 
960
+ function rejectInheritedCheckpointReplay(
961
+ ctx: ExtensionContext,
962
+ config: CompactionConfig,
963
+ compactionEntryId: string,
964
+ reason: string,
965
+ ): undefined {
966
+ writeReplayFailureArtifact({ reason, compactionEntryId }, config, ctx);
967
+ reportPiContextHookFailure(ctx, `replay-failed:${reason}`);
968
+ if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: inherited checkpoint replay failed (${reason}); request aborted`, "error");
969
+ ctx.abort();
970
+ return undefined;
971
+ }
972
+
960
973
  async function handleContextInternal(
961
974
  event: ContextWithSystemEvent,
962
975
  ctx: ExtensionContext,
@@ -977,6 +990,7 @@ async function handleContextInternal(
977
990
  }
978
991
 
979
992
  let localMessages: AgentMessage[] | undefined;
993
+ let remoteReplayProjection = false;
980
994
  if (config.contextManagement === "remote") {
981
995
  const contextModel = await resolveContextCapabilityModel(ctx);
982
996
  try {
@@ -994,7 +1008,11 @@ async function handleContextInternal(
994
1008
  // Measure the provider-visible current window, not Pi's durable transcript.
995
1009
  // The latter still contains retired remote windows and can be inflated by
996
1010
  // an upstream error even though this request only carries the live window.
997
- const projected = contextWindows.project(event.messages, "remote");
1011
+ let projected = contextWindows.project(event.messages, "remote", false);
1012
+ // Projection may observe the exact queued rollover before Pi persists
1013
+ // it. Decide replay ownership from this request, not the old branch.
1014
+ const ownsWindow = contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity();
1015
+ if (ownsWindow) projected = contextWindows.project(projected, "remote");
998
1016
  contextWindows.recordBudget(
999
1017
  pi,
1000
1018
  ctx,
@@ -1002,9 +1020,13 @@ async function handleContextInternal(
1002
1020
  config.contextReminderThresholdPercent,
1003
1021
  );
1004
1022
  reportProjectionDiagnostics(contextWindows, config, ctx);
1005
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1006
- ? undefined
1007
- : { messages: projected };
1023
+ if (ownsWindow) {
1024
+ return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1025
+ ? undefined
1026
+ : { messages: projected };
1027
+ }
1028
+ localMessages = projected;
1029
+ remoteReplayProjection = true;
1008
1030
  } catch (error) {
1009
1031
  notifyRemoteContextFailure(ctx, "malformed-window-state");
1010
1032
  reportPiContextHookFailure(ctx, "malformed-window-state");
@@ -1013,11 +1035,10 @@ async function handleContextInternal(
1013
1035
  }
1014
1036
  }
1015
1037
 
1016
- // An eligible Codex Remote session is never allowed to fall through to the
1017
- // older compaction-replay path. An explicit host exclusion is the one
1018
- // intentional downgrade: strip internal markers and let the ordinary request
1019
- // proceed without Remote Context affinity.
1020
- if (isCodexContextModel(contextModel, config)) {
1038
+ // An unavailable covered runtime must not silently become an ordinary
1039
+ // replay consumer. Explicit host exclusion remains the one intentional
1040
+ // downgrade; active Remote can replay a newer inherited checkpoint.
1041
+ if (!remoteActive && isCodexContextModel(contextModel, config)) {
1021
1042
  const registrationState = getContextToolRegistrationState();
1022
1043
  if (isExcludedGatewayContext(contextModel, config, registrationState)) {
1023
1044
  const visibleMessages = contextWindows.project(event.messages, "off");
@@ -1038,39 +1059,58 @@ async function handleContextInternal(
1038
1059
  : { messages: visibleMessages };
1039
1060
  }
1040
1061
 
1041
- try {
1042
- const projected = contextWindows.project(event.messages, "local");
1043
- localMessages = projected;
1044
- if (contextWindows.currentIdentity()) {
1045
- // This boundary owns earlier history, including any older Remote V2
1046
- // checkpoint. A local request must not replay that history back in.
1047
- return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1048
- ? undefined
1049
- : { messages: projected };
1062
+ if (!remoteActive) {
1063
+ try {
1064
+ const projected = contextWindows.project(event.messages, "local");
1065
+ localMessages = projected;
1066
+ if (contextWindows.hasEffectiveWindow()) {
1067
+ // This boundary owns earlier history, including any older Remote V2
1068
+ // checkpoint. A local request must not replay that history back in.
1069
+ return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1070
+ ? undefined
1071
+ : { messages: projected };
1072
+ }
1073
+ } catch (error) {
1074
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
1075
+ reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
1076
+ ctx.abort();
1077
+ return undefined;
1050
1078
  }
1051
- } catch (error) {
1052
- notifyRemoteContextFailure(ctx, "malformed-window-state");
1053
- reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
1054
- ctx.abort();
1055
- return undefined;
1056
1079
  }
1057
1080
  }
1058
1081
 
1059
- // With no Remote boundary, the ordinary checkpoint replay path remains the
1060
- // owner. Inactive covered Codex consumers were handled separately above.
1082
+ // Without an effective Remote boundary, the latest checkpoint owns replay.
1083
+ // This also covers a newer checkpoint superseding a historical window.
1061
1084
  const visibleMessages = localMessages ?? contextWindows.project(event.messages, "off");
1062
- const visibleResult = visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
1063
- ? undefined : { messages: visibleMessages };
1085
+ const finishProjection = (messages: AgentMessage[]) => {
1086
+ // Compare canonical retained copies before Remote encrypted-output encoding.
1087
+ const projected = remoteReplayProjection ? contextWindows.project(messages, "remote") : messages;
1088
+ return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
1089
+ ? undefined : { messages: projected };
1090
+ };
1064
1091
  const replayEvent = visibleMessages === event.messages ? event : { ...event, messages: visibleMessages };
1092
+ const branchEntries = ctx.sessionManager.getBranch();
1093
+ const inherited = config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
1094
+ ? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
1095
+ const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
1065
1096
  const resolution = await resolveNativeCompactionEnvironment(ctx, {
1066
1097
  enabled: config.enabled,
1067
- responsesApis: config.responsesApis,
1098
+ // Disabling checkpoint generation cannot turn an inherited opaque
1099
+ // checkpoint into a usable text summary. Replay checks transport support.
1100
+ responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
1068
1101
  codexGatewayModels: compactGatewayModels(resolved),
1069
1102
  });
1070
- if (!resolution.ok) return visibleResult;
1071
- const branchEntries = ctx.sessionManager.getBranch();
1103
+ if (!resolution.ok) {
1104
+ return inheritedCheckpoint
1105
+ ? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason)
1106
+ : finishProjection(visibleMessages);
1107
+ }
1072
1108
  const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
1073
- if (!latest.ok) return visibleResult;
1109
+ if (!latest.ok) {
1110
+ return inheritedCheckpoint
1111
+ ? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latest.reason)
1112
+ : finishProjection(visibleMessages);
1113
+ }
1074
1114
  const result = removeNativeCompactionRetainedMessages({
1075
1115
  messages: replayEvent.messages,
1076
1116
  branchEntries,
@@ -1084,7 +1124,7 @@ async function handleContextInternal(
1084
1124
  ctx.abort();
1085
1125
  return undefined;
1086
1126
  }
1087
- return result.messages === replayEvent.messages ? visibleResult : { messages: result.messages };
1127
+ return finishProjection(result.messages);
1088
1128
  }
1089
1129
 
1090
1130
  /**
@@ -1137,16 +1177,20 @@ async function handleBeforeProviderRequest(
1137
1177
  return undefined;
1138
1178
  }
1139
1179
  }
1140
- if (config.contextManagement === "remote" && await remoteContextActive(ctx, config, contextModel)) {
1180
+ const remoteActive = config.contextManagement === "remote" && await remoteContextActive(ctx, config, contextModel);
1181
+ let remotePayload: unknown;
1182
+ if (remoteActive) {
1141
1183
  try {
1142
- return contextWindows.rewritePayload(event.payload, ctx);
1184
+ remotePayload = contextWindows.rewritePayload(event.payload, ctx);
1185
+ if (contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity()) return remotePayload;
1186
+ event = { ...event, payload: remotePayload };
1143
1187
  } catch {
1144
1188
  notifyRemoteContextFailure(ctx, "malformed-request-state");
1145
1189
  ctx.abort();
1146
1190
  return undefined;
1147
1191
  }
1148
1192
  }
1149
- if (isCodexContextModel(contextModel, config)) {
1193
+ if (!remoteActive && isCodexContextModel(contextModel, config)) {
1150
1194
  const registrationState = getContextToolRegistrationState();
1151
1195
  // A readable empty registry is an intentional host exclusion. Do not
1152
1196
  // rewrite gateway identity or abort an ordinary child-session request.
@@ -1165,18 +1209,23 @@ async function handleBeforeProviderRequest(
1165
1209
 
1166
1210
  // Do not reintroduce a pre-window opaque checkpoint after local projection.
1167
1211
  // Request affinity/encrypted-output rewriting is reserved for active Remote.
1168
- if (config.contextManagement === "remote" && contextWindows.currentIdentity()) return undefined;
1212
+ if (config.contextManagement === "remote" && contextWindows.hasEffectiveWindow()) return undefined;
1169
1213
 
1214
+ const branchEntries = ctx.sessionManager.getBranch();
1215
+ const inherited = config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
1216
+ ? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
1217
+ const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
1170
1218
  const resolution = await resolveNativeCompactionEnvironment(
1171
1219
  ctx,
1172
1220
  {
1173
1221
  enabled: config.enabled,
1174
- responsesApis: config.responsesApis,
1222
+ responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
1175
1223
  codexGatewayModels: compactGatewayModels(resolved),
1176
1224
  },
1177
1225
  event.payload,
1178
1226
  );
1179
1227
  if (resolution.ok === false) {
1228
+ if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason);
1180
1229
  writeDebugArtifact(
1181
1230
  "provider-request",
1182
1231
  {
@@ -1193,7 +1242,7 @@ async function handleBeforeProviderRequest(
1193
1242
  config,
1194
1243
  ctx,
1195
1244
  );
1196
- return undefined;
1245
+ return remotePayload;
1197
1246
  }
1198
1247
 
1199
1248
  const runtime = resolution.runtime;
@@ -1221,11 +1270,11 @@ async function handleBeforeProviderRequest(
1221
1270
  },
1222
1271
  );
1223
1272
 
1224
- const branchEntries = ctx.sessionManager.getBranch();
1225
1273
  const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
1226
1274
  baseUrl: runtime.baseUrl,
1227
1275
  });
1228
1276
  if (!latestNativeCompaction.ok) {
1277
+ if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latestNativeCompaction.reason);
1229
1278
  writeDebugArtifact(
1230
1279
  "provider-request",
1231
1280
  {
@@ -1243,7 +1292,7 @@ async function handleBeforeProviderRequest(
1243
1292
  config,
1244
1293
  ctx,
1245
1294
  );
1246
- return undefined;
1295
+ return remotePayload;
1247
1296
  }
1248
1297
 
1249
1298
  const latestNativeCompactionEntry = latestNativeCompaction.entry;
@@ -1471,7 +1520,7 @@ export default function registerCompactionExtension(
1471
1520
  return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
1472
1521
  });
1473
1522
  pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive, getContextToolRegistrationState));
1474
- pi.on("session_compact", (event, _ctx) => contextWindows.recordCompaction(event.compactionEntry.details));
1523
+ pi.on("session_compact", (_event, ctx) => contextWindows.synchronize(ctx));
1475
1524
  pi.on("session_compact_failed", async (event, ctx) => {
1476
1525
  if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
1477
1526
  });