@opengeni/runtime 0.14.1 → 0.14.15

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.
Files changed (79) hide show
  1. package/dist/{chunk-4YJ5Z52Z.js → chunk-7E2ZSCZK.js} +6 -6
  2. package/dist/{chunk-4YJ5Z52Z.js.map → chunk-7E2ZSCZK.js.map} +1 -1
  3. package/dist/{chunk-HHWIZHQM.js → chunk-GNEGANUF.js} +942 -300
  4. package/dist/chunk-GNEGANUF.js.map +1 -0
  5. package/dist/codex-tool-search.d.ts +100 -0
  6. package/dist/context-compaction.d.ts +236 -0
  7. package/dist/history-sanitizer.d.ts +320 -0
  8. package/dist/index.d.ts +186 -718
  9. package/dist/index.js +612 -126
  10. package/dist/index.js.map +1 -1
  11. package/dist/{localSources-T3HZ6KY4.js → localSources-BXHBKGF4.js} +2 -2
  12. package/dist/mcp-network.d.ts +32 -35
  13. package/dist/metrics.d.ts +33 -0
  14. package/dist/sandbox/capabilities.d.ts +11 -0
  15. package/dist/sandbox/channel-a.d.ts +226 -0
  16. package/dist/sandbox/display-stack.d.ts +99 -0
  17. package/dist/sandbox/errors.d.ts +16 -0
  18. package/dist/sandbox/exec-banner.d.ts +19 -0
  19. package/dist/sandbox/index.d.ts +190 -6
  20. package/dist/sandbox/index.js +20 -4
  21. package/dist/sandbox/op-correlation.d.ts +24 -0
  22. package/dist/sandbox/provider-errors.d.ts +32 -0
  23. package/dist/sandbox/provider-operation-gate.d.ts +12 -0
  24. package/dist/sandbox/providers/blaxel.d.ts +2 -0
  25. package/dist/sandbox/providers/cloudflare.d.ts +2 -0
  26. package/dist/sandbox/providers/daytona.d.ts +2 -0
  27. package/dist/sandbox/providers/docker.d.ts +2 -0
  28. package/dist/sandbox/providers/e2b.d.ts +2 -0
  29. package/dist/sandbox/providers/index.d.ts +11 -0
  30. package/dist/sandbox/providers/local.d.ts +2 -0
  31. package/dist/sandbox/providers/modal.d.ts +132 -0
  32. package/dist/sandbox/providers/none.d.ts +2 -0
  33. package/dist/sandbox/providers/runloop.d.ts +2 -0
  34. package/dist/sandbox/providers/selfhosted.d.ts +2 -0
  35. package/dist/sandbox/providers/types.d.ts +30 -0
  36. package/dist/sandbox/providers/vercel.d.ts +2 -0
  37. package/dist/sandbox/recording.d.ts +80 -0
  38. package/dist/sandbox/routing/backend-resolver.d.ts +173 -0
  39. package/dist/sandbox/routing/routing-session.d.ts +401 -0
  40. package/dist/sandbox/run-credentials.d.ts +65 -0
  41. package/dist/sandbox/select.d.ts +120 -0
  42. package/dist/sandbox/selfhosted/capabilities.d.ts +91 -0
  43. package/dist/sandbox/selfhosted/control-rpc.d.ts +175 -0
  44. package/dist/sandbox/selfhosted/fault-rendering.d.ts +40 -0
  45. package/dist/sandbox/selfhosted/op-observer.d.ts +40 -0
  46. package/dist/sandbox/selfhosted/op-stream.d.ts +104 -0
  47. package/dist/sandbox/selfhosted/op-testing.d.ts +108 -0
  48. package/dist/sandbox/selfhosted/op-transport.d.ts +60 -0
  49. package/dist/sandbox/selfhosted/retry-policy.d.ts +106 -0
  50. package/dist/sandbox/selfhosted/session.d.ts +552 -0
  51. package/dist/sandbox/selfhosted/testing.d.ts +48 -0
  52. package/dist/sandbox/stream-port.d.ts +80 -0
  53. package/dist/sandbox/stream-token.d.ts +44 -0
  54. package/dist/sandbox/terminal-server.d.ts +67 -0
  55. package/dist/sandbox/toolspace-token.d.ts +23 -0
  56. package/dist/sandbox/turn-tool-cancellation.d.ts +72 -0
  57. package/dist/sandbox/workspace-archive.d.ts +39 -0
  58. package/dist/sandbox-computer.d.ts +227 -0
  59. package/dist/screenshot-error-card.d.ts +1 -0
  60. package/dist/skill-library.d.ts +10 -12
  61. package/dist/usage-telemetry.d.ts +45 -0
  62. package/dist/workspace-skills.d.ts +31 -0
  63. package/package.json +7 -7
  64. package/src/context-compaction.ts +600 -6
  65. package/src/history-sanitizer.ts +10 -92
  66. package/src/index.ts +301 -62
  67. package/src/sandbox/channel-a.ts +267 -81
  68. package/src/sandbox/index.ts +56 -94
  69. package/src/sandbox/provider-operation-gate.ts +110 -0
  70. package/src/sandbox/providers/docker.ts +13 -1
  71. package/src/sandbox/providers/modal.ts +433 -3
  72. package/src/sandbox/routing/routing-session.ts +84 -8
  73. package/src/sandbox/selfhosted/session.ts +7 -6
  74. package/src/sandbox/turn-tool-cancellation.ts +47 -7
  75. package/src/sandbox/workspace-archive.ts +176 -73
  76. package/src/sandbox-computer.ts +25 -75
  77. package/dist/chunk-HHWIZHQM.js.map +0 -1
  78. package/dist/index-BraazrAO.d.ts +0 -2931
  79. /package/dist/{localSources-T3HZ6KY4.js.map → localSources-BXHBKGF4.js.map} +0 -0
@@ -27,6 +27,10 @@ export const SUMMARY_BUFFER_TOKENS = 20_000;
27
27
  // A single cumulative budget for all retained real user messages, matching
28
28
  // Codex core's build_compacted_history_with_limit (not a per-message allowance).
29
29
  export const COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000;
30
+ /** Codex CLI remote compaction v2 retained-message budget (codex-rs). */
31
+ export const REMOTE_V2_RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
32
+ export const REMOTE_COMPACTION_V2_IMPLEMENTATION = "responses_compaction_v2" as const;
33
+ export const REMOTE_COMPACTION_V2_BETA_FEATURE = "remote_compaction_v2" as const;
30
34
  // 0.9: compact as LATE as possible — retained context is worth more than early
31
35
  // headroom now that declared per-model windows are honest. Model-catalog
32
36
  // explicit limits take precedence; the ratio is used for models without one.
@@ -118,15 +122,480 @@ export function estimateTextTokens(text: string): number {
118
122
  return nonAsciiCodeUnits + Math.ceil(asciiCodeUnits / 4);
119
123
  }
120
124
 
121
- /** Serialized-item estimate for pre-call accounting and retained user budgets. */
122
- export function estimateItemTokens(item: CompactionItem): number {
125
+ const IMAGE_LOW_DETAIL_TOKENS = 85;
126
+ const IMAGE_HIGH_DETAIL_BASE_TOKENS = 85;
127
+ const IMAGE_HIGH_DETAIL_TILE_TOKENS = 170;
128
+ const IMAGE_HIGH_DETAIL_TILE_SIZE = 512;
129
+ const IMAGE_HIGH_DETAIL_MAX_SIDE = 2_048;
130
+ const IMAGE_HIGH_DETAIL_TARGET_SHORT_SIDE = 768;
131
+ export const UNKNOWN_IMAGE_TOKENS = 4_096;
132
+ export const MAX_NATIVE_IMAGE_TOKENS = 8_192;
133
+
134
+ export type NativeImageEstimateReason = "dimensions" | "bounded_fallback";
135
+ export type NativeImageTokenEstimate = {
136
+ tokens: number;
137
+ width: number | null;
138
+ height: number | null;
139
+ detail: string;
140
+ reason: NativeImageEstimateReason;
141
+ };
142
+
143
+ export type ModelInputTokenBreakdown = {
144
+ totalTokens: number;
145
+ textTokens: number;
146
+ imageTokens: number;
147
+ imageCount: number;
148
+ imageFallbackCount: number;
149
+ };
150
+
151
+ type ImageDescriptor = {
152
+ source: unknown;
153
+ detail: string;
154
+ width: number | null;
155
+ height: number | null;
156
+ };
157
+
158
+ type NativeImageProjection = {
159
+ value: unknown;
160
+ imageTokens: number;
161
+ imageCount: number;
162
+ imageFallbackCount: number;
163
+ };
164
+
165
+ function finitePositiveInteger(value: unknown): number | null {
166
+ return typeof value === "number" && Number.isFinite(value) && value > 0
167
+ ? Math.floor(value)
168
+ : null;
169
+ }
170
+
171
+ function dimensionsFromRecord(record: Record<string, unknown>): [number, number] | null {
172
+ const width = finitePositiveInteger(record.width);
173
+ const height = finitePositiveInteger(record.height);
174
+ if (width && height) return [width, height];
175
+ if (Array.isArray(record.dimensions) && record.dimensions.length >= 2) {
176
+ const dimensionsWidth = finitePositiveInteger(record.dimensions[0]);
177
+ const dimensionsHeight = finitePositiveInteger(record.dimensions[1]);
178
+ if (dimensionsWidth && dimensionsHeight) return [dimensionsWidth, dimensionsHeight];
179
+ }
180
+ return null;
181
+ }
182
+
183
+ function dataUrlBytesPrefix(value: string, maxBytes = 262_144): Uint8Array | null {
184
+ const match = /^data:image\/[a-z0-9.+-]+;base64,([a-z0-9+/=\r\n]+)$/i.exec(value);
185
+ if (!match) return null;
186
+ try {
187
+ const maxBase64CodeUnits = Math.ceil(maxBytes / 3) * 4;
188
+ return new Uint8Array(Buffer.from(match[1]!.slice(0, maxBase64CodeUnits), "base64"));
189
+ } catch {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ function base64BytesPrefix(value: string, maxBytes = 262_144): Uint8Array | null {
195
+ if (!/^[a-z0-9+/=\r\n]+$/i.test(value)) return null;
196
+ try {
197
+ const maxBase64CodeUnits = Math.ceil(maxBytes / 3) * 4;
198
+ return new Uint8Array(Buffer.from(value.slice(0, maxBase64CodeUnits), "base64"));
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+
204
+ function uint16BigEndian(bytes: Uint8Array, offset: number): number {
205
+ return ((bytes[offset] ?? 0) << 8) | (bytes[offset + 1] ?? 0);
206
+ }
207
+
208
+ function uint32BigEndian(bytes: Uint8Array, offset: number): number {
209
+ return (
210
+ ((bytes[offset] ?? 0) * 0x1000000 +
211
+ ((bytes[offset + 1] ?? 0) << 16) +
212
+ ((bytes[offset + 2] ?? 0) << 8) +
213
+ (bytes[offset + 3] ?? 0)) >>>
214
+ 0
215
+ );
216
+ }
217
+
218
+ function crc32(bytes: Uint8Array, start: number, end: number): number {
219
+ let crc = 0xffff_ffff;
220
+ for (let offset = start; offset < end; offset += 1) {
221
+ crc ^= bytes[offset] ?? 0;
222
+ for (let bit = 0; bit < 8; bit += 1) {
223
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb8_8320 : 0);
224
+ }
225
+ }
226
+ return (crc ^ 0xffff_ffff) >>> 0;
227
+ }
228
+
229
+ function dimensionsFromImageBytes(bytes: Uint8Array): [number, number] | null {
230
+ // PNG IHDR: trust geometry only from a complete first IHDR chunk whose CRC32
231
+ // authenticates the 4-byte chunk type plus 13-byte data. This inspects exactly
232
+ // the bounded 33-byte prefix and never decodes the rest of the image.
233
+ if (
234
+ bytes.length >= 33 &&
235
+ bytes[0] === 0x89 &&
236
+ bytes[1] === 0x50 &&
237
+ bytes[2] === 0x4e &&
238
+ bytes[3] === 0x47 &&
239
+ bytes[4] === 0x0d &&
240
+ bytes[5] === 0x0a &&
241
+ bytes[6] === 0x1a &&
242
+ bytes[7] === 0x0a &&
243
+ uint32BigEndian(bytes, 8) === 13 &&
244
+ bytes[12] === 0x49 &&
245
+ bytes[13] === 0x48 &&
246
+ bytes[14] === 0x44 &&
247
+ bytes[15] === 0x52 &&
248
+ crc32(bytes, 12, 29) === uint32BigEndian(bytes, 29)
249
+ ) {
250
+ const width = uint32BigEndian(bytes, 16);
251
+ const height = uint32BigEndian(bytes, 20);
252
+ return width > 0 && height > 0 ? [width, height] : null;
253
+ }
254
+ // GIF logical screen descriptor.
255
+ if (
256
+ bytes.length >= 10 &&
257
+ bytes[0] === 0x47 &&
258
+ bytes[1] === 0x49 &&
259
+ bytes[2] === 0x46 &&
260
+ bytes[3] === 0x38
261
+ ) {
262
+ const width = (bytes[6] ?? 0) | ((bytes[7] ?? 0) << 8);
263
+ const height = (bytes[8] ?? 0) | ((bytes[9] ?? 0) << 8);
264
+ return width > 0 && height > 0 ? [width, height] : null;
265
+ }
266
+ // WebP VP8X canvas size (24-bit little-endian, stored minus one).
267
+ if (
268
+ bytes.length >= 30 &&
269
+ bytes[0] === 0x52 &&
270
+ bytes[1] === 0x49 &&
271
+ bytes[2] === 0x46 &&
272
+ bytes[3] === 0x46 &&
273
+ bytes[8] === 0x57 &&
274
+ bytes[9] === 0x45 &&
275
+ bytes[10] === 0x42 &&
276
+ bytes[11] === 0x50 &&
277
+ bytes[12] === 0x56 &&
278
+ bytes[13] === 0x50 &&
279
+ bytes[14] === 0x38 &&
280
+ bytes[15] === 0x58
281
+ ) {
282
+ const width = 1 + (bytes[24] ?? 0) + ((bytes[25] ?? 0) << 8) + ((bytes[26] ?? 0) << 16);
283
+ const height = 1 + (bytes[27] ?? 0) + ((bytes[28] ?? 0) << 8) + ((bytes[29] ?? 0) << 16);
284
+ return width > 0 && height > 0 ? [width, height] : null;
285
+ }
286
+ // JPEG SOF markers can appear after variable-length metadata segments.
287
+ if (bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8) {
288
+ let offset = 2;
289
+ while (offset + 8 < bytes.length) {
290
+ if (bytes[offset] !== 0xff) {
291
+ offset += 1;
292
+ continue;
293
+ }
294
+ const marker = bytes[offset + 1] ?? 0;
295
+ offset += 2;
296
+ if (marker === 0xd8 || marker === 0xd9 || marker === 0x01) continue;
297
+ if (marker >= 0xd0 && marker <= 0xd7) continue;
298
+ if (offset + 2 > bytes.length) break;
299
+ const segmentLength = uint16BigEndian(bytes, offset);
300
+ if (segmentLength < 2 || offset + segmentLength > bytes.length) break;
301
+ const isStartOfFrame =
302
+ (marker >= 0xc0 && marker <= 0xc3) ||
303
+ (marker >= 0xc5 && marker <= 0xc7) ||
304
+ (marker >= 0xc9 && marker <= 0xcb) ||
305
+ (marker >= 0xcd && marker <= 0xcf);
306
+ if (isStartOfFrame && segmentLength >= 7) {
307
+ const height = uint16BigEndian(bytes, offset + 3);
308
+ const width = uint16BigEndian(bytes, offset + 5);
309
+ return width > 0 && height > 0 ? [width, height] : null;
310
+ }
311
+ offset += segmentLength;
312
+ }
313
+ }
314
+ return null;
315
+ }
316
+
317
+ function dimensionsFromImageSource(source: unknown): [number, number] | null {
318
+ if (source instanceof Uint8Array) {
319
+ return dimensionsFromImageBytes(source);
320
+ }
321
+ if (typeof source === "string") {
322
+ const bytes = dataUrlBytesPrefix(source);
323
+ return bytes ? dimensionsFromImageBytes(bytes) : null;
324
+ }
325
+ if (!source || typeof source !== "object") return null;
326
+ const record = source as Record<string, unknown>;
327
+ const rawData = record.data;
328
+ const mediaType = record.mediaType ?? record.media_type;
329
+ const rawDataDimensions =
330
+ rawData instanceof Uint8Array
331
+ ? dimensionsFromImageBytes(rawData)
332
+ : typeof rawData === "string" &&
333
+ typeof mediaType === "string" &&
334
+ mediaType.toLowerCase().startsWith("image/")
335
+ ? dimensionsFromImageBytes(base64BytesPrefix(rawData) ?? new Uint8Array())
336
+ : null;
337
+ return (
338
+ dimensionsFromRecord(record) ??
339
+ rawDataDimensions ??
340
+ dimensionsFromImageSource(record.url) ??
341
+ dimensionsFromImageSource(rawData)
342
+ );
343
+ }
344
+
345
+ function highDetailImageTokens(width: number, height: number): number {
346
+ let scaledWidth = width;
347
+ let scaledHeight = height;
348
+ const longest = Math.max(scaledWidth, scaledHeight);
349
+ if (longest > IMAGE_HIGH_DETAIL_MAX_SIDE) {
350
+ const scale = IMAGE_HIGH_DETAIL_MAX_SIDE / longest;
351
+ scaledWidth *= scale;
352
+ scaledHeight *= scale;
353
+ }
354
+ const shortest = Math.min(scaledWidth, scaledHeight);
355
+ if (shortest > IMAGE_HIGH_DETAIL_TARGET_SHORT_SIDE) {
356
+ const scale = IMAGE_HIGH_DETAIL_TARGET_SHORT_SIDE / shortest;
357
+ scaledWidth *= scale;
358
+ scaledHeight *= scale;
359
+ }
360
+ const tiles =
361
+ Math.ceil(scaledWidth / IMAGE_HIGH_DETAIL_TILE_SIZE) *
362
+ Math.ceil(scaledHeight / IMAGE_HIGH_DETAIL_TILE_SIZE);
363
+ return Math.min(
364
+ MAX_NATIVE_IMAGE_TOKENS,
365
+ IMAGE_HIGH_DETAIL_BASE_TOKENS + IMAGE_HIGH_DETAIL_TILE_TOKENS * Math.max(1, tiles),
366
+ );
367
+ }
368
+
369
+ /**
370
+ * Provider-neutral local estimate for one native image. Inline bytes are used
371
+ * only to recover dimensions; their base64 length never becomes text tokens.
372
+ */
373
+ export function estimateNativeImageTokens(input: {
374
+ source?: unknown;
375
+ width?: number | null;
376
+ height?: number | null;
377
+ detail?: unknown;
378
+ }): NativeImageTokenEstimate {
379
+ const explicitWidth = finitePositiveInteger(input.width);
380
+ const explicitHeight = finitePositiveInteger(input.height);
381
+ const sourceDimensions = dimensionsFromImageSource(input.source);
382
+ const width = explicitWidth ?? sourceDimensions?.[0] ?? null;
383
+ const height = explicitHeight ?? sourceDimensions?.[1] ?? null;
384
+ const detail =
385
+ typeof input.detail === "string" && input.detail.length > 0 ? input.detail : "auto";
386
+ if (detail === "low") {
387
+ return { tokens: IMAGE_LOW_DETAIL_TOKENS, width, height, detail, reason: "dimensions" };
388
+ }
389
+ if (!width || !height) {
390
+ return {
391
+ tokens: UNKNOWN_IMAGE_TOKENS,
392
+ width: null,
393
+ height: null,
394
+ detail,
395
+ reason: "bounded_fallback",
396
+ };
397
+ }
398
+ return {
399
+ tokens: highDetailImageTokens(width, height),
400
+ width,
401
+ height,
402
+ detail,
403
+ reason: "dimensions",
404
+ };
405
+ }
406
+
407
+ function imageDescriptor(record: Record<string, unknown>): ImageDescriptor | null {
408
+ const type = typeof record.type === "string" ? record.type : "";
409
+ if (!new Set(["input_image", "image_url", "image", "computer_screenshot"]).has(type)) {
410
+ return null;
411
+ }
412
+ const imageUrl = record.image_url;
413
+ const nestedImageUrl =
414
+ imageUrl && typeof imageUrl === "object" ? (imageUrl as Record<string, unknown>).url : imageUrl;
415
+ const source =
416
+ record.image ??
417
+ record.imageUrl ??
418
+ nestedImageUrl ??
419
+ record.data ??
420
+ record.url ??
421
+ record.fileId ??
422
+ record.file_id ??
423
+ record.artifact ??
424
+ record.artifactReference;
425
+ const nestedDetail =
426
+ imageUrl && typeof imageUrl === "object"
427
+ ? (imageUrl as Record<string, unknown>).detail
428
+ : undefined;
429
+ const dimensions =
430
+ dimensionsFromRecord(record) ??
431
+ (source && typeof source === "object"
432
+ ? dimensionsFromRecord(source as Record<string, unknown>)
433
+ : null);
434
+ return {
435
+ source,
436
+ detail:
437
+ typeof record.detail === "string"
438
+ ? record.detail
439
+ : typeof nestedDetail === "string"
440
+ ? nestedDetail
441
+ : "auto",
442
+ width: dimensions?.[0] ?? null,
443
+ height: dimensions?.[1] ?? null,
444
+ };
445
+ }
446
+
447
+ function projectNativeImages(value: unknown, seen: WeakSet<object>): NativeImageProjection {
448
+ if (!value || typeof value !== "object") {
449
+ return { value, imageTokens: 0, imageCount: 0, imageFallbackCount: 0 };
450
+ }
451
+ if (seen.has(value)) {
452
+ return { value: "[circular]", imageTokens: 0, imageCount: 0, imageFallbackCount: 0 };
453
+ }
454
+ seen.add(value);
455
+ if (Array.isArray(value)) {
456
+ let imageTokens = 0;
457
+ let imageCount = 0;
458
+ let imageFallbackCount = 0;
459
+ const projected = value.map((entry) => {
460
+ const result = projectNativeImages(entry, seen);
461
+ imageTokens += result.imageTokens;
462
+ imageCount += result.imageCount;
463
+ imageFallbackCount += result.imageFallbackCount;
464
+ return result.value;
465
+ });
466
+ seen.delete(value);
467
+ return { value: projected, imageTokens, imageCount, imageFallbackCount };
468
+ }
469
+
470
+ const record = value as Record<string, unknown>;
471
+ const descriptor = imageDescriptor(record);
472
+ if (descriptor) {
473
+ const estimate = estimateNativeImageTokens(descriptor);
474
+ const projected: Record<string, unknown> = {};
475
+ const payloadKeys = new Set([
476
+ "image",
477
+ "image_url",
478
+ "imageUrl",
479
+ "data",
480
+ "url",
481
+ "fileId",
482
+ "file_id",
483
+ "artifact",
484
+ "artifactReference",
485
+ ]);
486
+ for (const [key, entry] of Object.entries(record)) {
487
+ projected[key] = payloadKeys.has(key) ? "[native image]" : entry;
488
+ }
489
+ projected.image_estimate = {
490
+ width: estimate.width,
491
+ height: estimate.height,
492
+ detail: estimate.detail,
493
+ reason: estimate.reason,
494
+ };
495
+ seen.delete(value);
496
+ return {
497
+ value: projected,
498
+ imageTokens: estimate.tokens,
499
+ imageCount: 1,
500
+ imageFallbackCount: estimate.reason === "bounded_fallback" ? 1 : 0,
501
+ };
502
+ }
503
+
504
+ let imageTokens = 0;
505
+ let imageCount = 0;
506
+ let imageFallbackCount = 0;
507
+ const projected: Record<string, unknown> = {};
508
+ for (const [key, entry] of Object.entries(record)) {
509
+ const result = projectNativeImages(entry, seen);
510
+ projected[key] = result.value;
511
+ imageTokens += result.imageTokens;
512
+ imageCount += result.imageCount;
513
+ imageFallbackCount += result.imageFallbackCount;
514
+ }
515
+ seen.delete(value);
516
+ return { value: projected, imageTokens, imageCount, imageFallbackCount };
517
+ }
518
+
519
+ /**
520
+ * Codex CLI `estimate_reasoning_length`: opaque encrypted payloads are not
521
+ * model-visible as raw JSON/base64. Visible bytes ≈ `len * 3/4 - 650`.
522
+ */
523
+ export function estimateOpaqueEncryptedModelVisibleBytes(encodedLen: number): number {
524
+ const length = Math.max(0, Math.floor(encodedLen));
525
+ return Math.max(0, Math.floor((length * 3) / 4) - 650);
526
+ }
527
+
528
+ /** Codex CLI bytes→tokens for opaque encrypted content (`ceil(bytes / 4)`). */
529
+ export function estimateOpaqueEncryptedTokens(encodedLen: number): number {
530
+ const bytes = estimateOpaqueEncryptedModelVisibleBytes(encodedLen);
531
+ return bytes === 0 ? 0 : Math.ceil(bytes / 4);
532
+ }
533
+
534
+ /**
535
+ * Length of opaque encrypted content that must use the Codex encrypted
536
+ * estimator instead of JSON.stringify (compaction blobs and reasoning).
537
+ */
538
+ export function opaqueEncryptedContentLength(item: unknown): number | null {
539
+ if (!item || typeof item !== "object") return null;
540
+ const record = item as Record<string, unknown>;
541
+ const type = typeof record.type === "string" ? record.type : "";
542
+ if (
543
+ (type === "compaction" || type === "context_compaction" || type === "reasoning") &&
544
+ typeof record.encrypted_content === "string" &&
545
+ record.encrypted_content.length > 0
546
+ ) {
547
+ return record.encrypted_content.length;
548
+ }
549
+ if (type === "reasoning") {
550
+ const providerData =
551
+ record.providerData && typeof record.providerData === "object"
552
+ ? (record.providerData as Record<string, unknown>)
553
+ : null;
554
+ const nested =
555
+ providerData && typeof providerData.encrypted_content === "string"
556
+ ? providerData.encrypted_content
557
+ : providerData && typeof providerData.encryptedContent === "string"
558
+ ? providerData.encryptedContent
559
+ : null;
560
+ if (nested && nested.length > 0) return nested.length;
561
+ }
562
+ return null;
563
+ }
564
+
565
+ /** Native-image-aware item estimate for pre-call accounting and retained budgets. */
566
+ export function estimateItemTokenBreakdown(item: CompactionItem): ModelInputTokenBreakdown {
567
+ const opaqueLen = opaqueEncryptedContentLength(item);
568
+ if (opaqueLen !== null) {
569
+ // Match Codex: Compaction / encrypted reasoning use the opaque byte
570
+ // heuristic only — never JSON-stringify the ciphertext into the budget.
571
+ const textTokens = estimateOpaqueEncryptedTokens(opaqueLen);
572
+ return {
573
+ totalTokens: textTokens,
574
+ textTokens,
575
+ imageTokens: 0,
576
+ imageCount: 0,
577
+ imageFallbackCount: 0,
578
+ };
579
+ }
580
+ const projected = projectNativeImages(item, new WeakSet<object>());
123
581
  let text: string;
124
582
  try {
125
- text = JSON.stringify(item);
583
+ text = JSON.stringify(projected.value);
126
584
  } catch {
127
- text = String(item);
585
+ text = String(projected.value);
128
586
  }
129
- return estimateTextTokens(text);
587
+ const textTokens = estimateTextTokens(text);
588
+ return {
589
+ totalTokens: textTokens + projected.imageTokens,
590
+ textTokens,
591
+ imageTokens: projected.imageTokens,
592
+ imageCount: projected.imageCount,
593
+ imageFallbackCount: projected.imageFallbackCount,
594
+ };
595
+ }
596
+
597
+ export function estimateItemTokens(item: CompactionItem): number {
598
+ return estimateItemTokenBreakdown(item).totalTokens;
130
599
  }
131
600
 
132
601
  export function estimateTokens(items: readonly CompactionItem[]): number {
@@ -137,6 +606,27 @@ export function estimateTokens(items: readonly CompactionItem[]): number {
137
606
  return total;
138
607
  }
139
608
 
609
+ export function estimateTokensBreakdown(
610
+ items: readonly CompactionItem[],
611
+ ): ModelInputTokenBreakdown {
612
+ const total: ModelInputTokenBreakdown = {
613
+ totalTokens: 0,
614
+ textTokens: 0,
615
+ imageTokens: 0,
616
+ imageCount: 0,
617
+ imageFallbackCount: 0,
618
+ };
619
+ for (const item of items) {
620
+ const estimate = estimateItemTokenBreakdown(item);
621
+ total.totalTokens += estimate.totalTokens;
622
+ total.textTokens += estimate.textTokens;
623
+ total.imageTokens += estimate.imageTokens;
624
+ total.imageCount += estimate.imageCount;
625
+ total.imageFallbackCount += estimate.imageFallbackCount;
626
+ }
627
+ return total;
628
+ }
629
+
140
630
  export function estimateSerializedValueTokens(value: unknown): number {
141
631
  let serialized: string;
142
632
  try {
@@ -164,6 +654,10 @@ export type CompleteModelInputEstimate = {
164
654
  tokens: number;
165
655
  source: "complete_estimate" | "provider_plus_local";
166
656
  inputTokens: number;
657
+ inputTextTokens: number;
658
+ inputImageTokens: number;
659
+ inputImageCount: number;
660
+ inputImageFallbackCount: number;
167
661
  instructionsTokens: number;
168
662
  toolSchemaTokens: number;
169
663
  appendedAfterModelTokens: number;
@@ -182,7 +676,8 @@ export function estimateCompleteModelInput(input: {
182
676
  provider?: ProviderContextTokenSignal | null;
183
677
  providerRequestFootprint?: CompleteModelInputFootprint | null;
184
678
  }): CompleteModelInputEstimate {
185
- const inputTokens = estimateTokens(input.current.input);
679
+ const inputEstimate = estimateTokensBreakdown(input.current.input);
680
+ const inputTokens = inputEstimate.totalTokens;
186
681
  const instructionsTokens = input.current.instructionsTokens;
187
682
  const toolSchemaTokens = input.current.toolSchemaTokens;
188
683
  if (!input.provider || !input.providerRequestFootprint || input.provider.totalTokens <= 0) {
@@ -190,6 +685,10 @@ export function estimateCompleteModelInput(input: {
190
685
  tokens: inputTokens + instructionsTokens + toolSchemaTokens,
191
686
  source: "complete_estimate",
192
687
  inputTokens,
688
+ inputTextTokens: inputEstimate.textTokens,
689
+ inputImageTokens: inputEstimate.imageTokens,
690
+ inputImageCount: inputEstimate.imageCount,
691
+ inputImageFallbackCount: inputEstimate.imageFallbackCount,
193
692
  instructionsTokens,
194
693
  toolSchemaTokens,
195
694
  appendedAfterModelTokens: 0,
@@ -211,6 +710,10 @@ export function estimateCompleteModelInput(input: {
211
710
  input.provider.totalTokens + appendedAfterModelTokens + instructionGrowth + toolSchemaGrowth,
212
711
  source: "provider_plus_local",
213
712
  inputTokens,
713
+ inputTextTokens: inputEstimate.textTokens,
714
+ inputImageTokens: inputEstimate.imageTokens,
715
+ inputImageCount: inputEstimate.imageCount,
716
+ inputImageFallbackCount: inputEstimate.imageFallbackCount,
214
717
  instructionsTokens,
215
718
  toolSchemaTokens,
216
719
  appendedAfterModelTokens,
@@ -551,6 +1054,97 @@ export function buildCompactionReplacementHistory(
551
1054
  return history;
552
1055
  }
553
1056
 
1057
+ /** True for a Codex remote-compaction output item with opaque encrypted content. */
1058
+ export function isRemoteCompactionItem(item: unknown): item is CompactionItem {
1059
+ if (!item || typeof item !== "object") return false;
1060
+ const record = item as Record<string, unknown>;
1061
+ return (
1062
+ record.type === "compaction" &&
1063
+ typeof record.encrypted_content === "string" &&
1064
+ record.encrypted_content.length > 0
1065
+ );
1066
+ }
1067
+
1068
+ /** Messages retained beside a remote v2 compaction blob (user + developer). */
1069
+ export function isRetainedRemoteV2Message(item: unknown): boolean {
1070
+ if (isCompactionSummary(item) || itemType(item) === "compaction") return false;
1071
+ const role = itemRole(item);
1072
+ return itemType(item) === "message" && (role === "user" || role === "developer");
1073
+ }
1074
+
1075
+ /**
1076
+ * Build the active history after Codex remote compaction v2:
1077
+ * newest retained user/developer messages within the CLI 64k budget plus the
1078
+ * opaque `{ type: "compaction", encrypted_content }` item.
1079
+ */
1080
+ export function buildRemoteV2ReplacementHistory(
1081
+ items: readonly CompactionItem[],
1082
+ compactionItem: CompactionItem,
1083
+ ): CompactionItem[] {
1084
+ if (!isRemoteCompactionItem(compactionItem)) {
1085
+ throw new EmptyCompactionSummaryError({ stage: "remote_v2_compaction_item" });
1086
+ }
1087
+ const retainedReversed: CompactionItem[] = [];
1088
+ let remaining = REMOTE_V2_RETAINED_MESSAGE_TOKEN_BUDGET;
1089
+ for (let index = items.length - 1; index >= 0 && remaining > 0; index -= 1) {
1090
+ const item = items[index]!;
1091
+ if (!isRetainedRemoteV2Message(item)) continue;
1092
+ const textTokens = estimateTextTokens(messageText(item));
1093
+ retainedReversed.push(compactMessageToTokenBudget(item, remaining));
1094
+ if (textTokens > remaining) {
1095
+ remaining = 0;
1096
+ break;
1097
+ }
1098
+ remaining -= textTokens;
1099
+ }
1100
+ const history = retainedReversed.reverse();
1101
+ history.push({
1102
+ type: "compaction",
1103
+ encrypted_content: compactionItem.encrypted_content,
1104
+ ...(typeof compactionItem.summary === "string" ? { summary: compactionItem.summary } : {}),
1105
+ });
1106
+ return history;
1107
+ }
1108
+
1109
+ /**
1110
+ * Append the transient compaction_trigger used only for the remote v2 request.
1111
+ *
1112
+ * The OpenAI Agents SDK's Responses converter rejects a bare
1113
+ * `{ type: "compaction_trigger" }` (`UserError: Unsupported item`). It does
1114
+ * accept `type: "unknown"` and forwards `providerData` onto the wire, which is
1115
+ * how we deliver the Codex-only trigger through CompactionResponsesModel into
1116
+ * the Codex fetch normalizer (which allowlists top-level `compaction_trigger`).
1117
+ */
1118
+ export function buildRemoteCompactionV2PromptInput(
1119
+ items: readonly CompactionItem[],
1120
+ ): CompactionItem[] {
1121
+ return [
1122
+ ...items,
1123
+ {
1124
+ type: "unknown",
1125
+ providerData: { type: "compaction_trigger" },
1126
+ },
1127
+ ];
1128
+ }
1129
+
1130
+ /** Extract exactly one compaction output item from a Responses payload. */
1131
+ export function extractRemoteCompactionV2OutputItem(response: unknown): CompactionItem {
1132
+ if (!response || typeof response !== "object") {
1133
+ throw new EmptyCompactionSummaryError({ stage: "remote_v2_extract", reason: "no_response" });
1134
+ }
1135
+ const record = response as Record<string, unknown>;
1136
+ const output = Array.isArray(record.output) ? record.output : [];
1137
+ const compactionItems = output.filter(isRemoteCompactionItem);
1138
+ if (compactionItems.length !== 1) {
1139
+ throw new EmptyCompactionSummaryError({
1140
+ stage: "remote_v2_extract",
1141
+ reason: "expected_exactly_one_compaction",
1142
+ found: compactionItems.length,
1143
+ });
1144
+ }
1145
+ return compactionItems[0]!;
1146
+ }
1147
+
554
1148
  export function compactionReplacementFingerprint(items: readonly CompactionItem[]): string {
555
1149
  // PostgreSQL JSONB does not preserve JavaScript object-key insertion order.
556
1150
  // Canonicalize recursively so a replacement has the same identity before