pi-openai-toolkit 0.12.0 → 0.13.0

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.
@@ -7,6 +7,15 @@ import type {
7
7
  SessionBeforeCompactEvent,
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import { loadToolkitConfig } from "./config";
10
+ import {
11
+ codexContextProviderHeaders,
12
+ resolveCodexContextProvider,
13
+ isCodexGatewayModel,
14
+ isNativeCodexModel,
15
+ } from "./context-management/codex-provider";
16
+ import { routeContextNamespaceToolMessage } from "./context-management/namespace-tools";
17
+ import { CodexContextWindowManager } from "./context-management/window-manager";
18
+ import { registerContextManagementTools } from "./context-management/tools";
10
19
  import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
11
20
  import { resolveLatestNativeCompactionEntry } from "./details-store";
12
21
  import { runNativeFallbackCompaction } from "./native-fallback";
@@ -38,8 +47,15 @@ type CompactionDependencies = {
38
47
  loadConfig: typeof loadToolkitConfig;
39
48
  remoteCompact: typeof executeRemoteV2Compaction;
40
49
  nativeFallback: typeof runNativeFallbackCompaction;
50
+ contextWindows: CodexContextWindowManager;
41
51
  };
42
52
 
53
+ type RemoteContextActive = (
54
+ ctx: ExtensionContext,
55
+ config: CompactionConfig,
56
+ model?: ExtensionContext["model"],
57
+ ) => Promise<boolean>;
58
+
43
59
  type ResponsesCompactOutcome =
44
60
  | { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
45
61
  | { outcome: "aborted" }
@@ -87,6 +103,29 @@ function notifyWarning(ctx: ExtensionContext, message: string): void {
87
103
  }
88
104
  }
89
105
 
106
+ async function isRemoteContextActive(
107
+ ctx: ExtensionContext,
108
+ config: CompactionConfig,
109
+ model: ExtensionContext["model"] = ctx.model,
110
+ ): Promise<boolean> {
111
+ if (!config.enabled || config.contextManagement !== "remote") return false;
112
+ const resolution = await resolveCodexContextProvider(ctx, model, config.codexGatewayModels);
113
+ return resolution.ok;
114
+ }
115
+
116
+ function isCodexContextModel(
117
+ model: ExtensionContext["model"] | undefined,
118
+ config: CompactionConfig,
119
+ ): boolean {
120
+ return isNativeCodexModel(model) || isCodexGatewayModel(model, config.codexGatewayModels);
121
+ }
122
+
123
+ function notifyRemoteContextFailure(ctx: ExtensionContext, reason: string): void {
124
+ if (ctx.hasUI) {
125
+ ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: Remote Context management inactive (${reason})`, "warning");
126
+ }
127
+ }
128
+
90
129
  function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
91
130
  return window.map((item) => structuredClone(item));
92
131
  }
@@ -299,6 +338,7 @@ async function handleSessionBeforeCompact(
299
338
  event: SessionBeforeCompactEvent,
300
339
  ctx: ExtensionContext,
301
340
  dependencies: CompactionDependencies,
341
+ remoteContextActive: RemoteContextActive,
302
342
  ) {
303
343
  const { config: toolkitConfig } = dependencies.loadConfig();
304
344
  const config = toolkitConfig.compaction;
@@ -327,6 +367,28 @@ async function handleSessionBeforeCompact(
327
367
  return { cancel: true };
328
368
  }
329
369
 
370
+ // Remote Context management owns this eligible Codex session. It persists a
371
+ // no-summary boundary and deliberately never calls remote_compaction_v2.
372
+ if (config.contextManagement === "remote" && isCodexContextModel(ctx.model, config)) {
373
+ if (await remoteContextActive(ctx, config)) {
374
+ try {
375
+ dependencies.contextWindows.synchronize(ctx);
376
+ return dependencies.contextWindows.prepareCompaction(event);
377
+ } catch {
378
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
379
+ return { cancel: true };
380
+ }
381
+ }
382
+ // A configured native Codex Remote session must never re-enter this
383
+ // extension's Remote V2 compaction chain, and Pi's native compaction is
384
+ // deliberately disabled for Remote-managed models: cancelling here keeps
385
+ // the boundary the only rollover mechanism. The inactive reason is
386
+ // surfaced so the user can fix activation instead of losing context to a
387
+ // silent native summary.
388
+ notifyRemoteContextFailure(ctx, "native-codex-context-unavailable");
389
+ return { cancel: true };
390
+ }
391
+
330
392
  // Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
331
393
  let remoteAttempted = false;
332
394
  const resolution = await resolveRemoteCompactionExecution(
@@ -426,11 +488,67 @@ async function handleSessionBeforeCompact(
426
488
  return undefined;
427
489
  }
428
490
 
429
- async function handleContext(event: ContextEvent, ctx: ExtensionContext, loadConfig: typeof loadToolkitConfig) {
491
+ async function handleContext(
492
+ event: ContextEvent,
493
+ ctx: ExtensionContext,
494
+ pi: ExtensionAPI,
495
+ loadConfig: typeof loadToolkitConfig,
496
+ contextWindows: CodexContextWindowManager,
497
+ remoteContextActive: RemoteContextActive,
498
+ ) {
430
499
  const { config: { compaction: config } } = loadConfig();
431
- if (!config.enabled) return undefined;
432
- // Resolve the effective authenticated endpoint before dropping any history.
433
- // A checkpoint for a different endpoint must keep Pi's original context.
500
+ if (!config.enabled) {
501
+ const visibleMessages = contextWindows.project(event.messages, "off");
502
+ return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
503
+ ? undefined
504
+ : { messages: visibleMessages };
505
+ }
506
+
507
+ if (config.contextManagement === "remote") {
508
+ try {
509
+ contextWindows.synchronize(ctx);
510
+ } catch {
511
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
512
+ ctx.abort();
513
+ return undefined;
514
+ }
515
+
516
+ const remoteActive = await remoteContextActive(ctx, config);
517
+ if (remoteActive) {
518
+ try {
519
+ contextWindows.recordBudget(
520
+ pi,
521
+ ctx,
522
+ true,
523
+ config.contextReminderThresholdPercent,
524
+ );
525
+ const projected = contextWindows.project(event.messages, "remote");
526
+ return projected.length === event.messages.length && projected.every((message, index) => message === event.messages[index])
527
+ ? undefined
528
+ : { messages: projected };
529
+ } catch (error) {
530
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
531
+ ctx.abort();
532
+ return undefined;
533
+ }
534
+ }
535
+
536
+ // An eligible Codex Remote session is never allowed to fall through to the
537
+ // older compaction-replay path. Keep its internal markers hidden, but let
538
+ // Pi's normal request construction handle the inactive session.
539
+ if (isCodexContextModel(ctx.model, config)) {
540
+ const visibleMessages = contextWindows.project(event.messages, "off");
541
+ return visibleMessages.length === event.messages.length && visibleMessages.every((message, index) => message === event.messages[index])
542
+ ? undefined
543
+ : { messages: visibleMessages };
544
+ }
545
+ }
546
+
547
+ // Inactive/ineligible Remote mode must not expose internal window markers to a
548
+ // gateway or another provider. The existing compaction replay path remains the
549
+ // owner for this safe fallback.
550
+ const visibleMessages = contextWindows.project(event.messages, "off");
551
+ const replayEvent = visibleMessages === event.messages ? event : { ...event, messages: visibleMessages };
434
552
  const resolution = await resolveNativeCompactionEnvironment(ctx, {
435
553
  enabled: config.enabled,
436
554
  responsesApis: config.responsesApis,
@@ -439,20 +557,22 @@ async function handleContext(event: ContextEvent, ctx: ExtensionContext, loadCon
439
557
  const branchEntries = ctx.sessionManager.getBranch();
440
558
  const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
441
559
  if (!latest.ok) return undefined;
442
- const result = removeNativeCompactionRetainedMessages({ messages: event.messages, branchEntries, compactionEntry: latest.entry });
560
+ const result = removeNativeCompactionRetainedMessages({ messages: replayEvent.messages, branchEntries, compactionEntry: latest.entry });
443
561
  if (!result.ok) {
444
562
  writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
445
563
  if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
446
564
  ctx.abort();
447
565
  return undefined;
448
566
  }
449
- return result.messages === event.messages ? undefined : { messages: result.messages };
567
+ return result.messages === replayEvent.messages ? undefined : { messages: result.messages };
450
568
  }
451
569
 
452
570
  async function handleBeforeProviderRequest(
453
571
  event: BeforeProviderRequestEvent,
454
572
  ctx: ExtensionContext,
455
573
  loadConfig: typeof loadToolkitConfig,
574
+ contextWindows: CodexContextWindowManager,
575
+ remoteContextActive: RemoteContextActive,
456
576
  ) {
457
577
  const { config: toolkitConfig } = loadConfig();
458
578
  const config = toolkitConfig.compaction;
@@ -460,6 +580,30 @@ async function handleBeforeProviderRequest(
460
580
  return undefined;
461
581
  }
462
582
 
583
+ if (config.contextManagement === "remote") {
584
+ try {
585
+ contextWindows.synchronize(ctx);
586
+ } catch {
587
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
588
+ ctx.abort();
589
+ return undefined;
590
+ }
591
+ }
592
+ if (config.contextManagement === "remote" && await remoteContextActive(ctx, config)) {
593
+ try {
594
+ return contextWindows.rewritePayload(event.payload, ctx);
595
+ } catch {
596
+ notifyRemoteContextFailure(ctx, "malformed-request-state");
597
+ ctx.abort();
598
+ return undefined;
599
+ }
600
+ }
601
+ if (config.contextManagement === "remote" && isCodexContextModel(ctx.model, config)) {
602
+ // Keep Codex Remote mutually exclusive with the legacy replay
603
+ // pipeline when authentication or tool ownership is unavailable.
604
+ return undefined;
605
+ }
606
+
463
607
  const resolution = await resolveNativeCompactionEnvironment(
464
608
  ctx,
465
609
  {
@@ -613,17 +757,59 @@ export default function registerCompactionExtension(
613
757
  pi: ExtensionAPI,
614
758
  overrides: Partial<CompactionDependencies> = {},
615
759
  ) {
760
+ const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
761
+ const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager(
762
+ undefined,
763
+ () => loadConfig().config.compaction.codexGatewayModels,
764
+ );
616
765
  const dependencies: CompactionDependencies = {
617
- loadConfig: loadToolkitConfig,
766
+ loadConfig,
618
767
  remoteCompact: executeRemoteV2Compaction,
619
768
  nativeFallback: runNativeFallbackCompaction,
769
+ contextWindows,
620
770
  ...overrides,
621
771
  };
622
- pi.on("session_start", (_event, ctx) => {
772
+ let tools!: ReturnType<typeof registerContextManagementTools>;
773
+ tools = registerContextManagementTools(
774
+ pi,
775
+ contextWindows,
776
+ async (ctx) => {
777
+ const config = dependencies.loadConfig().config.compaction;
778
+ return tools.isRegistered && await isRemoteContextActive(ctx, config);
779
+ },
780
+ () => dependencies.loadConfig().config.compaction.codexGatewayModels,
781
+ );
782
+ const remoteContextActive: RemoteContextActive = async (ctx, config, model = ctx.model) =>
783
+ tools.isRegistered && await isRemoteContextActive(ctx, config, model);
784
+ const syncTools = async (ctx: ExtensionContext, model = ctx.model): Promise<boolean> => {
785
+ const config = dependencies.loadConfig().config.compaction;
786
+ const active = await isRemoteContextActive(ctx, config, model);
787
+ return tools.sync(active);
788
+ };
789
+ pi.on("session_start", async (_event, ctx) => {
790
+ const active = await syncTools(ctx);
623
791
  const { config: toolkitConfig, source, warnings } = dependencies.loadConfig();
624
792
  const config = toolkitConfig.compaction;
625
793
  if (!config.enabled) return;
626
794
 
795
+ let activationReason: string | undefined;
796
+ if (config.contextManagement === "remote") {
797
+ if (active) {
798
+ try {
799
+ contextWindows.ensureInitialized(pi, ctx, true);
800
+ } catch {
801
+ notifyRemoteContextFailure(ctx, "malformed-window-state");
802
+ }
803
+ } else if (!tools.isRegistered) {
804
+ activationReason = "tool-name-conflict";
805
+ notifyRemoteContextFailure(ctx, activationReason);
806
+ } else {
807
+ const remoteResolution = await resolveCodexContextProvider(ctx, ctx.model, config.codexGatewayModels);
808
+ activationReason = remoteResolution.ok ? "codex-context-unavailable" : remoteResolution.reason;
809
+ notifyRemoteContextFailure(ctx, activationReason);
810
+ }
811
+ }
812
+
627
813
  if (warnings.length > 0 && ctx.hasUI && config.debug) {
628
814
  ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${warnings[0]}`, "warning");
629
815
  }
@@ -635,6 +821,12 @@ export default function registerCompactionExtension(
635
821
  config,
636
822
  configSource: source,
637
823
  warnings,
824
+ activation: {
825
+ active,
826
+ contextManagement: config.contextManagement,
827
+ model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
828
+ ...(activationReason ? { reason: activationReason } : {}),
829
+ },
638
830
  },
639
831
  config,
640
832
  ctx,
@@ -650,7 +842,57 @@ export default function registerCompactionExtension(
650
842
  }
651
843
  });
652
844
 
653
- pi.on("context", (event, ctx) => handleContext(event, ctx, dependencies.loadConfig));
654
- pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies));
655
- pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, dependencies.loadConfig));
845
+ pi.on("context", (event, ctx) => handleContext(event, ctx, pi, dependencies.loadConfig, contextWindows, remoteContextActive));
846
+ pi.on("session_before_compact", (event, ctx) => handleSessionBeforeCompact(event, ctx, dependencies, remoteContextActive));
847
+ pi.on("session_compact", (event, _ctx) => contextWindows.recordCompaction(event.compactionEntry.details));
848
+ pi.on("session_shutdown", () => {
849
+ contextWindows.reset();
850
+ tools.reset();
851
+ });
852
+ pi.on("model_select", async (event, ctx) => {
853
+ await syncTools(ctx, event.model);
854
+ });
855
+ pi.on("before_agent_start", async (_event, ctx) => {
856
+ await syncTools(ctx);
857
+ });
858
+ pi.on("before_provider_request", (event, ctx) => handleBeforeProviderRequest(event, ctx, dependencies.loadConfig, contextWindows, remoteContextActive));
859
+ pi.on("before_provider_headers", async (event, ctx) => {
860
+ const config = dependencies.loadConfig().config.compaction;
861
+ if (config.contextManagement !== "remote" || !isCodexContextModel(ctx.model, config)) return;
862
+ if (!(await remoteContextActive(ctx, config))) return;
863
+ const provider = await resolveCodexContextProvider(ctx, ctx.model, config.codexGatewayModels);
864
+ if (provider.ok && provider.provider.kind === "codex-gateway") {
865
+ const sessionId = getSessionId(ctx);
866
+ const gatewayHeaders = codexContextProviderHeaders(provider.provider, {
867
+ sessionId,
868
+ clientRequestId: sessionId,
869
+ });
870
+ for (const name of [
871
+ "authorization",
872
+ "originator",
873
+ "user-agent",
874
+ "version",
875
+ "session-id",
876
+ "x-client-request-id",
877
+ "x-codex-affinity-scope",
878
+ "x-codex-model",
879
+ ]) {
880
+ for (const existing of Object.keys(event.headers)) {
881
+ if (existing.toLowerCase() === name) delete event.headers[existing];
882
+ }
883
+ const value = gatewayHeaders.get(name);
884
+ if (value) event.headers[name] = value;
885
+ }
886
+ for (const existing of Object.keys(event.headers)) {
887
+ if (["cookie", "chatgpt-account-id", "x-api-key"].includes(existing.toLowerCase())) delete event.headers[existing];
888
+ }
889
+ }
890
+ contextWindows.rewriteHeaders(event.headers, ctx);
891
+ });
892
+ pi.on("message_end", (event, ctx) => {
893
+ const config = dependencies.loadConfig().config.compaction;
894
+ if (config.contextManagement !== "remote" || !isCodexContextModel(ctx.model, config) || !tools.isRegistered) return undefined;
895
+ const message = routeContextNamespaceToolMessage(event.message);
896
+ return message === event.message ? undefined : { message };
897
+ });
656
898
  }
package/src/types.ts CHANGED
@@ -38,6 +38,8 @@ export type DebugArtifactKind =
38
38
  | "compaction-event"
39
39
  | "lifecycle";
40
40
 
41
+ export type ContextManagementMode = "off" | "remote";
42
+
41
43
  /** Native-method fallback compaction: which model runs pi's compact() and how deeply. */
42
44
  export type NativeFallbackConfig = {
43
45
  enabled: boolean;
@@ -54,6 +56,10 @@ export type NativeFallbackConfig = {
54
56
 
55
57
  export type CompactionConfig = {
56
58
  enabled: boolean;
59
+ /** Optional Codex Remote Context management. Disabled by default for compatibility. */
60
+ contextManagement: ContextManagementMode;
61
+ /** Exact provider/model specs allowed to use the extension's CPA-backed Codex gateway. */
62
+ codexGatewayModels: string[];
57
63
  /**
58
64
  * Allow a Responses session whose latest compaction was not created by this extension
59
65
  * to restart native compaction from Pi's current serialized session context.
@@ -66,8 +72,15 @@ export type CompactionConfig = {
66
72
  remoteCompactModel?: string;
67
73
  /** Native-method fallback compaction policy (non-Responses APIs, or when the compact endpoint fails). */
68
74
  nativeFallback: NativeFallbackConfig;
69
- /** Subset of RESPONSES_COMPACT_CAPABLE_APIS that should use remote compaction. */
75
+ /**
76
+ * Subset of RESPONSES_COMPACT_CAPABLE_APIS that should use remote compaction.
77
+ */
70
78
  responsesApis: string[];
79
+ /**
80
+ * Percentage of the context window that, when remaining tokens drop below it,
81
+ * triggers a checkpoint/new_context reminder (0-100; 0 disables reminders).
82
+ */
83
+ contextReminderThresholdPercent: number;
71
84
  notifyOnLoad: boolean;
72
85
  debug: boolean;
73
86
  logProviderPayloads: boolean;
@@ -491,10 +504,13 @@ export const DEFAULT_NATIVE_FALLBACK_CONFIG: NativeFallbackConfig = {
491
504
 
492
505
  export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
493
506
  enabled: true,
507
+ contextManagement: "off",
508
+ codexGatewayModels: [],
494
509
  allowCompactionContinuityBreak: false,
495
510
  remoteCompactModel: undefined,
496
511
  nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
497
512
  responsesApis: [...RESPONSES_COMPACT_CAPABLE_APIS],
513
+ contextReminderThresholdPercent: 5,
498
514
  notifyOnLoad: false,
499
515
  debug: false,
500
516
  logProviderPayloads: false,