@narumitw/pi-codex-compact 0.51.3 → 0.52.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/dist/index.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  // @generated by scripts/build-runtime.mjs; do not edit.
2
2
  // @ts-nocheck -- the generated entry uses a .ts extension for Pi's Jiti loader.
3
3
  import {
4
- usesCodexResponsesApi
5
- } from "./chunks/chunk-6TZ2L2BM.js";
4
+ RESPONSES_COMPACTION_APIS,
5
+ resolveCompactionRoute,
6
+ terminalText,
7
+ usesResponsesCompactionApi
8
+ } from "./chunks/chunk-JYXZMCXD.js";
6
9
 
7
10
  // src/codex-compact.ts
8
11
  import {
@@ -17,6 +20,7 @@ import { createHash, randomUUID } from "node:crypto";
17
20
 
18
21
  // src/protocol.ts
19
22
  var MAX_SSE_BYTES = 8 * 1024 * 1024;
23
+ var MAX_COMPACT_JSON_BYTES = 8 * 1024 * 1024;
20
24
  var MAX_COMPACTION_ITEM_BYTES = 2 * 1024 * 1024;
21
25
  var CodexCompactionProtocolError = class extends Error {
22
26
  constructor(message) {
@@ -141,6 +145,115 @@ async function collectCompactionSse(stream, options = {}) {
141
145
  }
142
146
  return { item: [...items.values()][0], completedResponse };
143
147
  }
148
+ function isRetainedCompactContent(value) {
149
+ if (!isObject(value)) return false;
150
+ if (value.type === "input_text") return typeof value.text === "string";
151
+ if (value.type !== "input_image") return false;
152
+ if (value.detail !== void 0 && value.detail !== null && value.detail !== "auto" && value.detail !== "low" && value.detail !== "high" && value.detail !== "original") {
153
+ return false;
154
+ }
155
+ if (value.file_id !== void 0 && value.file_id !== null && typeof value.file_id !== "string" || value.image_url !== void 0 && value.image_url !== null && typeof value.image_url !== "string") {
156
+ return false;
157
+ }
158
+ return typeof value.file_id === "string" && value.file_id.length > 0 || typeof value.image_url === "string" && value.image_url.length > 0;
159
+ }
160
+ function isRetainedCompactMessage(value) {
161
+ return isObject(value) && value.role === "user" && (value.type === void 0 || value.type === "message") && Array.isArray(value.content) && value.content.length > 0 && value.content.every(isRetainedCompactContent);
162
+ }
163
+ function validateCompactedResponse(value, options = {}) {
164
+ if (!isObject(value) || !Array.isArray(value.output)) {
165
+ throw new CodexCompactionProtocolError("Responses Compact returned an invalid response object");
166
+ }
167
+ const maxBytes = options.maxBytes ?? MAX_COMPACT_JSON_BYTES;
168
+ const maxItemBytes = options.maxItemBytes ?? MAX_COMPACTION_ITEM_BYTES;
169
+ if (byteLength(value) > maxBytes) {
170
+ throw new CodexCompactionProtocolError("Responses Compact response exceeded the size limit");
171
+ }
172
+ if (value.output.length === 0) {
173
+ throw new CodexCompactionProtocolError("Responses Compact returned no output items");
174
+ }
175
+ const output = value.output.map((item2) => {
176
+ if (!isObject(item2)) {
177
+ throw new CodexCompactionProtocolError("Responses Compact returned a non-object output item");
178
+ }
179
+ if (byteLength(item2) > maxItemBytes) {
180
+ throw new CodexCompactionProtocolError(
181
+ "Responses Compact output item exceeded the size limit"
182
+ );
183
+ }
184
+ return structuredClone(item2);
185
+ });
186
+ const compactionItems = output.filter((item2) => item2.type === "compaction");
187
+ if (compactionItems.length !== 1 || output.at(-1)?.type !== "compaction") {
188
+ throw new CodexCompactionProtocolError(
189
+ "Responses Compact must return retained messages followed by one compaction item"
190
+ );
191
+ }
192
+ for (const item2 of output.slice(0, -1)) {
193
+ if (!isRetainedCompactMessage(item2)) {
194
+ throw new CodexCompactionProtocolError(
195
+ "Responses Compact returned an unsupported retained output item"
196
+ );
197
+ }
198
+ }
199
+ const item = validateCompactionItem(output.at(-1), maxItemBytes);
200
+ return { item, output: [...output.slice(0, -1), item], response: structuredClone(value) };
201
+ }
202
+ async function collectCompactResponse(response, options = {}) {
203
+ const maxBytes = options.maxBytes ?? MAX_COMPACT_JSON_BYTES;
204
+ const declaredLength = Number(response.headers.get("content-length"));
205
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
206
+ const error = new CodexCompactionProtocolError(
207
+ "Responses Compact response exceeded the size limit"
208
+ );
209
+ await response.body?.cancel(error).catch(() => void 0);
210
+ throw error;
211
+ }
212
+ if (!response.body) {
213
+ throw new CodexCompactionProtocolError("Responses Compact response did not contain a body");
214
+ }
215
+ const reader = response.body.getReader();
216
+ const chunks = [];
217
+ let bytes = 0;
218
+ const onAbort = () => {
219
+ void reader.cancel(new DOMException("Compaction aborted", "AbortError")).catch(() => void 0);
220
+ };
221
+ options.signal?.addEventListener("abort", onAbort, { once: true });
222
+ try {
223
+ while (true) {
224
+ if (options.signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
225
+ const { done, value } = await reader.read();
226
+ if (done) break;
227
+ bytes += value.byteLength;
228
+ if (bytes > maxBytes) {
229
+ throw new CodexCompactionProtocolError(
230
+ "Responses Compact response exceeded the size limit"
231
+ );
232
+ }
233
+ chunks.push(value);
234
+ }
235
+ if (options.signal?.aborted) throw new DOMException("Compaction aborted", "AbortError");
236
+ } catch (error) {
237
+ await reader.cancel(error).catch(() => void 0);
238
+ throw error;
239
+ } finally {
240
+ options.signal?.removeEventListener("abort", onAbort);
241
+ reader.releaseLock();
242
+ }
243
+ const body = new Uint8Array(bytes);
244
+ let offset = 0;
245
+ for (const chunk of chunks) {
246
+ body.set(chunk, offset);
247
+ offset += chunk.byteLength;
248
+ }
249
+ let parsed;
250
+ try {
251
+ parsed = JSON.parse(new TextDecoder().decode(body));
252
+ } catch {
253
+ throw new CodexCompactionProtocolError("Responses Compact returned malformed JSON");
254
+ }
255
+ return validateCompactedResponse(parsed, options);
256
+ }
144
257
  function markerTextFromItem(item) {
145
258
  if (!isObject(item) || item.role !== "user" || !Array.isArray(item.content)) return void 0;
146
259
  if (item.content.length !== 1) return void 0;
@@ -181,9 +294,17 @@ function appendCompactionTrigger(payload) {
181
294
  }
182
295
  return { ...payload, input: [...payload.input, { type: "compaction_trigger" }] };
183
296
  }
297
+ function expandRemoteCompactionPayload(payload, checkpoint) {
298
+ if (checkpoint) {
299
+ return rewriteCheckpointMarker(payload, checkpoint.marker, checkpoint.replacementHistory);
300
+ }
301
+ if (!isObject(payload) || !Array.isArray(payload.input)) {
302
+ throw new CodexCompactionProtocolError("Responses payload is missing an input array");
303
+ }
304
+ return structuredClone(payload);
305
+ }
184
306
  function prepareRemoteCompactionPayload(payload, checkpoint) {
185
- const expanded = checkpoint ? rewriteCheckpointMarker(payload, checkpoint.marker, checkpoint.replacementHistory) : payload;
186
- return appendCompactionTrigger(expanded);
307
+ return appendCompactionTrigger(expandRemoteCompactionPayload(payload, checkpoint));
187
308
  }
188
309
  function hasCheckpointMarker(payload, marker) {
189
310
  return isObject(payload) && Array.isArray(payload.input) && payload.input.some((item) => markerTextFromItem(item) === marker);
@@ -191,11 +312,15 @@ function hasCheckpointMarker(payload, marker) {
191
312
 
192
313
  // src/checkpoint.ts
193
314
  var CHECKPOINT_KIND = "pi-codex-remote-compaction";
194
- var CHECKPOINT_VERSION = 1;
315
+ var CHECKPOINT_VERSION = 2;
195
316
  var REPLACEMENT_TOKEN_BUDGET = 64e3;
196
317
  var REPLACEMENT_BYTE_BUDGET = 8 * 1024 * 1024;
197
318
  var MAX_MEDIA_ITEM_BYTES = 2 * 1024 * 1024;
319
+ var MAX_CHECKPOINT_DETAILS_BYTES = 10 * 1024 * 1024;
320
+ var MAX_CHECKPOINT_ID_LENGTH = 128;
198
321
  var MAX_PROVIDER_ID_LENGTH = 256;
322
+ var MAX_MODEL_ID_LENGTH = 512;
323
+ var MAX_KEPT_FINGERPRINTS = 1e5;
199
324
  function isObject2(value) {
200
325
  return typeof value === "object" && value !== null && !Array.isArray(value);
201
326
  }
@@ -216,13 +341,13 @@ function checkpointMarker(checkpointId) {
216
341
  return [
217
342
  `[PI_CODEX_REMOTE_CHECKPOINT:${checkpointId}]`,
218
343
  "Opaque checkpoint injection failed. Do not infer missing history; tell the user to re-enable",
219
- "@narumitw/pi-codex-compact with a model using the openai-codex-responses API."
344
+ "@narumitw/pi-codex-compact with the same model and Responses API."
220
345
  ].join(" ");
221
346
  }
222
347
  function fallbackSummary(checkpointId) {
223
348
  return [
224
- `OpenAI Codex Remote Compaction V2 checkpoint ${checkpointId} stores the older history opaquely.`,
225
- "Full replay requires @narumitw/pi-codex-compact and the same model through a compatible Codex Responses provider.",
349
+ `Responses compaction checkpoint ${checkpointId} stores the older history opaquely.`,
350
+ "Full replay requires @narumitw/pi-codex-compact and the same model through a compatible Responses provider.",
226
351
  "Without them, only Pi's retained recent messages remain available."
227
352
  ].join(" ");
228
353
  }
@@ -235,7 +360,14 @@ function markerMessage(checkpointId, timestamp) {
235
360
  }
236
361
  function parseCheckpointDetails(value) {
237
362
  if (!isObject2(value)) return void 0;
238
- if (value.kind !== CHECKPOINT_KIND || value.version !== CHECKPOINT_VERSION || typeof value.checkpointId !== "string" || value.checkpointId.length < 8 || typeof value.provider !== "string" || value.provider.length === 0 || value.provider.length > MAX_PROVIDER_ID_LENGTH || value.api !== "openai-codex-responses" || typeof value.modelId !== "string" || value.protocol !== "remote-compaction-v2" || !Array.isArray(value.replacementHistory) || !Array.isArray(value.keptMessageFingerprints) || typeof value.createdAt !== "string") {
363
+ try {
364
+ if (serializedBytes(value) > MAX_CHECKPOINT_DETAILS_BYTES) return void 0;
365
+ } catch {
366
+ return void 0;
367
+ }
368
+ const isVersionOne = value.version === 1 && value.api === "openai-codex-responses" && value.protocol === "remote-compaction-v2";
369
+ const isVersionTwo = value.version === CHECKPOINT_VERSION && RESPONSES_COMPACTION_APIS.includes(value.api) && (value.protocol === "remote-v2" || value.protocol === "responses-compact");
370
+ if (value.kind !== CHECKPOINT_KIND || !isVersionOne && !isVersionTwo || typeof value.checkpointId !== "string" || value.checkpointId.length < 8 || value.checkpointId.length > MAX_CHECKPOINT_ID_LENGTH || typeof value.provider !== "string" || value.provider.length === 0 || value.provider.length > MAX_PROVIDER_ID_LENGTH || typeof value.modelId !== "string" || value.modelId.length === 0 || value.modelId.length > MAX_MODEL_ID_LENGTH || !Array.isArray(value.replacementHistory) || !Array.isArray(value.keptMessageFingerprints) || value.keptMessageFingerprints.length > MAX_KEPT_FINGERPRINTS || typeof value.createdAt !== "string" || value.createdAt.length > 64) {
239
371
  return void 0;
240
372
  }
241
373
  if (value.replacementHistory.length === 0 || !value.replacementHistory.every(isObject2) || !value.keptMessageFingerprints.every(
@@ -249,7 +381,18 @@ function parseCheckpointDetails(value) {
249
381
  } catch {
250
382
  return void 0;
251
383
  }
252
- return structuredClone(value);
384
+ return {
385
+ kind: CHECKPOINT_KIND,
386
+ version: CHECKPOINT_VERSION,
387
+ checkpointId: value.checkpointId,
388
+ provider: value.provider,
389
+ api: isVersionOne ? "openai-codex-responses" : value.api,
390
+ modelId: value.modelId,
391
+ protocol: isVersionOne ? "remote-v2" : value.protocol,
392
+ replacementHistory: structuredClone(value.replacementHistory),
393
+ keptMessageFingerprints: [...value.keptMessageFingerprints],
394
+ createdAt: value.createdAt
395
+ };
253
396
  }
254
397
  function latestCheckpoint(entries) {
255
398
  for (let index = entries.length - 1; index >= 0; index--) {
@@ -362,9 +505,9 @@ function createCheckpointDetails(input) {
362
505
  version: CHECKPOINT_VERSION,
363
506
  checkpointId: input.checkpointId ?? randomUUID(),
364
507
  provider: input.provider,
365
- api: "openai-codex-responses",
508
+ api: input.api,
366
509
  modelId: input.modelId,
367
- protocol: "remote-compaction-v2",
510
+ protocol: input.protocol,
368
511
  replacementHistory: structuredClone(input.replacementHistory),
369
512
  keptMessageFingerprints: input.keptMessages.map(fingerprintMessage),
370
513
  createdAt: input.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
@@ -374,22 +517,298 @@ function createCheckpointDetails(input) {
374
517
  return parsed;
375
518
  }
376
519
 
377
- // src/remote.ts
378
- var EMPTY_USAGE = {
379
- input: 0,
380
- output: 0,
381
- cacheRead: 0,
382
- cacheWrite: 0,
383
- totalTokens: 0,
384
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
385
- };
386
- function isObject3(value) {
520
+ // src/remote-types.ts
521
+ function isJsonObject(value) {
387
522
  return typeof value === "object" && value !== null && !Array.isArray(value);
388
523
  }
389
524
  function abortError() {
390
525
  return new DOMException("Compaction aborted", "AbortError");
391
526
  }
392
- async function requestRemoteCompaction(request) {
527
+ function assertPreparedInput(payload) {
528
+ if (!Array.isArray(payload.input) || !payload.input.every(isJsonObject)) {
529
+ throw new Error("Prepared compaction payload has invalid input items");
530
+ }
531
+ return structuredClone(payload.input);
532
+ }
533
+
534
+ // src/remote-shared.ts
535
+ async function collectProviderUsage(stream, signal) {
536
+ let usage;
537
+ for await (const event of stream) {
538
+ if (signal.aborted) throw abortError();
539
+ if (event.type === "error") {
540
+ throw new Error(event.error.errorMessage ?? "Responses compaction request failed");
541
+ }
542
+ if (event.type === "done") usage = event.message.usage;
543
+ }
544
+ if (signal.aborted) throw abortError();
545
+ if (!usage) throw new Error("Responses provider stream ended without completion usage");
546
+ return usage;
547
+ }
548
+
549
+ // src/remote-compact.ts
550
+ var OFFICIAL_COMPACT_FIELDS = [
551
+ "model",
552
+ "input",
553
+ "instructions",
554
+ "previous_response_id",
555
+ "prompt_cache_key",
556
+ "prompt_cache_retention",
557
+ "service_tier"
558
+ ];
559
+ var CODEX_COMPACT_FIELDS = [
560
+ "model",
561
+ "input",
562
+ "instructions",
563
+ "tools",
564
+ "parallel_tool_calls",
565
+ "reasoning",
566
+ "service_tier",
567
+ "prompt_cache_key",
568
+ "text",
569
+ "access_programs"
570
+ ];
571
+ function compactPayload(payload, api) {
572
+ const fields = api === "openai-codex-responses" ? CODEX_COMPACT_FIELDS : OFFICIAL_COMPACT_FIELDS;
573
+ const result = {};
574
+ for (const field of fields) {
575
+ if (Object.hasOwn(payload, field) && payload[field] !== void 0) {
576
+ result[field] = structuredClone(payload[field]);
577
+ }
578
+ }
579
+ if (typeof result.model !== "string" || result.model.length === 0) {
580
+ throw new CodexCompactionProtocolError("Responses payload is missing a model");
581
+ }
582
+ assertPreparedInput(result);
583
+ return result;
584
+ }
585
+ function requestUrl(input) {
586
+ return new URL(input instanceof Request ? input.url : String(input));
587
+ }
588
+ function responsesCompactUrl(input) {
589
+ const original = requestUrl(input);
590
+ if (!original.pathname.endsWith("/responses")) {
591
+ throw new CodexCompactionProtocolError(
592
+ "Provider request URL does not end with the Responses endpoint"
593
+ );
594
+ }
595
+ const compact = new URL(original);
596
+ compact.pathname = `${compact.pathname}/compact`;
597
+ if (compact.origin !== original.origin) {
598
+ throw new CodexCompactionProtocolError("Responses Compact URL changed origin");
599
+ }
600
+ return compact;
601
+ }
602
+ function mergedHeaders(input, init) {
603
+ const headers = new Headers(input instanceof Request ? input.headers : void 0);
604
+ new Headers(init?.headers).forEach((value, name) => {
605
+ headers.set(name, value);
606
+ });
607
+ headers.delete("content-encoding");
608
+ headers.delete("content-length");
609
+ headers.set("accept", "application/json");
610
+ headers.set("content-type", "application/json");
611
+ return headers;
612
+ }
613
+ function mergedSignal(input, init, ownerSignal) {
614
+ const signals = [ownerSignal];
615
+ if (input instanceof Request) signals.push(input.signal);
616
+ if (init?.signal) signals.push(init.signal);
617
+ return signals.length === 1 ? ownerSignal : AbortSignal.any(signals);
618
+ }
619
+ function nonRetryableBridgeFailure(error) {
620
+ const message = error instanceof Error ? error.message : String(error);
621
+ return Response.json(
622
+ {
623
+ error: {
624
+ message,
625
+ type: "invalid_request_error",
626
+ code: "invalid_compact_response"
627
+ }
628
+ },
629
+ { status: 400 }
630
+ );
631
+ }
632
+ function nonNegativeInteger(value) {
633
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
634
+ }
635
+ function optionalUsageDetail(details, field) {
636
+ if (details === void 0 || details === null) return 0;
637
+ if (!isJsonObject(details)) {
638
+ throw new CodexCompactionProtocolError("Responses Compact response has invalid usage details");
639
+ }
640
+ const value = details[field];
641
+ if (value === void 0 || value === null) return 0;
642
+ if (!nonNegativeInteger(value)) {
643
+ throw new CodexCompactionProtocolError(
644
+ `Responses Compact response has invalid usage detail ${field}`
645
+ );
646
+ }
647
+ return value;
648
+ }
649
+ function validatedUsage(response) {
650
+ const usage = response.usage;
651
+ if (!isJsonObject(usage)) {
652
+ throw new CodexCompactionProtocolError("Responses Compact response is missing usage");
653
+ }
654
+ for (const field of ["input_tokens", "output_tokens", "total_tokens"]) {
655
+ if (!nonNegativeInteger(usage[field])) {
656
+ throw new CodexCompactionProtocolError(`Responses Compact response has invalid ${field}`);
657
+ }
658
+ }
659
+ const inputTokens = usage.input_tokens;
660
+ const outputTokens = usage.output_tokens;
661
+ const totalTokens = usage.total_tokens;
662
+ const cachedTokens = optionalUsageDetail(usage.input_tokens_details, "cached_tokens");
663
+ const cacheWriteTokens = optionalUsageDetail(usage.input_tokens_details, "cache_write_tokens");
664
+ const reasoningTokens = optionalUsageDetail(usage.output_tokens_details, "reasoning_tokens");
665
+ if (cachedTokens + cacheWriteTokens > inputTokens || reasoningTokens > outputTokens) {
666
+ throw new CodexCompactionProtocolError(
667
+ "Responses Compact response has inconsistent usage details"
668
+ );
669
+ }
670
+ if (totalTokens !== inputTokens + outputTokens) {
671
+ throw new CodexCompactionProtocolError(
672
+ "Responses Compact response has inconsistent total usage"
673
+ );
674
+ }
675
+ return structuredClone(usage);
676
+ }
677
+ function syntheticCompletion(result, payload) {
678
+ const completed = {
679
+ id: typeof result.response.id === "string" ? result.response.id : "resp_pi_compact_bridge",
680
+ object: "response",
681
+ created_at: typeof result.response.created_at === "number" ? result.response.created_at : Math.floor(Date.now() / 1e3),
682
+ status: "completed",
683
+ model: payload.model,
684
+ output: [],
685
+ parallel_tool_calls: false,
686
+ tool_choice: "auto",
687
+ tools: [],
688
+ usage: validatedUsage(result.response)
689
+ };
690
+ const events = [
691
+ { type: "response.created", response: { ...completed, status: "in_progress" } },
692
+ { type: "response.completed", response: completed }
693
+ ];
694
+ return new Response(events.map((event) => `data: ${JSON.stringify(event)}
695
+
696
+ `).join(""), {
697
+ status: 200,
698
+ headers: { "content-type": "text/event-stream" }
699
+ });
700
+ }
701
+ async function requestResponsesCompact(request) {
702
+ if (request.signal.aborted) throw abortError();
703
+ let preparedPayload;
704
+ let sentInput;
705
+ let compactResult;
706
+ let bridgeError;
707
+ let dispatchInFlight = false;
708
+ let successfulResponses = 0;
709
+ const baseFetch = request.fetch ?? globalThis.fetch;
710
+ const bridgeFetch = async (input, init) => {
711
+ if (request.signal.aborted) throw abortError();
712
+ if (successfulResponses > 0) {
713
+ bridgeError = new CodexCompactionProtocolError(
714
+ "Provider dispatched again after Responses Compact succeeded"
715
+ );
716
+ return nonRetryableBridgeFailure(bridgeError);
717
+ }
718
+ if (dispatchInFlight) {
719
+ bridgeError = new CodexCompactionProtocolError(
720
+ "Provider dispatched overlapping Responses Compact requests"
721
+ );
722
+ return nonRetryableBridgeFailure(bridgeError);
723
+ }
724
+ if (!preparedPayload) {
725
+ bridgeError = new CodexCompactionProtocolError(
726
+ "Provider dispatched before exposing its request payload"
727
+ );
728
+ return nonRetryableBridgeFailure(bridgeError);
729
+ }
730
+ let compactUrl;
731
+ try {
732
+ compactUrl = responsesCompactUrl(input);
733
+ } catch (error) {
734
+ bridgeError = error;
735
+ return nonRetryableBridgeFailure(error);
736
+ }
737
+ const signal = mergedSignal(input, init, request.signal);
738
+ dispatchInFlight = true;
739
+ try {
740
+ const response = await baseFetch(compactUrl, {
741
+ ...init,
742
+ method: "POST",
743
+ headers: mergedHeaders(input, init),
744
+ body: JSON.stringify(preparedPayload),
745
+ signal
746
+ });
747
+ if (!response.ok) return response;
748
+ try {
749
+ const result = await collectCompactResponse(response, { signal });
750
+ successfulResponses += 1;
751
+ if (successfulResponses !== 1) {
752
+ throw new CodexCompactionProtocolError(
753
+ "Provider returned more than one successful Responses Compact response"
754
+ );
755
+ }
756
+ compactResult = result;
757
+ return syntheticCompletion(result, preparedPayload);
758
+ } catch (error) {
759
+ bridgeError = error;
760
+ return nonRetryableBridgeFailure(error);
761
+ }
762
+ } finally {
763
+ dispatchInFlight = false;
764
+ }
765
+ };
766
+ const stream = request.provider.stream(request.model, request.context, {
767
+ apiKey: request.apiKey,
768
+ headers: request.headers,
769
+ env: request.env,
770
+ signal: request.signal,
771
+ transport: "sse",
772
+ cacheRetention: "none",
773
+ timeoutMs: request.requestTimeoutMs ?? 5 * 60 * 1e3,
774
+ maxRetries: request.maxRetries ?? 2,
775
+ fetch: bridgeFetch,
776
+ onPayload: (payload) => {
777
+ if (preparedPayload) {
778
+ throw new CodexCompactionProtocolError(
779
+ "Provider exposed more than one compaction request payload"
780
+ );
781
+ }
782
+ const expanded = expandRemoteCompactionPayload(payload, request.priorCheckpoint);
783
+ preparedPayload = compactPayload(expanded, request.model.api);
784
+ sentInput = assertPreparedInput(preparedPayload);
785
+ return expanded;
786
+ }
787
+ });
788
+ let usage;
789
+ try {
790
+ usage = await collectProviderUsage(stream, request.signal);
791
+ } catch (error) {
792
+ if (bridgeError) throw bridgeError;
793
+ throw error;
794
+ }
795
+ if (request.signal.aborted) throw abortError();
796
+ if (bridgeError) throw bridgeError;
797
+ if (!preparedPayload || !sentInput || !compactResult || successfulResponses !== 1) {
798
+ throw new CodexCompactionProtocolError(
799
+ "Provider did not complete exactly one Responses Compact request"
800
+ );
801
+ }
802
+ return {
803
+ item: compactResult.item,
804
+ promptInput: sentInput,
805
+ compactedOutput: compactResult.output,
806
+ usage
807
+ };
808
+ }
809
+
810
+ // src/remote-v2.ts
811
+ async function requestRemoteCompactionV2(request) {
393
812
  if (request.signal.aborted) throw abortError();
394
813
  let sentInput;
395
814
  const inspections = [];
@@ -421,35 +840,30 @@ async function requestRemoteCompaction(request) {
421
840
  fetch: inspectedFetch,
422
841
  onPayload: (payload) => {
423
842
  const prepared = prepareRemoteCompactionPayload(payload, request.priorCheckpoint);
424
- if (!Array.isArray(prepared.input) || !prepared.input.every(isObject3)) {
425
- throw new CodexCompactionProtocolError(
426
- "Prepared compaction payload has invalid input items"
427
- );
428
- }
429
- sentInput = structuredClone(prepared.input.slice(0, -1));
843
+ sentInput = assertPreparedInput(prepared).slice(0, -1);
430
844
  return prepared;
431
845
  }
432
846
  });
433
- let usage = EMPTY_USAGE;
434
- for await (const event of stream) {
435
- if (request.signal.aborted) throw abortError();
436
- if (event.type === "error") {
437
- throw new Error(event.error.errorMessage ?? "Codex remote compaction request failed");
438
- }
439
- if (event.type === "done") usage = event.message.usage;
440
- }
441
- if (request.signal.aborted) throw abortError();
442
- if (!sentInput)
847
+ const usage = await collectProviderUsage(stream, request.signal);
848
+ if (!sentInput) {
443
849
  throw new CodexCompactionProtocolError("Provider did not expose a request payload");
444
- if (inspections.length === 0) {
445
- throw new CodexCompactionProtocolError("Provider response did not expose an SSE body");
446
850
  }
447
- const inspection = await inspections.at(-1);
851
+ if (inspections.length !== 1) {
852
+ throw new CodexCompactionProtocolError(
853
+ `Provider exposed ${inspections.length} successful SSE responses; expected exactly one`
854
+ );
855
+ }
856
+ const inspection = await inspections[0];
448
857
  if (request.signal.aborted) throw abortError();
449
- if (!inspection?.ok) throw inspection?.error ?? new Error("Remote compaction inspection failed");
858
+ if (!inspection.ok) throw inspection.error;
450
859
  return { item: inspection.value.item, promptInput: sentInput, usage };
451
860
  }
452
861
 
862
+ // src/remote.ts
863
+ function requestRemoteCompaction(request) {
864
+ return request.protocol === "responses-compact" ? requestResponsesCompact(request) : requestRemoteCompactionV2(request);
865
+ }
866
+
453
867
  // src/settings.ts
454
868
  import { randomUUID as randomUUID2 } from "node:crypto";
455
869
  import { constants } from "node:fs";
@@ -460,6 +874,7 @@ var CODEX_COMPACT_SETTINGS_FILE = "pi-codex-compact.json";
460
874
  var MAX_SETTINGS_BYTES = 64 * 1024;
461
875
  var DEFAULT_CODEX_COMPACT_SETTINGS = Object.freeze({
462
876
  enabled: true,
877
+ protocol: "auto",
463
878
  requestTimeoutMs: 3e5,
464
879
  maxRetries: 2,
465
880
  replacementTokenBudget: 64e3,
@@ -479,6 +894,9 @@ function validInteger(value, minimum, maximum) {
479
894
  function normalizeCodexCompactSettings(value) {
480
895
  if (!isRecord(value)) return void 0;
481
896
  if (Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean") return void 0;
897
+ if (Object.hasOwn(value, "protocol") && value.protocol !== "auto" && value.protocol !== "remote-v2" && value.protocol !== "responses-compact") {
898
+ return void 0;
899
+ }
482
900
  if (Object.hasOwn(value, "notifyOnFallback") && typeof value.notifyOnFallback !== "boolean") {
483
901
  return void 0;
484
902
  }
@@ -489,6 +907,7 @@ function normalizeCodexCompactSettings(value) {
489
907
  }
490
908
  return {
491
909
  enabled: typeof value.enabled === "boolean" ? value.enabled : DEFAULT_CODEX_COMPACT_SETTINGS.enabled,
910
+ protocol: value.protocol === "remote-v2" || value.protocol === "responses-compact" ? value.protocol : DEFAULT_CODEX_COMPACT_SETTINGS.protocol,
492
911
  requestTimeoutMs: typeof value.requestTimeoutMs === "number" ? value.requestTimeoutMs : DEFAULT_CODEX_COMPACT_SETTINGS.requestTimeoutMs,
493
912
  maxRetries: typeof value.maxRetries === "number" ? value.maxRetries : DEFAULT_CODEX_COMPACT_SETTINGS.maxRetries,
494
913
  replacementTokenBudget: typeof value.replacementTokenBudget === "number" ? value.replacementTokenBudget : DEFAULT_CODEX_COMPACT_SETTINGS.replacementTokenBudget,
@@ -608,7 +1027,7 @@ function activeCheckpoint(ctx) {
608
1027
  return latestCheckpoint(ctx.sessionManager.getBranch());
609
1028
  }
610
1029
  function isCheckpointCompatible(details, model) {
611
- return usesCodexResponsesApi(model) && model.id === details.modelId;
1030
+ return usesResponsesCompactionApi(model) && model.api === details.api && model.id === details.modelId;
612
1031
  }
613
1032
  function keptMessages(event) {
614
1033
  const leafId = event.branchEntries.at(-1)?.id ?? null;
@@ -622,20 +1041,25 @@ function keptMessages(event) {
622
1041
  return contextEntries.slice(keptIndex).flatMap(sessionEntryToContextMessages);
623
1042
  }
624
1043
  function activeTools(pi) {
625
- const enabled = new Set(pi.getActiveTools());
626
- return pi.getAllTools().filter((tool) => enabled.has(tool.name)).map((tool) => ({
627
- name: tool.name,
628
- description: tool.description,
629
- parameters: tool.parameters
630
- }));
1044
+ const available = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
1045
+ return pi.getActiveTools().flatMap((name) => {
1046
+ const tool = available.get(name);
1047
+ return tool ? [
1048
+ {
1049
+ name: tool.name,
1050
+ description: tool.description,
1051
+ parameters: tool.parameters
1052
+ }
1053
+ ] : [];
1054
+ });
631
1055
  }
632
1056
  function projectedCurrentMessages(event, model) {
633
1057
  const leafId = event.branchEntries.at(-1)?.id ?? null;
634
1058
  const session = buildSessionContext(event.branchEntries, leafId);
635
1059
  const prior = latestCheckpoint(event.branchEntries);
636
1060
  if (!prior) return { messages: session.messages };
637
- if (prior.details.modelId !== model.id) {
638
- throw new Error("The active opaque checkpoint belongs to a different Codex model");
1061
+ if (prior.details.api !== model.api || prior.details.modelId !== model.id) {
1062
+ throw new Error("The active opaque checkpoint belongs to a different Responses model");
639
1063
  }
640
1064
  const projected = projectCheckpointContext(session.messages, prior.details, prior.entry.summary);
641
1065
  if (!projected) {
@@ -645,23 +1069,29 @@ function projectedCurrentMessages(event, model) {
645
1069
  }
646
1070
  function notifyFailure(ctx, error, settings) {
647
1071
  if (!ctx.hasUI || !settings.notifyOnFallback) return;
648
- const message = error instanceof Error ? error.message : String(error);
649
- ctx.ui.notify(`Codex remote compaction failed; using Pi compaction. ${message}`, "warning");
1072
+ const message = terminalText(error instanceof Error ? error.message : String(error));
1073
+ ctx.ui.notify(`Responses compaction failed; using Pi compaction. ${message}`, "warning");
650
1074
  }
651
1075
  function sessionStillOwned(ctx, sessionId, signal) {
652
1076
  return !signal.aborted && ctx.sessionManager.getSessionId() === sessionId;
653
1077
  }
654
- async function compactRemotely(pi, event, ctx, settings, fetch) {
1078
+ async function compactRemotely(pi, event, ctx, settings, ownerSignal, fetch) {
655
1079
  const model = ctx.model;
656
- if (!settings.enabled || !usesCodexResponsesApi(model)) return void 0;
1080
+ const route = resolveCompactionRoute(model, settings);
1081
+ if (route.kind === "native" || !usesResponsesCompactionApi(model)) return void 0;
1082
+ const signal = AbortSignal.any([event.signal, ownerSignal]);
1083
+ if (signal.aborted) return { cancel: true };
657
1084
  const sessionId = ctx.sessionManager.getSessionId();
658
- ctx.ui.setStatus(STATUS_KEY, "Codex remote compaction\u2026");
1085
+ ctx.ui.setStatus(
1086
+ STATUS_KEY,
1087
+ route.protocol === "remote-v2" ? "Responses Remote V2\u2026" : "Responses Compact API\u2026"
1088
+ );
659
1089
  try {
660
1090
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
661
- if (!sessionStillOwned(ctx, sessionId, event.signal)) return { cancel: true };
1091
+ if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
662
1092
  if (!auth.ok) throw new Error(auth.error);
663
1093
  const provider = ctx.modelRegistry.getProvider(model.provider);
664
- if (!provider) throw new Error("The active Codex Responses provider is unavailable");
1094
+ if (!provider) throw new Error("The active Responses provider is unavailable");
665
1095
  const current = projectedCurrentMessages(event, model);
666
1096
  const context = {
667
1097
  systemPrompt: ctx.getSystemPrompt(),
@@ -672,10 +1102,11 @@ async function compactRemotely(pi, event, ctx, settings, fetch) {
672
1102
  provider,
673
1103
  model,
674
1104
  context,
1105
+ protocol: route.protocol,
675
1106
  apiKey: auth.apiKey,
676
1107
  headers: auth.headers,
677
1108
  env: auth.env,
678
- signal: event.signal,
1109
+ signal,
679
1110
  priorCheckpoint: current.prior ? {
680
1111
  marker: checkpointMarker(current.prior.checkpointId),
681
1112
  replacementHistory: current.prior.replacementHistory
@@ -684,13 +1115,17 @@ async function compactRemotely(pi, event, ctx, settings, fetch) {
684
1115
  maxRetries: settings.maxRetries,
685
1116
  fetch
686
1117
  });
687
- if (!sessionStillOwned(ctx, sessionId, event.signal)) return { cancel: true };
688
- const replacementHistory = buildReplacementHistory(response.promptInput, response.item, {
689
- tokenBudget: settings.replacementTokenBudget
690
- });
1118
+ if (!sessionStillOwned(ctx, sessionId, signal)) return { cancel: true };
1119
+ const replacementHistory = buildReplacementHistory(
1120
+ response.compactedOutput?.slice(0, -1) ?? response.promptInput,
1121
+ response.item,
1122
+ { tokenBudget: settings.replacementTokenBudget }
1123
+ );
691
1124
  const details = createCheckpointDetails({
692
1125
  provider: model.provider,
1126
+ api: route.api,
693
1127
  modelId: model.id,
1128
+ protocol: route.protocol,
694
1129
  replacementHistory,
695
1130
  keptMessages: keptMessages(event)
696
1131
  });
@@ -704,7 +1139,7 @@ async function compactRemotely(pi, event, ctx, settings, fetch) {
704
1139
  }
705
1140
  };
706
1141
  } catch (error) {
707
- if (event.signal.aborted || ctx.sessionManager.getSessionId() !== sessionId) {
1142
+ if (signal.aborted || ctx.sessionManager.getSessionId() !== sessionId) {
708
1143
  return { cancel: true };
709
1144
  }
710
1145
  notifyFailure(ctx, error, settings);
@@ -720,11 +1155,12 @@ function createCodexCompactExtension(options = {}) {
720
1155
  let sessionController = new AbortController();
721
1156
  let generation = 0;
722
1157
  pi.registerCommand("codex-compact", {
723
- description: "Compact now or configure Codex Remote Compaction V2",
724
- handler: async (_args, ctx) => {
1158
+ description: "Compact now or configure Responses compaction",
1159
+ handler: async (args, ctx) => {
1160
+ if (args.trim()) throw new Error("Usage: /codex-compact");
725
1161
  const ownerGeneration = generation;
726
1162
  const controller = sessionController;
727
- const { showCodexCompactMenu } = await import("./chunks/settings-menu-PEDLURUH.js");
1163
+ const { showCodexCompactMenu } = await import("./chunks/settings-menu-VYQ6ABCP.js");
728
1164
  if (ownerGeneration !== generation || controller.signal.aborted) return;
729
1165
  await showCodexCompactMenu(settingsRuntime, ctx, {
730
1166
  signal: controller.signal,
@@ -746,7 +1182,7 @@ function createCodexCompactExtension(options = {}) {
746
1182
  if (sessionController.signal.aborted || ownerGeneration !== generation) return;
747
1183
  if (ctx.hasUI) {
748
1184
  ctx.ui.notify(
749
- `Could not load pi-codex-compact.json; using defaults. ${error instanceof Error ? error.message : String(error)}`,
1185
+ `Could not load pi-codex-compact.json; using defaults. ${terminalText(error instanceof Error ? error.message : String(error))}`,
750
1186
  "warning"
751
1187
  );
752
1188
  }
@@ -757,14 +1193,21 @@ function createCodexCompactExtension(options = {}) {
757
1193
  }
758
1194
  if (ctx.hasUI && state.kind === "invalid") {
759
1195
  ctx.ui.notify(
760
- `Invalid pi-codex-compact.json; using defaults without overwriting it. ${state.issue}`,
1196
+ `Invalid pi-codex-compact.json; using defaults without overwriting it. ${terminalText(state.issue ?? "unknown validation error")}`,
761
1197
  "warning"
762
1198
  );
763
1199
  }
764
1200
  });
765
1201
  pi.on(
766
1202
  "session_before_compact",
767
- (event, ctx) => compactRemotely(pi, event, ctx, settingsRuntime.get().settings, options.fetch)
1203
+ (event, ctx) => compactRemotely(
1204
+ pi,
1205
+ event,
1206
+ ctx,
1207
+ settingsRuntime.get().settings,
1208
+ sessionController.signal,
1209
+ options.fetch
1210
+ )
768
1211
  );
769
1212
  pi.on("context", (event, ctx) => {
770
1213
  if (!settingsRuntime.get().settings.enabled) return void 0;
@@ -794,7 +1237,7 @@ function createCodexCompactExtension(options = {}) {
794
1237
  providerWarnings.add(key);
795
1238
  if (ctx.hasUI) {
796
1239
  ctx.ui.notify(
797
- "The active Codex checkpoint cannot replay on this model; Pi will expose only its fallback marker and retained recent messages.",
1240
+ "The active Responses checkpoint cannot replay on this model; Pi will expose only its fallback marker and retained recent messages.",
798
1241
  "warning"
799
1242
  );
800
1243
  }