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.
@@ -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 executeRemoteV2Compaction({
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(event: SessionBeforeCompactEvent, ctx: ExtensionContext) {
291
- const { config: toolkitConfig } = loadToolkitConfig();
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 runNativeFallbackCompaction({ ctx, event, config, modelSpec: fallbackModelSpec });
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 handleBeforeProviderRequest(event: BeforeProviderRequestEvent, ctx: ExtensionContext) {
418
- const { config: toolkitConfig } = 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
+ ) {
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 (pi: ExtensionAPI) {
574
- const inlineCompaction = registerInlineCompactionRuntime(pi, loadToolkitConfig);
575
-
576
- pi.on("session_start", (_event, ctx) => {
577
- const { config: toolkitConfig, source, warnings } = loadToolkitConfig();
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("session_before_compact", handleSessionBeforeCompact);
608
- pi.on("before_provider_request", handleBeforeProviderRequest);
609
- pi.on("turn_end", (event, ctx) => inlineCompaction.handlePublicTurnEnd(event, ctx));
610
- pi.on("session_shutdown", (_event, ctx) => inlineCompaction.dispose(ctx));
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
  }
@@ -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
- const preCompactionEntries = args.branchEntries.slice(firstKeptEntryIndex, boundaryIndex);
374
- const postCompactionEntries = args.branchEntries.slice(boundaryIndex + 1);
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 {