pi-openai-toolkit 0.22.3 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/docs/configuration.md +5 -5
- package/docs/internals.md +11 -11
- package/package.json +2 -1
- package/src/context-management/manual-compact.ts +599 -422
- package/src/context-management/responses-tools.ts +238 -0
- package/src/context-management/window-manager.ts +94 -0
- package/src/extension-runtime.ts +348 -23
package/src/extension-runtime.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { declarationsEqual, getCurrentSystemMessage, getCurrentTools } from "@earendil-works/pi-ai";
|
|
4
|
+
import { convertResponsesTools } from "./context-management/responses-tools";
|
|
2
5
|
import type {
|
|
3
6
|
BeforeProviderRequestEvent,
|
|
4
7
|
ContextWithSystemEvent,
|
|
@@ -16,10 +19,13 @@ import {
|
|
|
16
19
|
isCodexGatewayModel,
|
|
17
20
|
isNativeCodexModel,
|
|
18
21
|
} from "./context-management/codex-provider";
|
|
19
|
-
import { routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
|
|
22
|
+
import { rewriteContextNamespaceCalls, rewriteContextNamespaceTools, routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
|
|
23
|
+
import { rewriteEncryptedToolOutputs } from "./context-management/window-request";
|
|
20
24
|
import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
|
|
21
25
|
import { CodexContextWindowManager } from "./context-management/window-manager";
|
|
22
|
-
import { ManagedManualCompact } from "./context-management/manual-compact";
|
|
26
|
+
import { ManagedManualCompact, RemoteManualCompact, type RemoteManualCompactOperation } from "./context-management/manual-compact";
|
|
27
|
+
import { isSystemTranscriptMessage } from "./context-management/messages";
|
|
28
|
+
import { isRecord } from "./context-management/types";
|
|
23
29
|
import {
|
|
24
30
|
registerContextManagementTools,
|
|
25
31
|
type ContextToolRegistrationState,
|
|
@@ -79,7 +85,9 @@ import {
|
|
|
79
85
|
getRemoteV2InputProvenance,
|
|
80
86
|
isNativeCompactionDetails,
|
|
81
87
|
type CompactionConfig,
|
|
88
|
+
type DeferredToolCarryoverV1,
|
|
82
89
|
type NativeCompactionDetails,
|
|
90
|
+
type NativeCompactionEntry,
|
|
83
91
|
type NativeCompactionRequestMeta,
|
|
84
92
|
} from "./types";
|
|
85
93
|
|
|
@@ -116,6 +124,7 @@ type CompactionDependencies = {
|
|
|
116
124
|
nativeFallback: typeof runNativeFallbackCompaction;
|
|
117
125
|
contextWindows: CodexContextWindowManager;
|
|
118
126
|
manualCompact?: ManagedManualCompact;
|
|
127
|
+
remoteManualCompact: RemoteManualCompact;
|
|
119
128
|
/** Test seam for the same ordered projection exposed by the Pi host patch. */
|
|
120
129
|
projectCompactionContext?: CompactionContextProjection;
|
|
121
130
|
piContextHookPatch: PiContextHookPatchResult;
|
|
@@ -311,8 +320,9 @@ type CompactionProjectionResult =
|
|
|
311
320
|
async function projectSessionContextForCompaction(
|
|
312
321
|
ctx: ExtensionContext,
|
|
313
322
|
projectCompactionContext: CompactionContextProjection | undefined,
|
|
323
|
+
canonicalMessages?: readonly AgentMessage[],
|
|
314
324
|
): Promise<CompactionProjectionResult> {
|
|
315
|
-
const messages = readSessionContextMessages(ctx);
|
|
325
|
+
const messages = canonicalMessages ? structuredClone([...canonicalMessages]) : readSessionContextMessages(ctx);
|
|
316
326
|
if (!messages) {
|
|
317
327
|
return { ok: false, reason: COMPACTION_SESSION_CONTEXT_UNAVAILABLE };
|
|
318
328
|
}
|
|
@@ -342,6 +352,187 @@ function getLegacySessionContextMessages(
|
|
|
342
352
|
];
|
|
343
353
|
}
|
|
344
354
|
|
|
355
|
+
type ScopedResponsesCompactInput = {
|
|
356
|
+
request: NativeCompactionRequestBody;
|
|
357
|
+
firstKeptEntryId: string;
|
|
358
|
+
deferredToolCarryover?: DeferredToolCarryoverV1;
|
|
359
|
+
requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
|
|
360
|
+
isCurrent: () => boolean;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/** Reject incomplete/ambiguous tool batches rather than let serialization invent results. */
|
|
364
|
+
function requireCompleteCompactionToolPairs(messages: readonly AgentMessage[]): ReadonlyMap<string, string | undefined> {
|
|
365
|
+
const calls = new Map<string, string | undefined>();
|
|
366
|
+
const pending = new Set<string>();
|
|
367
|
+
for (const message of messages) {
|
|
368
|
+
if (message.role === "assistant") {
|
|
369
|
+
if (pending.size) throw new Error("compaction-tool-pair-mismatch");
|
|
370
|
+
for (const block of message.content) {
|
|
371
|
+
if (block.type !== "toolCall") continue;
|
|
372
|
+
const callId = block.id.split("|")[0];
|
|
373
|
+
if (!callId || calls.has(callId)) throw new Error("compaction-tool-pair-mismatch");
|
|
374
|
+
calls.set(callId, block.namespace);
|
|
375
|
+
pending.add(block.id);
|
|
376
|
+
}
|
|
377
|
+
} else if (message.role === "toolResult") {
|
|
378
|
+
if (!pending.delete(message.toolCallId)) throw new Error("compaction-tool-pair-mismatch");
|
|
379
|
+
} else if (pending.size && !isSystemTranscriptMessage(message)) {
|
|
380
|
+
throw new Error("compaction-tool-pair-mismatch");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (pending.size) throw new Error("compaction-tool-pair-mismatch");
|
|
384
|
+
return calls;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Serialization must preserve every verified pair, with unique provider call IDs. */
|
|
388
|
+
function requireCompactionWireToolPairs(input: readonly unknown[], namespaces: ReadonlyMap<string, string | undefined>): void {
|
|
389
|
+
const calls = new Set<string>();
|
|
390
|
+
const pending = new Set<string>();
|
|
391
|
+
for (const item of input) {
|
|
392
|
+
if (!isRecord(item)) throw new Error("compaction-tool-pair-mismatch");
|
|
393
|
+
if (item.type === "function_call") {
|
|
394
|
+
if (typeof item.call_id !== "string" || !namespaces.has(item.call_id) || calls.has(item.call_id)) {
|
|
395
|
+
throw new Error("compaction-tool-pair-mismatch");
|
|
396
|
+
}
|
|
397
|
+
calls.add(item.call_id);
|
|
398
|
+
pending.add(item.call_id);
|
|
399
|
+
} else if (item.type === "function_call_output") {
|
|
400
|
+
if (typeof item.call_id !== "string" || !pending.delete(item.call_id)) throw new Error("compaction-tool-pair-mismatch");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (pending.size || calls.size !== namespaces.size) throw new Error("compaction-tool-pair-mismatch");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function requireCompactionHeadAndRetention(
|
|
407
|
+
canonical: readonly AgentMessage[],
|
|
408
|
+
projected: readonly AgentMessage[],
|
|
409
|
+
retained: readonly AgentMessage[],
|
|
410
|
+
): void {
|
|
411
|
+
const before = getCurrentTools(canonical);
|
|
412
|
+
const after = getCurrentTools(projected);
|
|
413
|
+
if (!getCurrentSystemMessage(projected) || before.length !== after.length
|
|
414
|
+
|| before.some((tool, index) => !after[index] || !declarationsEqual(tool, after[index]!))) {
|
|
415
|
+
throw new Error("compaction-system-tool-head-lost");
|
|
416
|
+
}
|
|
417
|
+
// These exact source contributions will accompany the new summary in Pi.
|
|
418
|
+
// If a hook rewrites/omits one, future strict retained-copy removal cannot be proven.
|
|
419
|
+
let cursor = 0;
|
|
420
|
+
for (const message of retained) {
|
|
421
|
+
if (message.role === "custom" || message.role === "compactionSummary" || isSystemTranscriptMessage(message)) continue;
|
|
422
|
+
const index = projected.findIndex((candidate, index) => index >= cursor && isDeepStrictEqual(candidate, message));
|
|
423
|
+
if (index < 0) throw new Error("compaction-retained-context-mismatch");
|
|
424
|
+
cursor = index + 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Apply only pure wire adapters, never ordinary turn/history-ingest hooks. */
|
|
429
|
+
function adaptCompactionWireInput(input: ResponsesInputItem[], namespaces: ReadonlyMap<string, string | undefined>): ResponsesInputItem[] {
|
|
430
|
+
// The shared legacy serializer omits namespace. Restore it from the verified
|
|
431
|
+
// canonical call before applying the existing context-router wire adapter.
|
|
432
|
+
const namespaced = input.map((item: unknown) => {
|
|
433
|
+
if (!isRecord(item) || item.type !== "function_call" || typeof item.call_id !== "string") return item;
|
|
434
|
+
const namespace = namespaces.get(item.call_id);
|
|
435
|
+
return namespace ? { ...item, namespace } : item;
|
|
436
|
+
});
|
|
437
|
+
const adapted = rewriteEncryptedToolOutputs(rewriteContextNamespaceCalls({ input: namespaced }));
|
|
438
|
+
if (!isRecord(adapted) || !Array.isArray(adapted.input)) throw new Error("compaction-wire-adaptation-failed");
|
|
439
|
+
// The adapters preserve the validated array shape, with native encrypted outputs.
|
|
440
|
+
return adapted.input as ResponsesInputItem[];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function prepareActiveRemoteCompaction(
|
|
444
|
+
event: SessionBeforeCompactEvent,
|
|
445
|
+
ctx: ExtensionContext,
|
|
446
|
+
config: CompactionConfig,
|
|
447
|
+
execution: RemoteCompactionExecution,
|
|
448
|
+
dependencies: CompactionDependencies,
|
|
449
|
+
isCurrent: () => boolean,
|
|
450
|
+
): Promise<ScopedResponsesCompactInput> {
|
|
451
|
+
const windows = dependencies.contextWindows;
|
|
452
|
+
const prepared = windows.prepareRemoteCompaction(event, ctx);
|
|
453
|
+
const branch = ctx.sessionManager.getBranch();
|
|
454
|
+
const provenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
|
|
455
|
+
let checkpoint: NativeCompactionEntry | undefined;
|
|
456
|
+
let requestSource: ScopedResponsesCompactInput["requestSource"] = "session-context";
|
|
457
|
+
if (prepared.owningCompaction) {
|
|
458
|
+
const latest = resolveLatestNativeCompactionEntry(branch, { baseUrl: execution.consumer.baseUrl });
|
|
459
|
+
if (latest.ok) {
|
|
460
|
+
if (latest.entry.id !== prepared.owningCompaction.id) throw new Error("compaction-owner-mismatch");
|
|
461
|
+
if (hasInvalidatedCompactionContext(branch, latest.entry.id)) throw new Error(COMPACTION_CHECKPOINT_CONTEXT_EDITED);
|
|
462
|
+
if (!hasVerifiedCompactionInputProvenance(latest.entry.details, provenance)) throw new Error(COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE);
|
|
463
|
+
checkpoint = latest.entry;
|
|
464
|
+
requestSource = "latest-native-replay";
|
|
465
|
+
} else {
|
|
466
|
+
if (latest.reason !== "latest-compaction-not-native" || !config.allowCompactionContinuityBreak) throw new Error("compaction-continuity-unavailable");
|
|
467
|
+
requestSource = "non-native-session-context";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
let messages = prepared.messages;
|
|
471
|
+
if (config.remoteV2ContextSource === "pi-context-hook") {
|
|
472
|
+
const projection = await windows.withCompactionProjection(() => projectSessionContextForCompaction(
|
|
473
|
+
ctx, dependencies.projectCompactionContext, messages,
|
|
474
|
+
));
|
|
475
|
+
if (!isCurrent()) throw new Error("compaction-operation-superseded");
|
|
476
|
+
if (!projection.ok) throw new Error(projection.reason);
|
|
477
|
+
messages = windows.projectCompactionContext(projection.messages);
|
|
478
|
+
}
|
|
479
|
+
if (!isCurrent()) throw new Error("compaction-operation-superseded");
|
|
480
|
+
requireCompactionHeadAndRetention(prepared.messages, messages, prepared.retainedMessages);
|
|
481
|
+
if (checkpoint) {
|
|
482
|
+
// The synthetic Toolkit hook deliberately leaves these copies alone. Match
|
|
483
|
+
// exactly once, BEFORE encrypted-output markers change the canonical values.
|
|
484
|
+
const pruned = removeNativeCompactionRetainedMessages({
|
|
485
|
+
messages, branchEntries: branch, compactionEntry: checkpoint, expectedInputProvenance: provenance,
|
|
486
|
+
});
|
|
487
|
+
if (!pruned.ok) throw new Error(pruned.reason);
|
|
488
|
+
messages = pruned.messages;
|
|
489
|
+
}
|
|
490
|
+
const namespaces = requireCompleteCompactionToolPairs(messages);
|
|
491
|
+
const tools = getCurrentTools(messages);
|
|
492
|
+
const model = execution.compactor.currentModel;
|
|
493
|
+
const compat = isRecord(model.compat) ? model.compat : {};
|
|
494
|
+
const nativeCodex = model.api === "openai-codex-responses";
|
|
495
|
+
const encoded = windows.projectCompactionContext(messages, true);
|
|
496
|
+
let input: ResponsesInputItem[];
|
|
497
|
+
if (checkpoint) {
|
|
498
|
+
const summaryIndex = findLatestProjectedCompactionSummaryIndex(encoded, checkpoint.summary);
|
|
499
|
+
if (summaryIndex < 0 || encoded.filter((message) => message.role === "compactionSummary").length !== 1) {
|
|
500
|
+
throw new Error(COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE);
|
|
501
|
+
}
|
|
502
|
+
const leading = await serializeMessagesToResponsesInputWithLimits(model, encoded.slice(0, summaryIndex));
|
|
503
|
+
if (!isCurrent()) throw new Error("compaction-operation-superseded");
|
|
504
|
+
const tail = await serializeMessagesToResponsesInputWithLimits(model, encoded.slice(summaryIndex + 1), { firstSystemMessageIsUpdate: true });
|
|
505
|
+
requireCompactionWireToolPairs([...leading, ...tail], namespaces);
|
|
506
|
+
const details = checkpoint.details;
|
|
507
|
+
if (!details) throw new Error("compaction-owner-mismatch");
|
|
508
|
+
input = [...adaptCompactionWireInput(leading, namespaces), ...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]), ...adaptCompactionWireInput(tail, namespaces)];
|
|
509
|
+
} else {
|
|
510
|
+
const serialized = await serializeMessagesToResponsesInputWithLimits(model, encoded);
|
|
511
|
+
requireCompactionWireToolPairs(serialized, namespaces);
|
|
512
|
+
input = adaptCompactionWireInput(serialized, namespaces);
|
|
513
|
+
}
|
|
514
|
+
if (!isCurrent()) throw new Error("compaction-operation-superseded");
|
|
515
|
+
const toolPayload = rewriteContextNamespaceTools({ tools: convertResponsesTools(tools, {
|
|
516
|
+
...(nativeCodex ? { strict: null } : {}),
|
|
517
|
+
supportsStrictMode: typeof compat.supportsStrictMode === "boolean" ? compat.supportsStrictMode : nativeCodex,
|
|
518
|
+
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools === true,
|
|
519
|
+
}) }, { encrypted: true });
|
|
520
|
+
if (!isRecord(toolPayload) || !Array.isArray(toolPayload.tools)) throw new Error("compaction-wire-adaptation-failed");
|
|
521
|
+
// Current cache-stack state is authoritative; never borrow deferred metadata
|
|
522
|
+
// from an unrelated pre-window checkpoint. The canonical head proves which
|
|
523
|
+
// of those schemas are still live, including tools activated before rollover.
|
|
524
|
+
const activation = getLatestDeferredToolCarryover(branch) ?? checkpoint?.details?.deferredToolCarryover;
|
|
525
|
+
const names = new Set(tools.map((tool) => tool.name));
|
|
526
|
+
const deferredToolCarryover = activation ? { ...activation, toolNames: activation.toolNames.filter((name) => names.has(name)) } : undefined;
|
|
527
|
+
return {
|
|
528
|
+
request: { model: execution.compactor.requestModel ?? execution.compactor.model, input,
|
|
529
|
+
instructions: buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions), tools: toolPayload.tools },
|
|
530
|
+
firstKeptEntryId: prepared.firstKeptEntryId,
|
|
531
|
+
deferredToolCarryover: deferredToolCarryover?.toolNames.length ? deferredToolCarryover : undefined,
|
|
532
|
+
requestSource, isCurrent,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
345
536
|
async function runResponsesNativeCompact(
|
|
346
537
|
event: SessionBeforeCompactEvent,
|
|
347
538
|
ctx: ExtensionContext,
|
|
@@ -351,11 +542,13 @@ async function runResponsesNativeCompact(
|
|
|
351
542
|
projectCompactionContext: CompactionContextProjection | undefined,
|
|
352
543
|
/** Only ordinary compaction supplies this; managed recovery remains V2-only. */
|
|
353
544
|
standaloneCompact?: typeof executeNativeCompaction,
|
|
545
|
+
/** Active Remote manual input is strict/current-window-only and has no fallback. */
|
|
546
|
+
scopedInput?: ScopedResponsesCompactInput,
|
|
354
547
|
): Promise<ResponsesCompactOutcome> {
|
|
355
548
|
const { consumer, compactor } = execution;
|
|
356
549
|
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
357
550
|
const branchEntries = event.branchEntries ?? ctx.sessionManager.getBranch();
|
|
358
|
-
const deferredToolCarryover = getLatestDeferredToolCarryover(branchEntries);
|
|
551
|
+
const deferredToolCarryover = scopedInput ? scopedInput.deferredToolCarryover : getLatestDeferredToolCarryover(branchEntries);
|
|
359
552
|
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
360
553
|
baseUrl: consumer.baseUrl,
|
|
361
554
|
});
|
|
@@ -363,7 +556,10 @@ async function runResponsesNativeCompact(
|
|
|
363
556
|
const inputProvenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
|
|
364
557
|
let requestSource: "session-context" | "non-native-session-context" | "latest-native-replay";
|
|
365
558
|
let request: NativeCompactionRequestBody;
|
|
366
|
-
if (
|
|
559
|
+
if (scopedInput) {
|
|
560
|
+
request = scopedInput.request;
|
|
561
|
+
requestSource = scopedInput.requestSource;
|
|
562
|
+
} else if (latestNativeCompaction.ok) {
|
|
367
563
|
if (hasInvalidatedCompactionContext(branchEntries, latestNativeCompaction.entry.id)) {
|
|
368
564
|
return { outcome: "unprojected-input", reason: COMPACTION_CHECKPOINT_CONTEXT_EDITED };
|
|
369
565
|
}
|
|
@@ -482,10 +678,10 @@ async function runResponsesNativeCompact(
|
|
|
482
678
|
sessionId: getSessionId(ctx),
|
|
483
679
|
}, compactor.currentModel);
|
|
484
680
|
if (extras) {
|
|
485
|
-
request = { ...request, ...extras };
|
|
681
|
+
request = { ...request, ...extras, ...(scopedInput ? { tools: scopedInput.request.tools } : {}) };
|
|
486
682
|
}
|
|
487
683
|
|
|
488
|
-
if (event.signal?.aborted) return { outcome: "aborted" };
|
|
684
|
+
if (event.signal?.aborted || (scopedInput && !scopedInput.isCurrent())) return { outcome: "aborted" };
|
|
489
685
|
let protocol: "remote-v2" | "standalone-compact" = "remote-v2";
|
|
490
686
|
let compactResult: Awaited<ReturnType<typeof executeRemoteV2Compaction>> | NativeCompactionClientResult = await remoteCompact({
|
|
491
687
|
runtime: compactor,
|
|
@@ -495,6 +691,7 @@ async function runResponsesNativeCompact(
|
|
|
495
691
|
context: ctx,
|
|
496
692
|
});
|
|
497
693
|
|
|
694
|
+
if (scopedInput && !scopedInput.isCurrent()) return { outcome: "aborted" };
|
|
498
695
|
if (!compactResult.ok) {
|
|
499
696
|
if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
|
|
500
697
|
const tryStandalone = !!standaloneCompact && isStandaloneCompactionEligible(compactor) && qualifiesForStandaloneCompaction(compactResult);
|
|
@@ -507,7 +704,7 @@ async function runResponsesNativeCompact(
|
|
|
507
704
|
errorType: compactResult.errorType,
|
|
508
705
|
errorParam: compactResult.errorParam,
|
|
509
706
|
errorMessage: compactResult.errorMessage,
|
|
510
|
-
nextStep: tryStandalone ? "standalone-compact" : "native-summary",
|
|
707
|
+
nextStep: tryStandalone ? "standalone-compact" : scopedInput ? "stop-and-preserve" : "native-summary",
|
|
511
708
|
}, config, ctx);
|
|
512
709
|
if (tryStandalone && standaloneCompact) {
|
|
513
710
|
if (event.signal?.aborted) return { outcome: "aborted" };
|
|
@@ -578,7 +775,7 @@ async function runResponsesNativeCompact(
|
|
|
578
775
|
}
|
|
579
776
|
|
|
580
777
|
const compaction = createNativeCompactionResult({
|
|
581
|
-
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
778
|
+
firstKeptEntryId: scopedInput?.firstKeptEntryId ?? event.preparation.firstKeptEntryId,
|
|
582
779
|
tokensBefore: event.preparation.tokensBefore,
|
|
583
780
|
details,
|
|
584
781
|
});
|
|
@@ -607,12 +804,13 @@ async function runResponsesNativeCompact(
|
|
|
607
804
|
requestExtras: extras ? Object.keys(extras) : [],
|
|
608
805
|
compactResponseId: compactResult.compactResponseId,
|
|
609
806
|
compactedItems: compactResult.compactedWindow.length,
|
|
610
|
-
firstKeptEntryId:
|
|
807
|
+
firstKeptEntryId: compaction.firstKeptEntryId,
|
|
611
808
|
},
|
|
612
809
|
config,
|
|
613
810
|
ctx,
|
|
614
811
|
);
|
|
615
812
|
|
|
813
|
+
if (scopedInput && !scopedInput.isCurrent()) return { outcome: "aborted" };
|
|
616
814
|
return { outcome: "success", compaction };
|
|
617
815
|
}
|
|
618
816
|
|
|
@@ -744,6 +942,68 @@ async function runUnavailableContextCompaction(
|
|
|
744
942
|
return { cancel: true };
|
|
745
943
|
}
|
|
746
944
|
|
|
945
|
+
const ACTIVE_REMOTE_PREPARATION_REASONS = new Set([
|
|
946
|
+
"unsafe-compaction-window-boundary", "compaction-window-boundary-missing", "compaction-system-tool-head-lost",
|
|
947
|
+
"compaction-branch-changed", "compaction-source-projection-unavailable", "unverified-window-cleanup",
|
|
948
|
+
"compaction-owner-missing", "unsafe-compaction-retained-start", "compaction-owner-mismatch",
|
|
949
|
+
"compaction-continuity-unavailable", "compaction-operation-superseded", "compaction-tool-pair-mismatch",
|
|
950
|
+
"compaction-retained-context-mismatch", "compaction-wire-adaptation-failed", "compaction-boundary-not-found",
|
|
951
|
+
"first-kept-entry-not-found", "compaction-summary-not-found", "retained-context-mismatch", "unverified-compaction-input",
|
|
952
|
+
COMPACTION_CHECKPOINT_CONTEXT_EDITED, COMPACTION_CHECKPOINT_PROVENANCE_UNAVAILABLE,
|
|
953
|
+
COMPACTION_PROJECTED_CONTEXT_UNAVAILABLE, COMPACTION_PROJECTION_UNAVAILABLE, COMPACTION_SESSION_CONTEXT_UNAVAILABLE,
|
|
954
|
+
]);
|
|
955
|
+
|
|
956
|
+
async function runActiveRemoteManualCompact(
|
|
957
|
+
event: SessionBeforeCompactEvent,
|
|
958
|
+
ctx: ExtensionContext,
|
|
959
|
+
dependencies: CompactionDependencies,
|
|
960
|
+
resolved: ResolvedToolkitConfig,
|
|
961
|
+
operation: RemoteManualCompactOperation | undefined,
|
|
962
|
+
): Promise<{ cancel: true } | { compaction: CompactionResult<NativeCompactionDetails> }> {
|
|
963
|
+
const lifecycle = dependencies.remoteManualCompact;
|
|
964
|
+
const config = resolved.config.compaction;
|
|
965
|
+
const stop = (reason: string): { cancel: true } => {
|
|
966
|
+
// No exception text, provider body, auth or opaque content crosses this boundary.
|
|
967
|
+
lifecycle.cancel(operation);
|
|
968
|
+
writeDebugArtifact("compaction-event", {
|
|
969
|
+
event: "session_before_compact.remote-window-manual-stopped", reason, nextStep: "stop-and-preserve",
|
|
970
|
+
}, config, ctx);
|
|
971
|
+
notifyWarning(ctx, `Remote /compact stopped (${reason}); existing context preserved. No summary fallback or automatic continuation was started.`);
|
|
972
|
+
return { cancel: true };
|
|
973
|
+
};
|
|
974
|
+
const isCurrent = () => !!operation && lifecycle.current(operation, ctx);
|
|
975
|
+
try {
|
|
976
|
+
if (!operation || dependencies.manualCompact?.busy || !isCurrent()) return stop("compaction-operation-superseded");
|
|
977
|
+
// Producer policy and auth are resolved once from the initiating document.
|
|
978
|
+
if (resolved.format === "v2" && config.remoteCompactModel) {
|
|
979
|
+
const spec = parseModelSpec(config.remoteCompactModel);
|
|
980
|
+
if (spec) {
|
|
981
|
+
const producerPolicy = resolveToolkitConfig(resolved.snapshot, { provider: spec.provider, id: spec.modelId });
|
|
982
|
+
notifyConfigIssues(ctx, producerPolicy);
|
|
983
|
+
if (producerPolicy.invalidFeatures.includes("compatibility")) return stop("invalid-producer-compatibility");
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const resolution = await resolveRemoteCompactionExecution(ctx, {
|
|
987
|
+
enabled: config.enabled, responsesApis: config.responsesApis, codexGatewayModels: compactGatewayModels(resolved),
|
|
988
|
+
}, config.remoteCompactModel);
|
|
989
|
+
if (!isCurrent()) return stop("compaction-operation-superseded");
|
|
990
|
+
if (!resolution.ok) return stop(resolution.reason);
|
|
991
|
+
const scopedEvent = { ...event, signal: operation.signal };
|
|
992
|
+
const input = await prepareActiveRemoteCompaction(scopedEvent, ctx, config, resolution.execution, dependencies, isCurrent);
|
|
993
|
+
if (!isCurrent()) return stop("compaction-operation-superseded");
|
|
994
|
+
const outcome = await runResponsesNativeCompact(scopedEvent, ctx, config, resolution.execution,
|
|
995
|
+
dependencies.remoteCompact, dependencies.projectCompactionContext, undefined, input);
|
|
996
|
+
if (!isCurrent()) return stop("compaction-operation-superseded");
|
|
997
|
+
if (outcome.outcome !== "success") return stop(outcome.outcome === "unprojected-input" ? outcome.reason
|
|
998
|
+
: outcome.outcome === "failed" ? outcome.remoteFailure?.reason ?? "remote-compaction-failed" : "aborted");
|
|
999
|
+
const result = lifecycle.arm(operation, ctx, outcome.compaction);
|
|
1000
|
+
return result ?? stop("compaction-operation-superseded");
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
return stop(error instanceof Error && ACTIVE_REMOTE_PREPARATION_REASONS.has(error.message)
|
|
1003
|
+
? error.message : "remote-compaction-failed");
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
747
1007
|
async function handleSessionBeforeCompact(
|
|
748
1008
|
event: SessionBeforeCompactEvent,
|
|
749
1009
|
ctx: ExtensionContext,
|
|
@@ -786,16 +1046,33 @@ async function handleSessionBeforeCompact(
|
|
|
786
1046
|
return { cancel: true };
|
|
787
1047
|
}
|
|
788
1048
|
|
|
1049
|
+
// Capture intent BEFORE capability/auth awaits so selection/navigation/work
|
|
1050
|
+
// during resolution cannot silently become the owner of an older /compact.
|
|
1051
|
+
let manualOperation: RemoteManualCompactOperation | undefined;
|
|
1052
|
+
if (event.reason === "manual" && !dependencies.manualCompact?.isMaintenance && config.enabled && config.contextManagement === "remote") {
|
|
1053
|
+
try { manualOperation = dependencies.remoteManualCompact.begin(event, ctx, resolved); } catch { /* active route stops below */ }
|
|
1054
|
+
}
|
|
1055
|
+
|
|
789
1056
|
// Active and inactive covered Codex consumers retain their existing routing.
|
|
790
1057
|
const contextModel = config.enabled && config.contextManagement === "remote"
|
|
791
1058
|
? await resolveContextCapabilityModel(ctx)
|
|
792
1059
|
: undefined;
|
|
1060
|
+
if (manualOperation && !dependencies.remoteManualCompact.current(manualOperation, ctx)) {
|
|
1061
|
+
dependencies.remoteManualCompact.cancel(manualOperation);
|
|
1062
|
+
return { cancel: true };
|
|
1063
|
+
}
|
|
793
1064
|
if (isCodexContextModel(contextModel, config)) {
|
|
794
|
-
|
|
1065
|
+
const active = await remoteContextActive(ctx, config, contextModel);
|
|
1066
|
+
// New work may make a formerly active runtime look unavailable. That is
|
|
1067
|
+
// supersession, not permission to start the inactive-runtime fallback.
|
|
1068
|
+
if (manualOperation && !dependencies.remoteManualCompact.current(manualOperation, ctx)) {
|
|
1069
|
+
dependencies.remoteManualCompact.cancel(manualOperation);
|
|
1070
|
+
return { cancel: true };
|
|
1071
|
+
}
|
|
1072
|
+
if (active) {
|
|
795
1073
|
try {
|
|
796
|
-
if (event.reason === "manual" &&
|
|
797
|
-
await
|
|
798
|
-
return { cancel: true };
|
|
1074
|
+
if (event.reason === "manual" && !dependencies.manualCompact?.isMaintenance) {
|
|
1075
|
+
return await runActiveRemoteManualCompact(event, ctx, dependencies, resolved, manualOperation);
|
|
799
1076
|
}
|
|
800
1077
|
return dependencies.contextWindows.prepareCompaction(event);
|
|
801
1078
|
} catch {
|
|
@@ -804,6 +1081,7 @@ async function handleSessionBeforeCompact(
|
|
|
804
1081
|
}
|
|
805
1082
|
}
|
|
806
1083
|
|
|
1084
|
+
dependencies.remoteManualCompact.cancel(manualOperation);
|
|
807
1085
|
const registrationState = getContextToolRegistrationState();
|
|
808
1086
|
const failureReason = contextManagementFailureReason(registrationState);
|
|
809
1087
|
if (failureReason !== "context-tools-excluded") {
|
|
@@ -819,6 +1097,7 @@ async function handleSessionBeforeCompact(
|
|
|
819
1097
|
);
|
|
820
1098
|
}
|
|
821
1099
|
|
|
1100
|
+
dependencies.remoteManualCompact.cancel(manualOperation);
|
|
822
1101
|
try {
|
|
823
1102
|
const detached = dependencies.contextWindows.prepareDetachedCompaction(event);
|
|
824
1103
|
// A local consumer cannot summarize history owned by a Remote window,
|
|
@@ -1056,6 +1335,11 @@ async function handleContextInternal(
|
|
|
1056
1335
|
// The latter still contains retired remote windows and can be inflated by
|
|
1057
1336
|
// an upstream error even though this request only carries the live window.
|
|
1058
1337
|
let projected = contextWindows.project(event.messages, "remote", false);
|
|
1338
|
+
if (contextWindows.isPreparingCompaction) {
|
|
1339
|
+
// Synthetic preparation is read-only. The caller owns the one strict
|
|
1340
|
+
// retained-copy match, then encrypted wire adaptation after all hooks.
|
|
1341
|
+
return { messages: projected };
|
|
1342
|
+
}
|
|
1059
1343
|
// Projection may observe the exact queued rollover before Pi persists
|
|
1060
1344
|
// it. Decide replay ownership from this request, not the old branch.
|
|
1061
1345
|
const ownsWindow = contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity();
|
|
@@ -1216,10 +1500,11 @@ async function handleBeforeProviderRequest(
|
|
|
1216
1500
|
: undefined;
|
|
1217
1501
|
const remoteActive = config.contextManagement === "remote" && await remoteContextActive(ctx, config, contextModel);
|
|
1218
1502
|
let remotePayload: unknown;
|
|
1503
|
+
let activeWindowPayload = false;
|
|
1219
1504
|
if (remoteActive) {
|
|
1220
1505
|
try {
|
|
1221
1506
|
remotePayload = contextWindows.rewritePayload(event.payload, ctx);
|
|
1222
|
-
|
|
1507
|
+
activeWindowPayload = contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity();
|
|
1223
1508
|
event = { ...event, payload: remotePayload };
|
|
1224
1509
|
} catch {
|
|
1225
1510
|
notifyRemoteContextFailure(ctx, "malformed-request-state");
|
|
@@ -1246,10 +1531,10 @@ async function handleBeforeProviderRequest(
|
|
|
1246
1531
|
|
|
1247
1532
|
// Do not reintroduce a pre-window opaque checkpoint after local projection.
|
|
1248
1533
|
// Request affinity/encrypted-output rewriting is reserved for active Remote.
|
|
1249
|
-
if (contextWindows.hasEffectiveWindow()) return undefined;
|
|
1534
|
+
if (contextWindows.hasEffectiveWindow() && !activeWindowPayload) return undefined;
|
|
1250
1535
|
|
|
1251
1536
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
1252
|
-
const inherited = config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
|
|
1537
|
+
const inherited = !activeWindowPayload && config.contextManagement === "remote" && contextWindows.hasSupersededWindow()
|
|
1253
1538
|
? resolveLatestNativeCompactionEntry(branchEntries) : undefined;
|
|
1254
1539
|
const inheritedCheckpoint = inherited?.ok ? inherited.entry : undefined;
|
|
1255
1540
|
const resolution = await resolveNativeCompactionEnvironment(
|
|
@@ -1307,6 +1592,10 @@ async function handleBeforeProviderRequest(
|
|
|
1307
1592
|
},
|
|
1308
1593
|
);
|
|
1309
1594
|
|
|
1595
|
+
// Capture is pre-Web-Search metadata only. An effective Remote window still
|
|
1596
|
+
// owns this live payload and must NEVER enter ordinary opaque replay below.
|
|
1597
|
+
if (activeWindowPayload) return remotePayload;
|
|
1598
|
+
|
|
1310
1599
|
const latestNativeCompaction = resolveLatestNativeCompactionEntry(branchEntries, {
|
|
1311
1600
|
baseUrl: runtime.baseUrl,
|
|
1312
1601
|
});
|
|
@@ -1422,6 +1711,7 @@ export default function registerCompactionExtension(
|
|
|
1422
1711
|
loadHistoryNotesThreadHint(ctx, signal, gatewayModels)
|
|
1423
1712
|
);
|
|
1424
1713
|
const manualCompact = new ManagedManualCompact(pi, contextWindows);
|
|
1714
|
+
const remoteManualCompact = new RemoteManualCompact(pi, contextWindows);
|
|
1425
1715
|
const dependencies: CompactionDependencies = {
|
|
1426
1716
|
remoteCompact: executeRemoteV2Compaction,
|
|
1427
1717
|
standaloneCompact: executeNativeCompaction,
|
|
@@ -1429,7 +1719,8 @@ export default function registerCompactionExtension(
|
|
|
1429
1719
|
contextWindows,
|
|
1430
1720
|
...overrides,
|
|
1431
1721
|
manualCompact,
|
|
1432
|
-
|
|
1722
|
+
remoteManualCompact,
|
|
1723
|
+
loadConfig: () => remoteManualCompact.snapshot ?? manualCompact.snapshot ?? loadConfig(),
|
|
1433
1724
|
piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
|
|
1434
1725
|
};
|
|
1435
1726
|
let tools!: ReturnType<typeof registerContextManagementTools>;
|
|
@@ -1488,6 +1779,7 @@ export default function registerCompactionExtension(
|
|
|
1488
1779
|
}
|
|
1489
1780
|
};
|
|
1490
1781
|
pi.on("session_start", async (_event, ctx) => {
|
|
1782
|
+
remoteManualCompact.invalidate();
|
|
1491
1783
|
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
1492
1784
|
const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true }, resolved);
|
|
1493
1785
|
await manualCompact.recover(ctx);
|
|
@@ -1557,12 +1849,28 @@ export default function registerCompactionExtension(
|
|
|
1557
1849
|
manualCompact.guardRequest(ctx);
|
|
1558
1850
|
return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
|
|
1559
1851
|
});
|
|
1560
|
-
pi.on("session_before_compact", (event, ctx) =>
|
|
1561
|
-
|
|
1852
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
1853
|
+
remoteManualCompact.invalidate();
|
|
1854
|
+
try {
|
|
1855
|
+
return await handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive, getContextToolRegistrationState);
|
|
1856
|
+
} catch {
|
|
1857
|
+
// Pi treats a rejected hook as absent and can invoke its implicit summary.
|
|
1858
|
+
// Unexpected preparation/service failures must never authorize that work.
|
|
1859
|
+
try { notifyWarning(ctx, "Compaction stopped; existing context preserved. Resume manually."); } catch { /* stale UI */ }
|
|
1860
|
+
return { cancel: true };
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
pi.on("session_compact", (event, ctx) => {
|
|
1864
|
+
contextWindows.synchronize(ctx);
|
|
1865
|
+
if (event.reason === "manual" && event.fromExtension) remoteManualCompact.persisted(ctx);
|
|
1866
|
+
else remoteManualCompact.invalidate();
|
|
1867
|
+
});
|
|
1562
1868
|
pi.on("session_compact_failed", async (event, ctx) => {
|
|
1869
|
+
remoteManualCompact.invalidate();
|
|
1563
1870
|
if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
|
|
1564
1871
|
});
|
|
1565
1872
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1873
|
+
remoteManualCompact.invalidate();
|
|
1566
1874
|
await manualCompact.shutdown(ctx);
|
|
1567
1875
|
contextWindowReady = false;
|
|
1568
1876
|
contextWindows.reset();
|
|
@@ -1574,6 +1882,7 @@ export default function registerCompactionExtension(
|
|
|
1574
1882
|
await manualCompact.settled(ctx);
|
|
1575
1883
|
});
|
|
1576
1884
|
pi.on("model_select", async (event, ctx) => {
|
|
1885
|
+
remoteManualCompact.invalidate();
|
|
1577
1886
|
manualCompact.modelSelected(event.model, ctx);
|
|
1578
1887
|
// Switching into a covered model mid-session must open the window
|
|
1579
1888
|
// lifecycle immediately: without an identity the request rewrite skips
|
|
@@ -1584,13 +1893,29 @@ export default function registerCompactionExtension(
|
|
|
1584
1893
|
await syncTools(ctx, event.model, { notifyWindowFailure: true }, resolved);
|
|
1585
1894
|
});
|
|
1586
1895
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
1896
|
+
remoteManualCompact.invalidate();
|
|
1587
1897
|
await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
|
|
1588
1898
|
});
|
|
1589
|
-
pi.on("
|
|
1590
|
-
pi.on("
|
|
1899
|
+
pi.on("agent_start", () => remoteManualCompact.invalidate());
|
|
1900
|
+
pi.on("input", () => { remoteManualCompact.invalidate(); });
|
|
1901
|
+
pi.on("thinking_level_select", (event, ctx) => {
|
|
1902
|
+
remoteManualCompact.invalidate();
|
|
1903
|
+
manualCompact.thinkingSelected(event.level, ctx);
|
|
1904
|
+
});
|
|
1905
|
+
pi.on("message_start", (event) => {
|
|
1906
|
+
remoteManualCompact.invalidate();
|
|
1907
|
+
manualCompact.messageStarted(event.message);
|
|
1908
|
+
});
|
|
1591
1909
|
pi.on("turn_end", (_event, ctx) => manualCompact.turnEnded(ctx));
|
|
1592
|
-
pi.on("tool_call", (event) =>
|
|
1910
|
+
pi.on("tool_call", (event) => {
|
|
1911
|
+
remoteManualCompact.invalidate();
|
|
1912
|
+
return manualCompact.guardTool(event.toolName);
|
|
1913
|
+
});
|
|
1914
|
+
pi.on("session_before_switch", () => remoteManualCompact.invalidate());
|
|
1915
|
+
pi.on("session_before_fork", () => remoteManualCompact.invalidate());
|
|
1916
|
+
pi.on("session_before_tree", () => remoteManualCompact.invalidate());
|
|
1593
1917
|
pi.on("session_tree", async (_event, ctx) => {
|
|
1918
|
+
remoteManualCompact.invalidate();
|
|
1594
1919
|
contextWindows.synchronize(ctx);
|
|
1595
1920
|
await manualCompact.recover(ctx);
|
|
1596
1921
|
await syncTools(ctx);
|