pi-openai-toolkit 0.20.5 → 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 +9 -8
- package/package.json +1 -1
- package/src/context-management/window-manager.ts +84 -58
- package/src/extension-runtime.ts +93 -42
- package/src/native-fallback.ts +89 -30
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
|
|
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
|
|
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
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
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`.
|
|
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
|
@@ -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
|
-
|
|
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
|
|
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
|
-
/**
|
|
113
|
+
/** Derived eligibility, never a replayed marker command. */
|
|
112
114
|
hasPendingTrim(): boolean {
|
|
113
|
-
return this.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
247
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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 =
|
|
450
|
-
if (
|
|
451
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
package/src/extension-runtime.ts
CHANGED
|
@@ -882,7 +882,9 @@ async function handleSessionBeforeCompact(
|
|
|
882
882
|
config,
|
|
883
883
|
ctx,
|
|
884
884
|
);
|
|
885
|
-
|
|
885
|
+
const expectedConsumerCapabilityMiss =
|
|
886
|
+
resolution.reason === "unsupported-api" && resolution.modelSpec === undefined;
|
|
887
|
+
if (config.remoteCompactModel && !expectedConsumerCapabilityMiss) {
|
|
886
888
|
notifyWarning(
|
|
887
889
|
ctx,
|
|
888
890
|
`remote compaction model "${config.remoteCompactModel}" unusable (${resolution.reason}); using the native fallback chain`,
|
|
@@ -955,6 +957,19 @@ async function handleSessionBeforeCompact(
|
|
|
955
957
|
return undefined;
|
|
956
958
|
}
|
|
957
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
|
+
|
|
958
973
|
async function handleContextInternal(
|
|
959
974
|
event: ContextWithSystemEvent,
|
|
960
975
|
ctx: ExtensionContext,
|
|
@@ -975,6 +990,7 @@ async function handleContextInternal(
|
|
|
975
990
|
}
|
|
976
991
|
|
|
977
992
|
let localMessages: AgentMessage[] | undefined;
|
|
993
|
+
let remoteReplayProjection = false;
|
|
978
994
|
if (config.contextManagement === "remote") {
|
|
979
995
|
const contextModel = await resolveContextCapabilityModel(ctx);
|
|
980
996
|
try {
|
|
@@ -992,7 +1008,11 @@ async function handleContextInternal(
|
|
|
992
1008
|
// Measure the provider-visible current window, not Pi's durable transcript.
|
|
993
1009
|
// The latter still contains retired remote windows and can be inflated by
|
|
994
1010
|
// an upstream error even though this request only carries the live window.
|
|
995
|
-
|
|
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");
|
|
996
1016
|
contextWindows.recordBudget(
|
|
997
1017
|
pi,
|
|
998
1018
|
ctx,
|
|
@@ -1000,9 +1020,13 @@ async function handleContextInternal(
|
|
|
1000
1020
|
config.contextReminderThresholdPercent,
|
|
1001
1021
|
);
|
|
1002
1022
|
reportProjectionDiagnostics(contextWindows, config, ctx);
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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;
|
|
1006
1030
|
} catch (error) {
|
|
1007
1031
|
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
1008
1032
|
reportPiContextHookFailure(ctx, "malformed-window-state");
|
|
@@ -1011,11 +1035,10 @@ async function handleContextInternal(
|
|
|
1011
1035
|
}
|
|
1012
1036
|
}
|
|
1013
1037
|
|
|
1014
|
-
// An
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1018
|
-
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)) {
|
|
1019
1042
|
const registrationState = getContextToolRegistrationState();
|
|
1020
1043
|
if (isExcludedGatewayContext(contextModel, config, registrationState)) {
|
|
1021
1044
|
const visibleMessages = contextWindows.project(event.messages, "off");
|
|
@@ -1036,39 +1059,58 @@ async function handleContextInternal(
|
|
|
1036
1059
|
: { messages: visibleMessages };
|
|
1037
1060
|
}
|
|
1038
1061
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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;
|
|
1048
1078
|
}
|
|
1049
|
-
} catch (error) {
|
|
1050
|
-
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
1051
|
-
reportPiContextHookFailure(ctx, error instanceof Error ? error.message : String(error));
|
|
1052
|
-
ctx.abort();
|
|
1053
|
-
return undefined;
|
|
1054
1079
|
}
|
|
1055
1080
|
}
|
|
1056
1081
|
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1082
|
+
// Without an effective Remote boundary, the latest checkpoint owns replay.
|
|
1083
|
+
// This also covers a newer checkpoint superseding a historical window.
|
|
1059
1084
|
const visibleMessages = localMessages ?? contextWindows.project(event.messages, "off");
|
|
1060
|
-
const
|
|
1061
|
-
|
|
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
|
+
};
|
|
1062
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;
|
|
1063
1096
|
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
1064
1097
|
enabled: config.enabled,
|
|
1065
|
-
|
|
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,
|
|
1066
1101
|
codexGatewayModels: compactGatewayModels(resolved),
|
|
1067
1102
|
});
|
|
1068
|
-
if (!resolution.ok)
|
|
1069
|
-
|
|
1103
|
+
if (!resolution.ok) {
|
|
1104
|
+
return inheritedCheckpoint
|
|
1105
|
+
? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason)
|
|
1106
|
+
: finishProjection(visibleMessages);
|
|
1107
|
+
}
|
|
1070
1108
|
const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
|
|
1071
|
-
if (!latest.ok)
|
|
1109
|
+
if (!latest.ok) {
|
|
1110
|
+
return inheritedCheckpoint
|
|
1111
|
+
? rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latest.reason)
|
|
1112
|
+
: finishProjection(visibleMessages);
|
|
1113
|
+
}
|
|
1072
1114
|
const result = removeNativeCompactionRetainedMessages({
|
|
1073
1115
|
messages: replayEvent.messages,
|
|
1074
1116
|
branchEntries,
|
|
@@ -1082,7 +1124,7 @@ async function handleContextInternal(
|
|
|
1082
1124
|
ctx.abort();
|
|
1083
1125
|
return undefined;
|
|
1084
1126
|
}
|
|
1085
|
-
return result.messages
|
|
1127
|
+
return finishProjection(result.messages);
|
|
1086
1128
|
}
|
|
1087
1129
|
|
|
1088
1130
|
/**
|
|
@@ -1135,16 +1177,20 @@ async function handleBeforeProviderRequest(
|
|
|
1135
1177
|
return undefined;
|
|
1136
1178
|
}
|
|
1137
1179
|
}
|
|
1138
|
-
|
|
1180
|
+
const remoteActive = config.contextManagement === "remote" && await remoteContextActive(ctx, config, contextModel);
|
|
1181
|
+
let remotePayload: unknown;
|
|
1182
|
+
if (remoteActive) {
|
|
1139
1183
|
try {
|
|
1140
|
-
|
|
1184
|
+
remotePayload = contextWindows.rewritePayload(event.payload, ctx);
|
|
1185
|
+
if (contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity()) return remotePayload;
|
|
1186
|
+
event = { ...event, payload: remotePayload };
|
|
1141
1187
|
} catch {
|
|
1142
1188
|
notifyRemoteContextFailure(ctx, "malformed-request-state");
|
|
1143
1189
|
ctx.abort();
|
|
1144
1190
|
return undefined;
|
|
1145
1191
|
}
|
|
1146
1192
|
}
|
|
1147
|
-
if (isCodexContextModel(contextModel, config)) {
|
|
1193
|
+
if (!remoteActive && isCodexContextModel(contextModel, config)) {
|
|
1148
1194
|
const registrationState = getContextToolRegistrationState();
|
|
1149
1195
|
// A readable empty registry is an intentional host exclusion. Do not
|
|
1150
1196
|
// rewrite gateway identity or abort an ordinary child-session request.
|
|
@@ -1163,18 +1209,23 @@ async function handleBeforeProviderRequest(
|
|
|
1163
1209
|
|
|
1164
1210
|
// Do not reintroduce a pre-window opaque checkpoint after local projection.
|
|
1165
1211
|
// Request affinity/encrypted-output rewriting is reserved for active Remote.
|
|
1166
|
-
if (config.contextManagement === "remote" && contextWindows.
|
|
1212
|
+
if (config.contextManagement === "remote" && contextWindows.hasEffectiveWindow()) return undefined;
|
|
1167
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;
|
|
1168
1218
|
const resolution = await resolveNativeCompactionEnvironment(
|
|
1169
1219
|
ctx,
|
|
1170
1220
|
{
|
|
1171
1221
|
enabled: config.enabled,
|
|
1172
|
-
responsesApis: config.responsesApis,
|
|
1222
|
+
responsesApis: inheritedCheckpoint ? undefined : config.responsesApis,
|
|
1173
1223
|
codexGatewayModels: compactGatewayModels(resolved),
|
|
1174
1224
|
},
|
|
1175
1225
|
event.payload,
|
|
1176
1226
|
);
|
|
1177
1227
|
if (resolution.ok === false) {
|
|
1228
|
+
if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, resolution.reason);
|
|
1178
1229
|
writeDebugArtifact(
|
|
1179
1230
|
"provider-request",
|
|
1180
1231
|
{
|
|
@@ -1191,7 +1242,7 @@ async function handleBeforeProviderRequest(
|
|
|
1191
1242
|
config,
|
|
1192
1243
|
ctx,
|
|
1193
1244
|
);
|
|
1194
|
-
return
|
|
1245
|
+
return remotePayload;
|
|
1195
1246
|
}
|
|
1196
1247
|
|
|
1197
1248
|
const runtime = resolution.runtime;
|
|
@@ -1219,11 +1270,11 @@ async function handleBeforeProviderRequest(
|
|
|
1219
1270
|
},
|
|
1220
1271
|
);
|
|
1221
1272
|
|
|
1222
|
-
const branchEntries = ctx.sessionManager.getBranch();
|
|
1223
1273
|
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
1224
1274
|
baseUrl: runtime.baseUrl,
|
|
1225
1275
|
});
|
|
1226
1276
|
if (!latestNativeCompaction.ok) {
|
|
1277
|
+
if (inheritedCheckpoint) return rejectInheritedCheckpointReplay(ctx, config, inheritedCheckpoint.id, latestNativeCompaction.reason);
|
|
1227
1278
|
writeDebugArtifact(
|
|
1228
1279
|
"provider-request",
|
|
1229
1280
|
{
|
|
@@ -1241,7 +1292,7 @@ async function handleBeforeProviderRequest(
|
|
|
1241
1292
|
config,
|
|
1242
1293
|
ctx,
|
|
1243
1294
|
);
|
|
1244
|
-
return
|
|
1295
|
+
return remotePayload;
|
|
1245
1296
|
}
|
|
1246
1297
|
|
|
1247
1298
|
const latestNativeCompactionEntry = latestNativeCompaction.entry;
|
|
@@ -1469,7 +1520,7 @@ export default function registerCompactionExtension(
|
|
|
1469
1520
|
return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
|
|
1470
1521
|
});
|
|
1471
1522
|
pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive, getContextToolRegistrationState));
|
|
1472
|
-
pi.on("session_compact", (
|
|
1523
|
+
pi.on("session_compact", (_event, ctx) => contextWindows.synchronize(ctx));
|
|
1473
1524
|
pi.on("session_compact_failed", async (event, ctx) => {
|
|
1474
1525
|
if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
|
|
1475
1526
|
});
|
package/src/native-fallback.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
compact,
|
|
3
|
+
convertToLlm,
|
|
3
4
|
DEFAULT_COMPACTION_SETTINGS,
|
|
4
|
-
|
|
5
|
+
serializeConversation,
|
|
5
6
|
type CompactionResult,
|
|
6
7
|
type ExtensionContext,
|
|
7
8
|
type SessionBeforeCompactEvent,
|
|
@@ -59,16 +60,10 @@ function toErrorMessage(error: unknown): string {
|
|
|
59
60
|
return error instanceof Error ? error.message : String(error);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
/**
|
|
63
|
-
* Output budget Pi gives the summary itself: `generateSummary` caps `maxTokens` at
|
|
64
|
-
* `min(floor(0.8 * reserveTokens), model.maxTokens)`.
|
|
65
|
-
*/
|
|
66
|
-
const SUMMARIZATION_OUTPUT_RESERVE_TOKENS = Math.floor(
|
|
67
|
-
DEFAULT_COMPACTION_SETTINGS.reserveTokens * 0.8,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
/** Summarization instructions plus the `<conversation>` framing around the transcript. */
|
|
63
|
+
/** Summarization instructions plus the framing around the serialized transcript. */
|
|
71
64
|
const SUMMARIZATION_TEMPLATE_TOKENS = 1024;
|
|
65
|
+
const HISTORY_OUTPUT_RESERVE_RATIO = 0.8;
|
|
66
|
+
const TURN_PREFIX_OUTPUT_RESERVE_RATIO = 0.5;
|
|
72
67
|
|
|
73
68
|
export type SummarizationRequestSize = {
|
|
74
69
|
inputTokens: number;
|
|
@@ -76,33 +71,93 @@ export type SummarizationRequestSize = {
|
|
|
76
71
|
totalTokens: number;
|
|
77
72
|
};
|
|
78
73
|
|
|
74
|
+
function estimateTextTokens(text: string | undefined): number {
|
|
75
|
+
return text ? Math.ceil(String(text).length / 4) : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function estimateSerializedConversationTokens(
|
|
79
|
+
messages: SessionBeforeCompactEvent["preparation"]["messagesToSummarize"],
|
|
80
|
+
): number {
|
|
81
|
+
const serialized = serializeConversation(convertToLlm(messages));
|
|
82
|
+
return estimateTextTokens(serialized);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getEffectiveReserveTokens(preparation: SessionBeforeCompactEvent["preparation"]): number {
|
|
86
|
+
const reserveTokens = preparation.settings?.reserveTokens;
|
|
87
|
+
return typeof reserveTokens === "number" && Number.isFinite(reserveTokens) && reserveTokens >= 0
|
|
88
|
+
? reserveTokens
|
|
89
|
+
: DEFAULT_COMPACTION_SETTINGS.reserveTokens;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getOutputReserveTokens(
|
|
93
|
+
reserveTokens: number,
|
|
94
|
+
model: { maxTokens?: number },
|
|
95
|
+
ratio: number,
|
|
96
|
+
): number {
|
|
97
|
+
const outputTokens = Math.floor(ratio * reserveTokens);
|
|
98
|
+
return typeof model.maxTokens === "number" && model.maxTokens > 0
|
|
99
|
+
? Math.min(outputTokens, model.maxTokens)
|
|
100
|
+
: outputTokens;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function estimateSummaryRequest(
|
|
104
|
+
messages: SessionBeforeCompactEvent["preparation"]["messagesToSummarize"],
|
|
105
|
+
outputTokens: number,
|
|
106
|
+
previousSummary?: string,
|
|
107
|
+
customInstructions?: string,
|
|
108
|
+
): SummarizationRequestSize {
|
|
109
|
+
const inputTokens =
|
|
110
|
+
SUMMARIZATION_TEMPLATE_TOKENS
|
|
111
|
+
+ estimateSerializedConversationTokens(messages)
|
|
112
|
+
+ estimateTextTokens(previousSummary)
|
|
113
|
+
+ estimateTextTokens(customInstructions);
|
|
114
|
+
return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
|
|
115
|
+
}
|
|
116
|
+
|
|
79
117
|
/**
|
|
80
|
-
* Size of the request Pi's `compact()` is about to send
|
|
81
|
-
* measures context (the exported `estimateTokens`, a conservative chars/4).
|
|
118
|
+
* Size of the individual request(s) Pi's `compact()` is about to send.
|
|
82
119
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
120
|
+
* Pi first converts messages with `convertToLlm()` and then applies
|
|
121
|
+
* `serializeConversation()`, which truncates tool results before they enter the
|
|
122
|
+
* summarization prompt. Split-turn compaction sends history and turn-prefix
|
|
123
|
+
* summaries as separate requests, so this returns the largest individual request
|
|
124
|
+
* rather than adding their inputs together. The fixed allowance remains
|
|
125
|
+
* deliberately conservative for Pi's system prompt, tags, and instructions.
|
|
126
|
+
*
|
|
127
|
+
* A session driven by a wide-window model (400k) can produce a preparation that a
|
|
128
|
+
* narrower summary model (272k) cannot accept. Callers must not attempt that request;
|
|
129
|
+
* a missing `contextWindow` skips the check instead of guessing.
|
|
88
130
|
*/
|
|
89
131
|
export function estimateSummarizationRequest(
|
|
90
132
|
preparation: SessionBeforeCompactEvent["preparation"],
|
|
91
133
|
model: { contextWindow?: number; maxTokens?: number },
|
|
134
|
+
customInstructions?: string,
|
|
92
135
|
): SummarizationRequestSize {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
136
|
+
const reserveTokens = getEffectiveReserveTokens(preparation);
|
|
137
|
+
const historyMessages = preparation.messagesToSummarize ?? [];
|
|
138
|
+
const turnPrefixMessages = preparation.turnPrefixMessages ?? [];
|
|
139
|
+
const historyOutputTokens = getOutputReserveTokens(reserveTokens, model, HISTORY_OUTPUT_RESERVE_RATIO);
|
|
140
|
+
const historyRequest = estimateSummaryRequest(
|
|
141
|
+
historyMessages,
|
|
142
|
+
historyOutputTokens,
|
|
143
|
+
preparation.previousSummary,
|
|
144
|
+
customInstructions,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (!preparation.isSplitTurn || turnPrefixMessages.length === 0) {
|
|
148
|
+
return historyRequest;
|
|
101
149
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
:
|
|
105
|
-
|
|
150
|
+
|
|
151
|
+
const requests = [
|
|
152
|
+
...(historyMessages.length > 0 ? [historyRequest] : []),
|
|
153
|
+
estimateSummaryRequest(
|
|
154
|
+
turnPrefixMessages,
|
|
155
|
+
getOutputReserveTokens(reserveTokens, model, TURN_PREFIX_OUTPUT_RESERVE_RATIO),
|
|
156
|
+
),
|
|
157
|
+
];
|
|
158
|
+
return requests.reduce((largest, request) =>
|
|
159
|
+
request.totalTokens > largest.totalTokens ? request : largest,
|
|
160
|
+
);
|
|
106
161
|
}
|
|
107
162
|
|
|
108
163
|
/** Whether the summary model can accept this request at all; unknown windows always pass. */
|
|
@@ -163,7 +218,11 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
163
218
|
return { ok: false, reason: "same-as-current-model", modelSpec: spec };
|
|
164
219
|
}
|
|
165
220
|
|
|
166
|
-
const size = estimateSummarizationRequest(
|
|
221
|
+
const size = estimateSummarizationRequest(
|
|
222
|
+
event.preparation,
|
|
223
|
+
model as { contextWindow?: number; maxTokens?: number },
|
|
224
|
+
event.customInstructions,
|
|
225
|
+
);
|
|
167
226
|
if (!fitsSummarizationRequest(size, model as { contextWindow?: number })) {
|
|
168
227
|
return {
|
|
169
228
|
ok: false,
|