pi-openai-toolkit 0.11.1 → 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.
- package/NOTICE +28 -0
- package/README.md +57 -23
- package/README_zh.md +57 -23
- package/package.json +126 -114
- package/src/config.ts +39 -69
- package/src/context-management/codex-provider.ts +261 -0
- package/src/context-management/history-notes.ts +325 -0
- package/src/context-management/messages.ts +106 -0
- package/src/context-management/namespace-tools.ts +426 -0
- package/src/context-management/tool-contract.ts +23 -0
- package/src/context-management/tools.ts +313 -0
- package/src/context-management/types.ts +160 -0
- package/src/context-management/window-budget.ts +83 -0
- package/src/context-management/window-manager.ts +427 -0
- package/src/context-management/window-request.ts +125 -0
- package/src/debug.ts +3 -3
- package/src/extension-runtime.ts +305 -18
- package/src/payload-rewrite.ts +40 -10
- package/src/remote-v2-client.ts +340 -99
- package/src/request-context-cache.ts +36 -3
- package/src/serializer.ts +4 -12
- package/src/types.ts +16 -19
- package/tsconfig.check.json +0 -1
- package/src/inline-compaction.ts +0 -1264
package/src/extension-runtime.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BeforeProviderRequestEvent,
|
|
3
|
+
ContextEvent,
|
|
3
4
|
CompactionResult,
|
|
4
5
|
ExtensionAPI,
|
|
5
6
|
ExtensionContext,
|
|
6
7
|
SessionBeforeCompactEvent,
|
|
7
8
|
} from "@earendil-works/pi-coding-agent";
|
|
8
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";
|
|
9
19
|
import { writeDebugArtifact, writeReplayFailureArtifact } from "./debug";
|
|
10
20
|
import { resolveLatestNativeCompactionEntry } from "./details-store";
|
|
11
21
|
import { runNativeFallbackCompaction } from "./native-fallback";
|
|
12
22
|
import {
|
|
13
23
|
rewriteResponsesPayloadWithNativeReplay,
|
|
24
|
+
removeNativeCompactionRetainedMessages,
|
|
14
25
|
serializeLiveTailToResponsesInput,
|
|
15
26
|
} from "./payload-rewrite";
|
|
16
27
|
import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
|
|
17
28
|
import { executeRemoteV2Compaction } from "./remote-v2-client";
|
|
18
|
-
import { registerInlineCompactionRuntime } from "./inline-compaction";
|
|
19
29
|
import {
|
|
20
30
|
resolveNativeCompactionEnvironment,
|
|
21
31
|
resolveRemoteCompactionExecution,
|
|
@@ -33,6 +43,19 @@ import {
|
|
|
33
43
|
type NativeCompactionRequestMeta,
|
|
34
44
|
} from "./types";
|
|
35
45
|
|
|
46
|
+
type CompactionDependencies = {
|
|
47
|
+
loadConfig: typeof loadToolkitConfig;
|
|
48
|
+
remoteCompact: typeof executeRemoteV2Compaction;
|
|
49
|
+
nativeFallback: typeof runNativeFallbackCompaction;
|
|
50
|
+
contextWindows: CodexContextWindowManager;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type RemoteContextActive = (
|
|
54
|
+
ctx: ExtensionContext,
|
|
55
|
+
config: CompactionConfig,
|
|
56
|
+
model?: ExtensionContext["model"],
|
|
57
|
+
) => Promise<boolean>;
|
|
58
|
+
|
|
36
59
|
type ResponsesCompactOutcome =
|
|
37
60
|
| { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
|
|
38
61
|
| { outcome: "aborted" }
|
|
@@ -80,6 +103,29 @@ function notifyWarning(ctx: ExtensionContext, message: string): void {
|
|
|
80
103
|
}
|
|
81
104
|
}
|
|
82
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
|
+
|
|
83
129
|
function cloneOpaqueWindow(window: readonly unknown[]): unknown[] {
|
|
84
130
|
return window.map((item) => structuredClone(item));
|
|
85
131
|
}
|
|
@@ -98,6 +144,7 @@ async function runResponsesNativeCompact(
|
|
|
98
144
|
ctx: ExtensionContext,
|
|
99
145
|
config: CompactionConfig,
|
|
100
146
|
execution: RemoteCompactionExecution,
|
|
147
|
+
remoteCompact: typeof executeRemoteV2Compaction,
|
|
101
148
|
): Promise<ResponsesCompactOutcome> {
|
|
102
149
|
const { consumer, compactor } = execution;
|
|
103
150
|
const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
|
|
@@ -179,12 +226,12 @@ async function runResponsesNativeCompact(
|
|
|
179
226
|
model: consumer.model,
|
|
180
227
|
baseUrl: consumer.baseUrl,
|
|
181
228
|
sessionId: getSessionId(ctx),
|
|
182
|
-
});
|
|
229
|
+
}, compactor.currentModel);
|
|
183
230
|
if (extras) {
|
|
184
231
|
request = { ...request, ...extras };
|
|
185
232
|
}
|
|
186
233
|
|
|
187
|
-
const compactResult = await
|
|
234
|
+
const compactResult = await remoteCompact({
|
|
188
235
|
runtime: compactor,
|
|
189
236
|
request,
|
|
190
237
|
signal: event.signal,
|
|
@@ -287,8 +334,13 @@ async function runResponsesNativeCompact(
|
|
|
287
334
|
return { outcome: "success", compaction };
|
|
288
335
|
}
|
|
289
336
|
|
|
290
|
-
async function handleSessionBeforeCompact(
|
|
291
|
-
|
|
337
|
+
async function handleSessionBeforeCompact(
|
|
338
|
+
event: SessionBeforeCompactEvent,
|
|
339
|
+
ctx: ExtensionContext,
|
|
340
|
+
dependencies: CompactionDependencies,
|
|
341
|
+
remoteContextActive: RemoteContextActive,
|
|
342
|
+
) {
|
|
343
|
+
const { config: toolkitConfig } = dependencies.loadConfig();
|
|
292
344
|
const config = toolkitConfig.compaction;
|
|
293
345
|
if (!config.enabled) {
|
|
294
346
|
return undefined;
|
|
@@ -315,6 +367,28 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
315
367
|
return { cancel: true };
|
|
316
368
|
}
|
|
317
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
|
+
|
|
318
392
|
// Branch 1: Responses-family APIs use remote_compaction_v2 on the normal Responses stream.
|
|
319
393
|
let remoteAttempted = false;
|
|
320
394
|
const resolution = await resolveRemoteCompactionExecution(
|
|
@@ -327,7 +401,7 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
327
401
|
);
|
|
328
402
|
if (resolution.ok) {
|
|
329
403
|
remoteAttempted = true;
|
|
330
|
-
const responsesOutcome = await runResponsesNativeCompact(event, ctx, config, resolution.execution);
|
|
404
|
+
const responsesOutcome = await runResponsesNativeCompact(event, ctx, config, resolution.execution, dependencies.remoteCompact);
|
|
331
405
|
if (responsesOutcome.outcome === "success") {
|
|
332
406
|
return { compaction: responsesOutcome.compaction };
|
|
333
407
|
}
|
|
@@ -362,7 +436,7 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
362
436
|
// Branch 2: run pi's native compaction method. A failed remote request is compacted by the
|
|
363
437
|
// remote producer itself; a model that cannot use remote v2 at all uses nativeFallback.model.
|
|
364
438
|
const fallbackModelSpec = remoteAttempted ? config.remoteCompactModel : config.nativeFallback.model;
|
|
365
|
-
const fallback = await
|
|
439
|
+
const fallback = await dependencies.nativeFallback({ ctx, event, config, modelSpec: fallbackModelSpec });
|
|
366
440
|
if (fallback.ok) {
|
|
367
441
|
if (ctx.hasUI) {
|
|
368
442
|
ctx.ui.notify(
|
|
@@ -414,13 +488,122 @@ async function handleSessionBeforeCompact(event: SessionBeforeCompactEvent, ctx:
|
|
|
414
488
|
return undefined;
|
|
415
489
|
}
|
|
416
490
|
|
|
417
|
-
async function
|
|
418
|
-
|
|
491
|
+
async function handleContext(
|
|
492
|
+
event: ContextEvent,
|
|
493
|
+
ctx: ExtensionContext,
|
|
494
|
+
pi: ExtensionAPI,
|
|
495
|
+
loadConfig: typeof loadToolkitConfig,
|
|
496
|
+
contextWindows: CodexContextWindowManager,
|
|
497
|
+
remoteContextActive: RemoteContextActive,
|
|
498
|
+
) {
|
|
499
|
+
const { config: { compaction: config } } = loadConfig();
|
|
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 };
|
|
552
|
+
const resolution = await resolveNativeCompactionEnvironment(ctx, {
|
|
553
|
+
enabled: config.enabled,
|
|
554
|
+
responsesApis: config.responsesApis,
|
|
555
|
+
});
|
|
556
|
+
if (!resolution.ok) return undefined;
|
|
557
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
558
|
+
const latest = resolveLatestNativeCompactionEntry(branchEntries, { baseUrl: resolution.runtime.baseUrl });
|
|
559
|
+
if (!latest.ok) return undefined;
|
|
560
|
+
const result = removeNativeCompactionRetainedMessages({ messages: replayEvent.messages, branchEntries, compactionEntry: latest.entry });
|
|
561
|
+
if (!result.ok) {
|
|
562
|
+
writeReplayFailureArtifact({ reason: result.reason, compactionEntryId: latest.entry.id }, config, ctx);
|
|
563
|
+
if (ctx.hasUI) ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: replay failed (${result.reason}); request aborted`, "error");
|
|
564
|
+
ctx.abort();
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
return result.messages === replayEvent.messages ? undefined : { messages: result.messages };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function handleBeforeProviderRequest(
|
|
571
|
+
event: BeforeProviderRequestEvent,
|
|
572
|
+
ctx: ExtensionContext,
|
|
573
|
+
loadConfig: typeof loadToolkitConfig,
|
|
574
|
+
contextWindows: CodexContextWindowManager,
|
|
575
|
+
remoteContextActive: RemoteContextActive,
|
|
576
|
+
) {
|
|
577
|
+
const { config: toolkitConfig } = loadConfig();
|
|
419
578
|
const config = toolkitConfig.compaction;
|
|
420
579
|
if (!config.enabled) {
|
|
421
580
|
return undefined;
|
|
422
581
|
}
|
|
423
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
|
+
|
|
424
607
|
const resolution = await resolveNativeCompactionEnvironment(
|
|
425
608
|
ctx,
|
|
426
609
|
{
|
|
@@ -570,14 +753,63 @@ async function handleBeforeProviderRequest(event: BeforeProviderRequestEvent, ct
|
|
|
570
753
|
return rewrite.rewrittenPayload;
|
|
571
754
|
}
|
|
572
755
|
|
|
573
|
-
export default function (
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
756
|
+
export default function registerCompactionExtension(
|
|
757
|
+
pi: ExtensionAPI,
|
|
758
|
+
overrides: Partial<CompactionDependencies> = {},
|
|
759
|
+
) {
|
|
760
|
+
const loadConfig = overrides.loadConfig ?? loadToolkitConfig;
|
|
761
|
+
const contextWindows = overrides.contextWindows ?? new CodexContextWindowManager(
|
|
762
|
+
undefined,
|
|
763
|
+
() => loadConfig().config.compaction.codexGatewayModels,
|
|
764
|
+
);
|
|
765
|
+
const dependencies: CompactionDependencies = {
|
|
766
|
+
loadConfig,
|
|
767
|
+
remoteCompact: executeRemoteV2Compaction,
|
|
768
|
+
nativeFallback: runNativeFallbackCompaction,
|
|
769
|
+
contextWindows,
|
|
770
|
+
...overrides,
|
|
771
|
+
};
|
|
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);
|
|
791
|
+
const { config: toolkitConfig, source, warnings } = dependencies.loadConfig();
|
|
578
792
|
const config = toolkitConfig.compaction;
|
|
579
793
|
if (!config.enabled) return;
|
|
580
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
|
+
|
|
581
813
|
if (warnings.length > 0 && ctx.hasUI && config.debug) {
|
|
582
814
|
ctx.ui.notify(`${COMPACTION_EXTENSION_ID}: ${warnings[0]}`, "warning");
|
|
583
815
|
}
|
|
@@ -589,6 +821,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
589
821
|
config,
|
|
590
822
|
configSource: source,
|
|
591
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
|
+
},
|
|
592
830
|
},
|
|
593
831
|
config,
|
|
594
832
|
ctx,
|
|
@@ -604,8 +842,57 @@ export default function (pi: ExtensionAPI) {
|
|
|
604
842
|
}
|
|
605
843
|
});
|
|
606
844
|
|
|
607
|
-
pi.on("
|
|
608
|
-
pi.on("
|
|
609
|
-
pi.on("
|
|
610
|
-
pi.on("session_shutdown", (
|
|
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
|
+
});
|
|
611
898
|
}
|
package/src/payload-rewrite.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
3
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
4
|
import type {
|
|
@@ -33,7 +34,8 @@ export type NativeReplayPayloadRewriteFailureReason =
|
|
|
33
34
|
| "first-kept-entry-not-found"
|
|
34
35
|
| "invalid-compacted-window"
|
|
35
36
|
| "unexpected-compaction-after-boundary"
|
|
36
|
-
| "compaction-summary-not-found"
|
|
37
|
+
| "compaction-summary-not-found"
|
|
38
|
+
| "retained-context-mismatch";
|
|
37
39
|
|
|
38
40
|
export type NativeReplayPayloadRewriteFailure = {
|
|
39
41
|
ok: false;
|
|
@@ -311,6 +313,41 @@ export function collectLiveTailMessages(entries: readonly SessionEntry[]): Agent
|
|
|
311
313
|
return collectReplayMessages(entries);
|
|
312
314
|
}
|
|
313
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Remote V2 covers the complete pre-compaction context. Pi 0.85.1 also retains
|
|
318
|
+
* recent messages, so remove those verified copies before provider conversion
|
|
319
|
+
* (where IDs/fields may change across models). Persisted session entries are
|
|
320
|
+
* untouched. Preserve transient additions and all post-compaction messages.
|
|
321
|
+
*/
|
|
322
|
+
export function removeNativeCompactionRetainedMessages(args: {
|
|
323
|
+
messages: AgentMessage[];
|
|
324
|
+
branchEntries: readonly SessionEntry[];
|
|
325
|
+
compactionEntry: NativeCompactionEntry;
|
|
326
|
+
}): { ok: true; messages: AgentMessage[] } | NativeReplayPayloadRewriteFailure {
|
|
327
|
+
const boundary = findCompactionBoundaryIndex(args.branchEntries, args.compactionEntry.id);
|
|
328
|
+
if (boundary === undefined) return { ok: false, reason: "compaction-boundary-not-found" };
|
|
329
|
+
const firstKept = findEntryIndexByIdBeforeBoundary(args.branchEntries, args.compactionEntry.firstKeptEntryId, boundary);
|
|
330
|
+
if (firstKept === undefined) return { ok: false, reason: "first-kept-entry-not-found" };
|
|
331
|
+
const summary = sessionEntryToContextMessages(args.compactionEntry)[0];
|
|
332
|
+
const summaryIndex = args.messages.findIndex((message) => areEquivalentValues(message, summary));
|
|
333
|
+
if (summaryIndex < 0) return { ok: false, reason: "compaction-summary-not-found" };
|
|
334
|
+
const retained = args.branchEntries.slice(firstKept, boundary).flatMap(sessionEntryToContextMessages);
|
|
335
|
+
const removed = new Set<number>();
|
|
336
|
+
let cursor = summaryIndex + 1;
|
|
337
|
+
for (const expected of retained) {
|
|
338
|
+
const index = args.messages.findIndex((message, index) => index >= cursor && areEquivalentValues(message, expected));
|
|
339
|
+
if (index >= 0) {
|
|
340
|
+
removed.add(index);
|
|
341
|
+
cursor = index + 1;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Already removed by another context handler: leave it alone. Partial
|
|
345
|
+
// matches are ambiguous; never remove half a call/result batch.
|
|
346
|
+
if (removed.size === 0) return { ok: true, messages: args.messages };
|
|
347
|
+
if (removed.size !== retained.length) return { ok: false, reason: "retained-context-mismatch" };
|
|
348
|
+
return { ok: true, messages: args.messages.filter((_, index) => !removed.has(index)) };
|
|
349
|
+
}
|
|
350
|
+
|
|
314
351
|
export function serializeLiveTailToResponsesInput<TApi extends Api>(args: {
|
|
315
352
|
model: Model<TApi>;
|
|
316
353
|
entries: readonly SessionEntry[];
|
|
@@ -370,15 +407,8 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
370
407
|
};
|
|
371
408
|
}
|
|
372
409
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Pi-authored compaction summary item. Pi 0.84.3 renders the context after a
|
|
377
|
-
// compaction as [leading prompts][summary item][all post-compaction messages]:
|
|
378
|
-
// pre-compaction messages never appear in the payload (entry.retainedTail is
|
|
379
|
-
// never persisted by appendCompaction), so the summary item itself is the only
|
|
380
|
-
// anchor and the only thing that gets replaced. Everything else is preserved
|
|
381
|
-
// verbatim, so transient messages injected by other extensions stay intact.
|
|
410
|
+
// The context hook has removed Pi's verified retained copies. Replace only
|
|
411
|
+
// the summary anchor here; preserve provider fields and transient/post items.
|
|
382
412
|
const summaryIndex = findCompactionSummaryIndex(args.payload.input, args.compactionEntry.summary);
|
|
383
413
|
if (summaryIndex < 0) {
|
|
384
414
|
return {
|