pi-openai-toolkit 0.23.0 → 0.23.2
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 +6 -2
- package/README.zh.md +6 -2
- package/docs/configuration.md +14 -6
- package/docs/internals.md +17 -14
- package/package.json +3 -1
- package/src/context-management/manual-compact.ts +23 -375
- package/src/context-management/responses-tools.ts +238 -0
- package/src/context-management/scoped-compaction.ts +89 -0
- package/src/context-management/window-manager.ts +8 -1
- package/src/extension-runtime.ts +113 -180
- package/src/native-fallback.ts +32 -29
package/src/extension-runtime.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
3
|
import { declarationsEqual, getCurrentSystemMessage, getCurrentTools } from "@earendil-works/pi-ai";
|
|
4
|
-
import { convertResponsesTools } from "
|
|
4
|
+
import { convertResponsesTools } from "./context-management/responses-tools";
|
|
5
5
|
import type {
|
|
6
6
|
BeforeProviderRequestEvent,
|
|
7
7
|
ContextWithSystemEvent,
|
|
@@ -23,8 +23,10 @@ import { rewriteContextNamespaceCalls, rewriteContextNamespaceTools, routeContex
|
|
|
23
23
|
import { rewriteEncryptedToolOutputs } from "./context-management/window-request";
|
|
24
24
|
import { loadHistoryNotesThreadHint } from "./context-management/history-notes";
|
|
25
25
|
import { CodexContextWindowManager } from "./context-management/window-manager";
|
|
26
|
-
import { ManagedManualCompact
|
|
27
|
-
import {
|
|
26
|
+
import { ManagedManualCompact } from "./context-management/manual-compact";
|
|
27
|
+
import { prepareScopedNativeCompaction } from "./context-management/scoped-compaction";
|
|
28
|
+
import type { RemoteWindowCompactionInput } from "./context-management/window-manager";
|
|
29
|
+
import { CONTEXT_WINDOW_COMPACTION_SUMMARY, isSystemTranscriptMessage } from "./context-management/messages";
|
|
28
30
|
import { isRecord } from "./context-management/types";
|
|
29
31
|
import {
|
|
30
32
|
registerContextManagementTools,
|
|
@@ -81,6 +83,8 @@ import {
|
|
|
81
83
|
COMPACTION_EXTENSION_ID,
|
|
82
84
|
LEGACY_NATIVE_COMPACTION_STRATEGY,
|
|
83
85
|
REMOTE_V2_COMPACTION_STRATEGY,
|
|
86
|
+
NATIVE_COMPACTION_FALLBACK_SUMMARY,
|
|
87
|
+
STANDALONE_COMPACTION_FALLBACK_SUMMARY,
|
|
84
88
|
getLatestDeferredToolCarryover,
|
|
85
89
|
getRemoteV2InputProvenance,
|
|
86
90
|
isNativeCompactionDetails,
|
|
@@ -124,7 +128,8 @@ type CompactionDependencies = {
|
|
|
124
128
|
nativeFallback: typeof runNativeFallbackCompaction;
|
|
125
129
|
contextWindows: CodexContextWindowManager;
|
|
126
130
|
manualCompact?: ManagedManualCompact;
|
|
127
|
-
|
|
131
|
+
/** Read-only fence shared by all scoped producer tiers. */
|
|
132
|
+
isCurrentCompaction?: () => boolean;
|
|
128
133
|
/** Test seam for the same ordered projection exposed by the Pi host patch. */
|
|
129
134
|
projectCompactionContext?: CompactionContextProjection;
|
|
130
135
|
piContextHookPatch: PiContextHookPatchResult;
|
|
@@ -440,16 +445,16 @@ function adaptCompactionWireInput(input: ResponsesInputItem[], namespaces: Reado
|
|
|
440
445
|
return adapted.input as ResponsesInputItem[];
|
|
441
446
|
}
|
|
442
447
|
|
|
443
|
-
async function
|
|
448
|
+
async function prepareScopedResponsesCompaction(
|
|
444
449
|
event: SessionBeforeCompactEvent,
|
|
445
450
|
ctx: ExtensionContext,
|
|
446
451
|
config: CompactionConfig,
|
|
447
452
|
execution: RemoteCompactionExecution,
|
|
448
453
|
dependencies: CompactionDependencies,
|
|
449
454
|
isCurrent: () => boolean,
|
|
455
|
+
prepared: RemoteWindowCompactionInput,
|
|
450
456
|
): Promise<ScopedResponsesCompactInput> {
|
|
451
457
|
const windows = dependencies.contextWindows;
|
|
452
|
-
const prepared = windows.prepareRemoteCompaction(event, ctx);
|
|
453
458
|
const branch = ctx.sessionManager.getBranch();
|
|
454
459
|
const provenance = getRemoteV2InputProvenance(config.remoteV2ContextSource);
|
|
455
460
|
let checkpoint: NativeCompactionEntry | undefined;
|
|
@@ -521,7 +526,7 @@ async function prepareActiveRemoteCompaction(
|
|
|
521
526
|
// Current cache-stack state is authoritative; never borrow deferred metadata
|
|
522
527
|
// from an unrelated pre-window checkpoint. The canonical head proves which
|
|
523
528
|
// of those schemas are still live, including tools activated before rollover.
|
|
524
|
-
const activation = getLatestDeferredToolCarryover(branch) ?? checkpoint?.details?.deferredToolCarryover;
|
|
529
|
+
const activation = getLatestDeferredToolCarryover(branch.slice(prepared.windowBoundaryIndex)) ?? checkpoint?.details?.deferredToolCarryover;
|
|
525
530
|
const names = new Set(tools.map((tool) => tool.name));
|
|
526
531
|
const deferredToolCarryover = activation ? { ...activation, toolNames: activation.toolNames.filter((name) => names.has(name)) } : undefined;
|
|
527
532
|
return {
|
|
@@ -542,7 +547,7 @@ async function runResponsesNativeCompact(
|
|
|
542
547
|
projectCompactionContext: CompactionContextProjection | undefined,
|
|
543
548
|
/** Only ordinary compaction supplies this; managed recovery remains V2-only. */
|
|
544
549
|
standaloneCompact?: typeof executeNativeCompaction,
|
|
545
|
-
/**
|
|
550
|
+
/** Window scope is independent of the ordinary producer/fallback policy. */
|
|
546
551
|
scopedInput?: ScopedResponsesCompactInput,
|
|
547
552
|
): Promise<ResponsesCompactOutcome> {
|
|
548
553
|
const { consumer, compactor } = execution;
|
|
@@ -704,7 +709,7 @@ async function runResponsesNativeCompact(
|
|
|
704
709
|
errorType: compactResult.errorType,
|
|
705
710
|
errorParam: compactResult.errorParam,
|
|
706
711
|
errorMessage: compactResult.errorMessage,
|
|
707
|
-
nextStep: tryStandalone ? "standalone-compact" :
|
|
712
|
+
nextStep: tryStandalone ? "standalone-compact" : "native-summary",
|
|
708
713
|
}, config, ctx);
|
|
709
714
|
if (tryStandalone && standaloneCompact) {
|
|
710
715
|
if (event.signal?.aborted) return { outcome: "aborted" };
|
|
@@ -942,68 +947,6 @@ async function runUnavailableContextCompaction(
|
|
|
942
947
|
return { cancel: true };
|
|
943
948
|
}
|
|
944
949
|
|
|
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
|
-
|
|
1007
950
|
async function handleSessionBeforeCompact(
|
|
1008
951
|
event: SessionBeforeCompactEvent,
|
|
1009
952
|
ctx: ExtensionContext,
|
|
@@ -1046,84 +989,58 @@ async function handleSessionBeforeCompact(
|
|
|
1046
989
|
return { cancel: true };
|
|
1047
990
|
}
|
|
1048
991
|
|
|
1049
|
-
//
|
|
1050
|
-
|
|
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
|
-
|
|
1056
|
-
// Active and inactive covered Codex consumers retain their existing routing.
|
|
1057
|
-
const contextModel = config.enabled && config.contextManagement === "remote"
|
|
1058
|
-
? await resolveContextCapabilityModel(ctx)
|
|
1059
|
-
: undefined;
|
|
1060
|
-
if (manualOperation && !dependencies.remoteManualCompact.current(manualOperation, ctx)) {
|
|
1061
|
-
dependencies.remoteManualCompact.cancel(manualOperation);
|
|
1062
|
-
return { cancel: true };
|
|
1063
|
-
}
|
|
1064
|
-
if (isCodexContextModel(contextModel, config)) {
|
|
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) {
|
|
1073
|
-
try {
|
|
1074
|
-
if (event.reason === "manual" && !dependencies.manualCompact?.isMaintenance) {
|
|
1075
|
-
return await runActiveRemoteManualCompact(event, ctx, dependencies, resolved, manualOperation);
|
|
1076
|
-
}
|
|
1077
|
-
return dependencies.contextWindows.prepareCompaction(event);
|
|
1078
|
-
} catch {
|
|
1079
|
-
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
1080
|
-
return { cancel: true };
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
992
|
+
// A rollover trim is owned maintenance, not user /compact.
|
|
993
|
+
if (dependencies.manualCompact?.isMaintenance) return dependencies.contextWindows.prepareCompaction(event);
|
|
1083
994
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
995
|
+
// Preserve the existing automatic Remote Windows protocol. Manual operations
|
|
996
|
+
// select their engine by current policy, never by persisted marker ownership.
|
|
997
|
+
if (event.reason !== "manual" && config.enabled && config.contextManagement === "remote") {
|
|
998
|
+
const contextModel = await resolveContextCapabilityModel(ctx);
|
|
999
|
+
if (isCodexContextModel(contextModel, config)) {
|
|
1000
|
+
if (await remoteContextActive(ctx, config, contextModel)) return dependencies.contextWindows.prepareCompaction(event);
|
|
1001
|
+
return runUnavailableContextCompaction(event, ctx, dependencies, resolved, config, getContextToolRegistrationState());
|
|
1089
1002
|
}
|
|
1090
|
-
return runUnavailableContextCompaction(
|
|
1091
|
-
event,
|
|
1092
|
-
ctx,
|
|
1093
|
-
dependencies,
|
|
1094
|
-
resolved,
|
|
1095
|
-
config,
|
|
1096
|
-
registrationState,
|
|
1097
|
-
);
|
|
1098
1003
|
}
|
|
1099
1004
|
|
|
1100
|
-
dependencies.
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
"compaction-event",
|
|
1113
|
-
{
|
|
1114
|
-
event: "session_before_compact.detached-already-owned",
|
|
1115
|
-
windowId: ownedIdentity.currentWindowId,
|
|
1116
|
-
superseded: dependencies.contextWindows.hasSupersededWindow(),
|
|
1117
|
-
},
|
|
1118
|
-
config,
|
|
1119
|
-
ctx,
|
|
1120
|
-
);
|
|
1121
|
-
}
|
|
1122
|
-
} catch {
|
|
1123
|
-
notifyRemoteContextFailure(ctx, "malformed-window-state");
|
|
1124
|
-
return { cancel: true };
|
|
1005
|
+
const isCurrent = dependencies.isCurrentCompaction ?? (() => !event.signal.aborted);
|
|
1006
|
+
let scope: RemoteWindowCompactionInput | undefined;
|
|
1007
|
+
if (dependencies.contextWindows.currentIdentity()) {
|
|
1008
|
+
scope = dependencies.contextWindows.prepareRemoteCompaction(event, ctx);
|
|
1009
|
+
requireCompleteCompactionToolPairs(scope.messages);
|
|
1010
|
+
event = prepareScopedNativeCompaction(event, scope);
|
|
1011
|
+
const keptIndex = event.branchEntries.findIndex((entry) => entry.id === event.preparation.firstKeptEntryId);
|
|
1012
|
+
scope = { ...scope, firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
1013
|
+
retainedMessages: scope.entries.filter((entry) => entry.sourceEntry.type !== "compaction"
|
|
1014
|
+
&& event.branchEntries.findIndex((raw) => raw.id === entry.sourceEntry.id) >= keptIndex)
|
|
1015
|
+
.flatMap((entry) => entry.messages) };
|
|
1016
|
+
if (!isCurrent()) return { cancel: true };
|
|
1125
1017
|
}
|
|
1126
|
-
|
|
1018
|
+
const nativeScopeSafe = () => {
|
|
1019
|
+
if (scope && !event.preparation.messagesToSummarize.length && !event.preparation.turnPrefixMessages.length && !event.preparation.previousSummary) {
|
|
1020
|
+
notifyWarning(ctx, "Current window has no eligible native summary span; context preserved.");
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
const owner = scope?.owningCompaction;
|
|
1024
|
+
if (!owner) return true;
|
|
1025
|
+
const opaque = owner.summary === NATIVE_COMPACTION_FALLBACK_SUMMARY || owner.summary === STANDALONE_COMPACTION_FALLBACK_SUMMARY
|
|
1026
|
+
|| (isRecord(owner.details) && (owner.details.strategy === REMOTE_V2_COMPACTION_STRATEGY || owner.details.strategy === LEGACY_NATIVE_COMPACTION_STRATEGY));
|
|
1027
|
+
if (!opaque) return true;
|
|
1028
|
+
// Pi's text summarizer cannot interpret a Toolkit encrypted checkpoint.
|
|
1029
|
+
// Replacing it with a summary of its sentinel would silently lose history.
|
|
1030
|
+
notifyWarning(ctx, "Native compression cannot merge the current encrypted checkpoint; context preserved.");
|
|
1031
|
+
return false;
|
|
1032
|
+
};
|
|
1033
|
+
const nativeDefault = async () => {
|
|
1034
|
+
if (!scope) return undefined;
|
|
1035
|
+
if (!nativeScopeSafe()) return { cancel: true };
|
|
1036
|
+
const result = await dependencies.nativeFallback({ ctx, event, config, scoped: true, useCurrentModel: true, isCurrent });
|
|
1037
|
+
if (!isCurrent() || !result.ok) {
|
|
1038
|
+
if (!result.ok) notifyWarning(ctx, `Current-window native compression stopped (${result.reason}); context preserved.`);
|
|
1039
|
+
return { cancel: true };
|
|
1040
|
+
}
|
|
1041
|
+
return { compaction: result.result };
|
|
1042
|
+
};
|
|
1043
|
+
if (!config.enabled || resolved.policy.context.mode !== "remote-compaction") return nativeDefault();
|
|
1127
1044
|
|
|
1128
1045
|
// Resolve producer protocol policy from this operation's document, not a later disk read.
|
|
1129
1046
|
if (resolved.format === "v2" && config.remoteCompactModel) {
|
|
@@ -1145,7 +1062,9 @@ async function handleSessionBeforeCompact(
|
|
|
1145
1062
|
},
|
|
1146
1063
|
config.remoteCompactModel,
|
|
1147
1064
|
);
|
|
1065
|
+
if (scope && !isCurrent()) return { cancel: true };
|
|
1148
1066
|
if (resolution.ok) {
|
|
1067
|
+
const scopedInput = scope ? await prepareScopedResponsesCompaction(event, ctx, config, resolution.execution, dependencies, isCurrent, scope) : undefined;
|
|
1149
1068
|
remoteAttempted = true;
|
|
1150
1069
|
const responsesOutcome = await runResponsesNativeCompact(
|
|
1151
1070
|
event,
|
|
@@ -1155,6 +1074,7 @@ async function handleSessionBeforeCompact(
|
|
|
1155
1074
|
dependencies.remoteCompact,
|
|
1156
1075
|
dependencies.projectCompactionContext,
|
|
1157
1076
|
dependencies.standaloneCompact,
|
|
1077
|
+
scopedInput,
|
|
1158
1078
|
);
|
|
1159
1079
|
if (responsesOutcome.outcome === "success") {
|
|
1160
1080
|
return { compaction: responsesOutcome.compaction };
|
|
@@ -1232,7 +1152,9 @@ async function handleSessionBeforeCompact(
|
|
|
1232
1152
|
// Branch 2: run pi's native compaction method. A failed remote request is compacted by the
|
|
1233
1153
|
// remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
|
|
1234
1154
|
const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
|
|
1235
|
-
|
|
1155
|
+
if (scope && !nativeScopeSafe()) return { cancel: true };
|
|
1156
|
+
const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec, ...(scope ? { scoped: true, isCurrent } : {}) });
|
|
1157
|
+
if (scope && !isCurrent()) return { cancel: true };
|
|
1236
1158
|
if (fallback.ok) {
|
|
1237
1159
|
if (ctx.hasUI) {
|
|
1238
1160
|
ctx.ui.notify(
|
|
@@ -1283,8 +1205,9 @@ async function handleSessionBeforeCompact(
|
|
|
1283
1205
|
return { cancel: true };
|
|
1284
1206
|
}
|
|
1285
1207
|
|
|
1286
|
-
//
|
|
1287
|
-
|
|
1208
|
+
// An unscoped undefined handoff would reopen archived windows. A scoped
|
|
1209
|
+
// current-model selection uses the public native primitive instead.
|
|
1210
|
+
return nativeDefault();
|
|
1288
1211
|
}
|
|
1289
1212
|
|
|
1290
1213
|
function rejectInheritedCheckpointReplay(
|
|
@@ -1321,6 +1244,11 @@ async function handleContextInternal(
|
|
|
1321
1244
|
return undefined;
|
|
1322
1245
|
}
|
|
1323
1246
|
|
|
1247
|
+
// A synthetic compression projection is read-only in EVERY mode. The
|
|
1248
|
+
// caller performs the sole retained-copy match after all ordered hooks;
|
|
1249
|
+
// ordinary replay here would remove the same copies a second time.
|
|
1250
|
+
if (contextWindows.isPreparingCompaction) return { messages: contextWindows.projectCompactionContext(event.messages) };
|
|
1251
|
+
|
|
1324
1252
|
let localMessages: AgentMessage[] | undefined;
|
|
1325
1253
|
let remoteReplayProjection = false;
|
|
1326
1254
|
// Disabled and intentionally downgraded runtimes never enter opaque replay,
|
|
@@ -1335,11 +1263,6 @@ async function handleContextInternal(
|
|
|
1335
1263
|
// The latter still contains retired remote windows and can be inflated by
|
|
1336
1264
|
// an upstream error even though this request only carries the live window.
|
|
1337
1265
|
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
|
-
}
|
|
1343
1266
|
// Projection may observe the exact queued rollover before Pi persists
|
|
1344
1267
|
// it. Decide replay ownership from this request, not the old branch.
|
|
1345
1268
|
const ownsWindow = contextWindows.hasEffectiveWindow() || !contextWindows.currentIdentity();
|
|
@@ -1710,8 +1633,9 @@ export default function registerCompactionExtension(
|
|
|
1710
1633
|
const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager((ctx, signal, gatewayModels) =>
|
|
1711
1634
|
loadHistoryNotesThreadHint(ctx, signal, gatewayModels)
|
|
1712
1635
|
);
|
|
1713
|
-
const manualCompact = new ManagedManualCompact(pi
|
|
1714
|
-
|
|
1636
|
+
const manualCompact = new ManagedManualCompact(pi);
|
|
1637
|
+
let compactionGeneration = 0;
|
|
1638
|
+
const invalidateCompaction = () => { compactionGeneration++; };
|
|
1715
1639
|
const dependencies: CompactionDependencies = {
|
|
1716
1640
|
remoteCompact: executeRemoteV2Compaction,
|
|
1717
1641
|
standaloneCompact: executeNativeCompaction,
|
|
@@ -1719,8 +1643,7 @@ export default function registerCompactionExtension(
|
|
|
1719
1643
|
contextWindows,
|
|
1720
1644
|
...overrides,
|
|
1721
1645
|
manualCompact,
|
|
1722
|
-
|
|
1723
|
-
loadConfig: () => remoteManualCompact.snapshot ?? manualCompact.snapshot ?? loadConfig(),
|
|
1646
|
+
loadConfig,
|
|
1724
1647
|
piContextHookPatch: overrides.piContextHookPatch ?? piContextHookPatch,
|
|
1725
1648
|
};
|
|
1726
1649
|
let tools!: ReturnType<typeof registerContextManagementTools>;
|
|
@@ -1779,7 +1702,7 @@ export default function registerCompactionExtension(
|
|
|
1779
1702
|
}
|
|
1780
1703
|
};
|
|
1781
1704
|
pi.on("session_start", async (_event, ctx) => {
|
|
1782
|
-
|
|
1705
|
+
invalidateCompaction();
|
|
1783
1706
|
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
1784
1707
|
const syncOutcome = await syncTools(ctx, ctx.model, { notifyWindowFailure: true }, resolved);
|
|
1785
1708
|
await manualCompact.recover(ctx);
|
|
@@ -1850,9 +1773,26 @@ export default function registerCompactionExtension(
|
|
|
1850
1773
|
return handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive, getContextToolRegistrationState);
|
|
1851
1774
|
});
|
|
1852
1775
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
1853
|
-
|
|
1776
|
+
invalidateCompaction();
|
|
1777
|
+
const generation = compactionGeneration;
|
|
1778
|
+
const branch = structuredClone(event.branchEntries);
|
|
1779
|
+
const sessionId = getSessionId(ctx);
|
|
1780
|
+
const model = ctx.model ? { ...ctx.model } : undefined;
|
|
1781
|
+
const thinking = ctx.thinkingLevel;
|
|
1782
|
+
const isCurrent = () => generation === compactionGeneration && !event.signal.aborted
|
|
1783
|
+
&& getSessionId(ctx) === sessionId && isDeepStrictEqual(branch, ctx.sessionManager.getBranch())
|
|
1784
|
+
&& isDeepStrictEqual(model, ctx.model)
|
|
1785
|
+
&& thinking === ctx.thinkingLevel
|
|
1786
|
+
&& !(event.reason === "manual" && typeof ctx.hasPendingMessages === "function" && ctx.hasPendingMessages());
|
|
1854
1787
|
try {
|
|
1855
|
-
|
|
1788
|
+
const result = await handleSessionBeforeCompact(event, ctx, { ...dependencies, isCurrentCompaction: isCurrent }, remoteContextActive, getContextToolRegistrationState);
|
|
1789
|
+
if (contextWindows.currentIdentity() && !manualCompact.isMaintenance && result && "compaction" in result && result.compaction
|
|
1790
|
+
&& result.compaction.summary !== CONTEXT_WINDOW_COMPACTION_SUMMARY) {
|
|
1791
|
+
// Pi consumes this after every later before-compact observer. Reject
|
|
1792
|
+
// stale work then, without broad aborts that can drain host queues.
|
|
1793
|
+
return { compaction: result.compaction, get cancel() { return !isCurrent(); } };
|
|
1794
|
+
}
|
|
1795
|
+
return result;
|
|
1856
1796
|
} catch {
|
|
1857
1797
|
// Pi treats a rejected hook as absent and can invoke its implicit summary.
|
|
1858
1798
|
// Unexpected preparation/service failures must never authorize that work.
|
|
@@ -1862,15 +1802,11 @@ export default function registerCompactionExtension(
|
|
|
1862
1802
|
});
|
|
1863
1803
|
pi.on("session_compact", (event, ctx) => {
|
|
1864
1804
|
contextWindows.synchronize(ctx);
|
|
1865
|
-
|
|
1866
|
-
else remoteManualCompact.invalidate();
|
|
1867
|
-
});
|
|
1868
|
-
pi.on("session_compact_failed", async (event, ctx) => {
|
|
1869
|
-
remoteManualCompact.invalidate();
|
|
1870
|
-
if (event.reason === "manual" && !manualCompact.isMaintenance) await manualCompact.compactFailed(ctx);
|
|
1805
|
+
invalidateCompaction();
|
|
1871
1806
|
});
|
|
1807
|
+
pi.on("session_compact_failed", invalidateCompaction);
|
|
1872
1808
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1873
|
-
|
|
1809
|
+
invalidateCompaction();
|
|
1874
1810
|
await manualCompact.shutdown(ctx);
|
|
1875
1811
|
contextWindowReady = false;
|
|
1876
1812
|
contextWindows.reset();
|
|
@@ -1882,7 +1818,7 @@ export default function registerCompactionExtension(
|
|
|
1882
1818
|
await manualCompact.settled(ctx);
|
|
1883
1819
|
});
|
|
1884
1820
|
pi.on("model_select", async (event, ctx) => {
|
|
1885
|
-
|
|
1821
|
+
invalidateCompaction();
|
|
1886
1822
|
manualCompact.modelSelected(event.model, ctx);
|
|
1887
1823
|
// Switching into a covered model mid-session must open the window
|
|
1888
1824
|
// lifecycle immediately: without an identity the request rewrite skips
|
|
@@ -1893,29 +1829,26 @@ export default function registerCompactionExtension(
|
|
|
1893
1829
|
await syncTools(ctx, event.model, { notifyWindowFailure: true }, resolved);
|
|
1894
1830
|
});
|
|
1895
1831
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
1896
|
-
|
|
1832
|
+
invalidateCompaction();
|
|
1897
1833
|
await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
|
|
1898
1834
|
});
|
|
1899
|
-
pi.on("agent_start", () =>
|
|
1900
|
-
pi.on("input", () => {
|
|
1835
|
+
pi.on("agent_start", () => invalidateCompaction());
|
|
1836
|
+
pi.on("input", () => { invalidateCompaction(); });
|
|
1901
1837
|
pi.on("thinking_level_select", (event, ctx) => {
|
|
1902
|
-
|
|
1838
|
+
invalidateCompaction();
|
|
1903
1839
|
manualCompact.thinkingSelected(event.level, ctx);
|
|
1904
1840
|
});
|
|
1905
|
-
pi.on("message_start",
|
|
1906
|
-
remoteManualCompact.invalidate();
|
|
1907
|
-
manualCompact.messageStarted(event.message);
|
|
1908
|
-
});
|
|
1841
|
+
pi.on("message_start", invalidateCompaction);
|
|
1909
1842
|
pi.on("turn_end", (_event, ctx) => manualCompact.turnEnded(ctx));
|
|
1910
1843
|
pi.on("tool_call", (event) => {
|
|
1911
|
-
|
|
1844
|
+
invalidateCompaction();
|
|
1912
1845
|
return manualCompact.guardTool(event.toolName);
|
|
1913
1846
|
});
|
|
1914
|
-
pi.on("session_before_switch", () =>
|
|
1915
|
-
pi.on("session_before_fork", () =>
|
|
1916
|
-
pi.on("session_before_tree", () =>
|
|
1847
|
+
pi.on("session_before_switch", () => invalidateCompaction());
|
|
1848
|
+
pi.on("session_before_fork", () => invalidateCompaction());
|
|
1849
|
+
pi.on("session_before_tree", () => invalidateCompaction());
|
|
1917
1850
|
pi.on("session_tree", async (_event, ctx) => {
|
|
1918
|
-
|
|
1851
|
+
invalidateCompaction();
|
|
1919
1852
|
contextWindows.synchronize(ctx);
|
|
1920
1853
|
await manualCompact.recover(ctx);
|
|
1921
1854
|
await syncTools(ctx);
|
package/src/native-fallback.ts
CHANGED
|
@@ -20,7 +20,9 @@ export type NativeFallbackFailureReason =
|
|
|
20
20
|
| "auth-failed"
|
|
21
21
|
| "aborted"
|
|
22
22
|
| "empty-summary"
|
|
23
|
-
| "compact-failed"
|
|
23
|
+
| "compact-failed"
|
|
24
|
+
| "public-runtime-unavailable"
|
|
25
|
+
| "missing-model";
|
|
24
26
|
|
|
25
27
|
export type NativeFallbackResult =
|
|
26
28
|
| {
|
|
@@ -39,7 +41,7 @@ export type NativeFallbackResult =
|
|
|
39
41
|
export type NativeCompactFn = typeof compact;
|
|
40
42
|
|
|
41
43
|
type ResolvedAuth =
|
|
42
|
-
| { ok: true; apiKey?: string; headers?: ProviderHeaders; env?: Record<string, string> }
|
|
44
|
+
| { ok: true; apiKey?: string; headers?: ProviderHeaders; baseUrl?: string; env?: Record<string, string> }
|
|
43
45
|
| { ok: false; error: string };
|
|
44
46
|
|
|
45
47
|
function isAbortError(error: unknown): boolean {
|
|
@@ -61,10 +63,10 @@ function toErrorMessage(error: unknown): string {
|
|
|
61
63
|
* active model cannot use remote v2 at all. This function never inspects `remoteCompactModel`
|
|
62
64
|
* itself, so a model that cannot compact remotely can never inherit the remote producer.
|
|
63
65
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* Ordinary no-boundary defaults stay with Pi. When a window constrains input,
|
|
67
|
+
* useCurrentModel runs the same public native primitive with the public registry
|
|
68
|
+
* stream, current thinking and auth, rather than reopening unscoped host history.
|
|
69
|
+
* Host-private custom stream/retry callbacks are not exposed by ExtensionContext.
|
|
68
70
|
*/
|
|
69
71
|
export async function runNativeFallbackCompaction(args: {
|
|
70
72
|
ctx: ExtensionContext;
|
|
@@ -73,33 +75,33 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
73
75
|
/** Model spec supplied by the caller for this specific fallback reason. */
|
|
74
76
|
modelSpec?: string;
|
|
75
77
|
compactFn?: NativeCompactFn;
|
|
78
|
+
/** Scoped input cannot be handed back to an unscoped host default. */
|
|
79
|
+
scoped?: boolean;
|
|
80
|
+
/** Run current-model native policy, independently of Toolkit's fallback config. */
|
|
81
|
+
useCurrentModel?: boolean;
|
|
82
|
+
isCurrent?: () => boolean;
|
|
76
83
|
}): Promise<NativeFallbackResult> {
|
|
77
84
|
const { ctx, event, config } = args;
|
|
78
85
|
const compactFn = args.compactFn ?? compact;
|
|
79
86
|
const nativeFallback = config.nativeFallback;
|
|
87
|
+
const isCurrent = () => !event.signal.aborted && (args.isCurrent?.() ?? true);
|
|
88
|
+
if (!isCurrent()) return { ok: false, reason: "aborted" };
|
|
80
89
|
|
|
81
|
-
if (!nativeFallback.enabled) {
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const spec = args.modelSpec?.trim() ?? "";
|
|
86
|
-
if (!spec) {
|
|
87
|
-
return { ok: false, reason: "no-model-configured" };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
+
if (!args.useCurrentModel && !nativeFallback.enabled) return { ok: false, reason: "disabled" };
|
|
91
|
+
const spec = args.useCurrentModel ? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "") : args.modelSpec?.trim() ?? "";
|
|
92
|
+
if (!spec) return { ok: false, reason: args.useCurrentModel ? "missing-model" : "no-model-configured" };
|
|
90
93
|
const parsed = parseModelSpec(spec);
|
|
91
|
-
if (!parsed) {
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
96
|
-
if (!model) {
|
|
97
|
-
return { ok: false, reason: "model-not-found", modelSpec: spec };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (ctx.model && ctx.model.provider === model.provider && ctx.model.id === model.id) {
|
|
94
|
+
if (!parsed) return { ok: false, reason: "invalid-model-spec", modelSpec: spec };
|
|
95
|
+
const model = args.useCurrentModel ? ctx.model : ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
96
|
+
if (!model) return { ok: false, reason: "model-not-found", modelSpec: spec };
|
|
97
|
+
if (!args.useCurrentModel && ctx.model && ctx.model.provider === model.provider && ctx.model.id === model.id) {
|
|
101
98
|
return { ok: false, reason: "same-as-current-model", modelSpec: spec };
|
|
102
99
|
}
|
|
100
|
+
// The public registry preserves provider-native stream/auth wiring. Do not
|
|
101
|
+
// silently use a compat-only transport for scoped current-model compression.
|
|
102
|
+
if (args.scoped && typeof ctx.modelRegistry.streamSimple !== "function" && !args.compactFn) {
|
|
103
|
+
return { ok: false, reason: "public-runtime-unavailable", modelSpec: spec };
|
|
104
|
+
}
|
|
103
105
|
|
|
104
106
|
// Pi owns prompt construction, tool-result truncation and split-turn requests.
|
|
105
107
|
// A chars/4 estimate is not target-model token accounting and must not veto
|
|
@@ -115,20 +117,21 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
115
117
|
return { ok: false, reason: "auth-failed", modelSpec: spec, errorMessage: auth.error };
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
if (!isCurrent()) return { ok: false, reason: "aborted", modelSpec: spec };
|
|
118
121
|
try {
|
|
119
122
|
const result = await compactFn(
|
|
120
123
|
event.preparation,
|
|
121
|
-
model,
|
|
124
|
+
auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model,
|
|
122
125
|
auth.apiKey,
|
|
123
126
|
mergeProviderHeaders(auth.headers),
|
|
124
127
|
event.customInstructions,
|
|
125
128
|
event.signal,
|
|
126
|
-
nativeFallback.thinkingLevel,
|
|
127
|
-
undefined,
|
|
129
|
+
args.useCurrentModel ? ctx.thinkingLevel : nativeFallback.thinkingLevel,
|
|
130
|
+
args.scoped && typeof ctx.modelRegistry.streamSimple === "function" ? ctx.modelRegistry.streamSimple.bind(ctx.modelRegistry) : undefined,
|
|
128
131
|
auth.env,
|
|
129
132
|
);
|
|
130
133
|
|
|
131
|
-
if (
|
|
134
|
+
if (!isCurrent()) {
|
|
132
135
|
return { ok: false, reason: "aborted", modelSpec: spec };
|
|
133
136
|
}
|
|
134
137
|
if (!result.summary || result.summary.trim().length === 0) {
|