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.
@@ -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 "@earendil-works/pi-ai/api/openai-responses-shared";
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, RemoteManualCompact, type RemoteManualCompactOperation } from "./context-management/manual-compact";
27
- import { isSystemTranscriptMessage } from "./context-management/messages";
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
- remoteManualCompact: RemoteManualCompact;
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 prepareActiveRemoteCompaction(
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
- /** Active Remote manual input is strict/current-window-only and has no fallback. */
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" : scopedInput ? "stop-and-preserve" : "native-summary",
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
- // 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
-
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
- dependencies.remoteManualCompact.cancel(manualOperation);
1085
- const registrationState = getContextToolRegistrationState();
1086
- const failureReason = contextManagementFailureReason(registrationState);
1087
- if (failureReason !== "context-tools-excluded") {
1088
- notifyRemoteContextFailure(ctx, failureReason);
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.remoteManualCompact.cancel(manualOperation);
1101
- try {
1102
- const detached = dependencies.contextWindows.prepareDetachedCompaction(event);
1103
- // A local consumer cannot summarize history owned by a Remote window,
1104
- // even after switching modes or disabling Toolkit compaction.
1105
- if (detached) return detached;
1106
- // A boundary already persisted by an earlier detach still owns this session.
1107
- // Retired Remote history is outside Pi's effective projection at that point, so
1108
- // ordinary compaction may continue; record why it no longer detaches.
1109
- const ownedIdentity = dependencies.contextWindows.currentIdentity();
1110
- if (ownedIdentity) {
1111
- writeDebugArtifact(
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
- if (!config.enabled) return undefined;
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
- const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec });
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
- // Branch 3: pi's default native compaction with the current model.
1287
- return undefined;
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, contextWindows);
1714
- const remoteManualCompact = new RemoteManualCompact(pi, contextWindows);
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
- remoteManualCompact,
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
- remoteManualCompact.invalidate();
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
- remoteManualCompact.invalidate();
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
- return await handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive, getContextToolRegistrationState);
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
- if (event.reason === "manual" && event.fromExtension) remoteManualCompact.persisted(ctx);
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
- remoteManualCompact.invalidate();
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
- remoteManualCompact.invalidate();
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
- remoteManualCompact.invalidate();
1832
+ invalidateCompaction();
1897
1833
  await syncTools(ctx, ctx.model, { notifyWindowFailure: true });
1898
1834
  });
1899
- pi.on("agent_start", () => remoteManualCompact.invalidate());
1900
- pi.on("input", () => { remoteManualCompact.invalidate(); });
1835
+ pi.on("agent_start", () => invalidateCompaction());
1836
+ pi.on("input", () => { invalidateCompaction(); });
1901
1837
  pi.on("thinking_level_select", (event, ctx) => {
1902
- remoteManualCompact.invalidate();
1838
+ invalidateCompaction();
1903
1839
  manualCompact.thinkingSelected(event.level, ctx);
1904
1840
  });
1905
- pi.on("message_start", (event) => {
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
- remoteManualCompact.invalidate();
1844
+ invalidateCompaction();
1912
1845
  return manualCompact.guardTool(event.toolName);
1913
1846
  });
1914
- pi.on("session_before_switch", () => remoteManualCompact.invalidate());
1915
- pi.on("session_before_fork", () => remoteManualCompact.invalidate());
1916
- pi.on("session_before_tree", () => remoteManualCompact.invalidate());
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
- remoteManualCompact.invalidate();
1851
+ invalidateCompaction();
1919
1852
  contextWindows.synchronize(ctx);
1920
1853
  await manualCompact.recover(ctx);
1921
1854
  await syncTools(ctx);
@@ -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
- * Only the "resolved model differs from the current one" case is handled here: when the feature
65
- * is disabled, nothing is resolved, or it equals the current model, the caller returns undefined
66
- * from session_before_compact so pi runs the same native path itself, keeping its internal
67
- * streamFn/thinkingLevel wiring.
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
- return { ok: false, reason: "disabled" };
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
- return { ok: false, reason: "invalid-model-spec", modelSpec: spec };
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 (event.signal.aborted) {
134
+ if (!isCurrent()) {
132
135
  return { ok: false, reason: "aborted", modelSpec: spec };
133
136
  }
134
137
  if (!result.summary || result.summary.trim().length === 0) {