@bitkyc08/opencodex 2.8.0 → 2.9.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.
Files changed (175) hide show
  1. package/README.md +24 -0
  2. package/bin/ocx.mjs +32 -4
  3. package/gui/dist/assets/index-CHwf3tTD.css +1 -0
  4. package/gui/dist/assets/index-u5eFOv2y.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-image-normalize.ts +114 -20
  8. package/src/adapters/anthropic.ts +126 -10
  9. package/src/adapters/azure.ts +3 -3
  10. package/src/adapters/base.ts +7 -3
  11. package/src/adapters/cursor/discovery.ts +14 -4
  12. package/src/adapters/cursor/effort-map.ts +18 -6
  13. package/src/adapters/cursor/framing.ts +102 -27
  14. package/src/adapters/cursor/kv-store.ts +30 -3
  15. package/src/adapters/cursor/live-models.ts +22 -2
  16. package/src/adapters/cursor/live-transport.ts +245 -49
  17. package/src/adapters/cursor/mcp-manager.ts +105 -8
  18. package/src/adapters/cursor/native-exec-mcp.ts +5 -3
  19. package/src/adapters/cursor/native-exec-shell.ts +296 -14
  20. package/src/adapters/cursor/native-exec.ts +381 -33
  21. package/src/adapters/cursor/protobuf-events.ts +28 -1
  22. package/src/adapters/cursor/protobuf-request.ts +71 -39
  23. package/src/adapters/cursor/request-builder.ts +2 -2
  24. package/src/adapters/cursor/transport.ts +2 -0
  25. package/src/adapters/cursor.ts +13 -2
  26. package/src/adapters/google-antigravity-replay.ts +184 -17
  27. package/src/adapters/google.ts +58 -8
  28. package/src/adapters/kiro-thinking.ts +23 -9
  29. package/src/adapters/kiro-tools.ts +49 -18
  30. package/src/adapters/kiro.ts +377 -133
  31. package/src/adapters/mimo-free.ts +36 -4
  32. package/src/adapters/openai-chat.ts +143 -17
  33. package/src/adapters/openai-responses.ts +130 -14
  34. package/src/adapters/run-turn-queue.ts +7 -1
  35. package/src/bridge.ts +466 -69
  36. package/src/chat/outbound.ts +144 -38
  37. package/src/claude/inbound-debug.ts +53 -8
  38. package/src/claude/outbound.ts +224 -38
  39. package/src/cli/agent-driven.ts +34 -1
  40. package/src/cli/catalog-prewarm.ts +5 -2
  41. package/src/cli/claude-desktop.ts +2 -2
  42. package/src/cli/doctor.ts +12 -0
  43. package/src/cli/export-command.ts +187 -0
  44. package/src/cli/help.ts +11 -0
  45. package/src/cli/index.ts +13 -3
  46. package/src/cli/init.ts +129 -102
  47. package/src/cli/opencode.ts +36 -151
  48. package/src/cli/star-prompt.ts +13 -4
  49. package/src/cli/status-oauth.ts +12 -2
  50. package/src/clients/config-export.ts +377 -0
  51. package/src/codex/account-runtime-state.ts +19 -1
  52. package/src/codex/account-store.ts +162 -82
  53. package/src/codex/auth-api.ts +467 -159
  54. package/src/codex/auth-context.ts +15 -2
  55. package/src/codex/catalog/aggregation.ts +15 -0
  56. package/src/codex/catalog/effort.ts +16 -6
  57. package/src/codex/catalog/metadata.ts +6 -0
  58. package/src/codex/catalog/parsing.ts +3 -1
  59. package/src/codex/catalog/provider-fetch.ts +29 -0
  60. package/src/codex/catalog/sync.ts +64 -7
  61. package/src/codex/catalog.ts +2 -2
  62. package/src/codex/inject.ts +5 -5
  63. package/src/codex/main-account-cache.ts +8 -1
  64. package/src/codex/model-cache.ts +81 -2
  65. package/src/codex/pool-rotation.ts +39 -0
  66. package/src/codex/project-config-warnings.ts +12 -1
  67. package/src/codex/quota.ts +35 -3
  68. package/src/codex/routing.ts +46 -1
  69. package/src/codex/shim.ts +10 -4
  70. package/src/codex/subagent-model-fallback.ts +12 -0
  71. package/src/codex/websocket-registry.ts +27 -0
  72. package/src/combos/failover.ts +31 -1
  73. package/src/combos/request.ts +9 -0
  74. package/src/combos/resolve.ts +60 -4
  75. package/src/combos/types.ts +12 -0
  76. package/src/config.ts +510 -55
  77. package/src/github/star-state.ts +13 -1
  78. package/src/images/fulfill.ts +39 -1
  79. package/src/images/loop.ts +52 -12
  80. package/src/lib/admission.ts +83 -0
  81. package/src/lib/app-owned-memory-stores.ts +173 -0
  82. package/src/lib/app-owned-memory.ts +265 -0
  83. package/src/lib/bun-stream-caps.ts +31 -7
  84. package/src/lib/config-ownership.ts +33 -0
  85. package/src/lib/crash-guard.ts +65 -5
  86. package/src/lib/debug-log-buffer.ts +47 -6
  87. package/src/lib/destination-policy.ts +12 -1
  88. package/src/lib/errors.ts +3 -0
  89. package/src/lib/gcp-adc.ts +40 -2
  90. package/src/lib/injection-debug-log.ts +26 -2
  91. package/src/lib/provider-outbound.ts +3 -0
  92. package/src/lib/sidecar-tracker.ts +5 -2
  93. package/src/lib/sse-decoder.ts +257 -37
  94. package/src/lib/state-store-registrations.ts +109 -0
  95. package/src/lib/state-store-sweeper.ts +184 -0
  96. package/src/lib/translator-budget.ts +356 -0
  97. package/src/lib/windows-secret-acl.ts +33 -12
  98. package/src/lib/winsw.ts +14 -1
  99. package/src/oauth/anthropic-routing.ts +31 -7
  100. package/src/oauth/google-antigravity.ts +2 -1
  101. package/src/oauth/health.ts +30 -12
  102. package/src/oauth/index.ts +127 -23
  103. package/src/oauth/kiro-credentials.ts +72 -1
  104. package/src/oauth/kiro.ts +23 -4
  105. package/src/oauth/store.ts +165 -18
  106. package/src/oauth/token-guardian.ts +43 -4
  107. package/src/oauth/types.ts +2 -1
  108. package/src/providers/base-url-choices.ts +10 -0
  109. package/src/providers/derive.ts +12 -0
  110. package/src/providers/free-directory.ts +4 -1
  111. package/src/providers/key-failover.ts +12 -0
  112. package/src/providers/openai-sidecar.ts +4 -1
  113. package/src/providers/quota.ts +68 -7
  114. package/src/providers/registry.ts +279 -3
  115. package/src/responses/parser.ts +5 -1
  116. package/src/responses/spill-store.ts +394 -0
  117. package/src/responses/state.ts +520 -102
  118. package/src/router.ts +18 -1
  119. package/src/server/adapter-resolve.ts +20 -3
  120. package/src/server/auth-cors.ts +121 -28
  121. package/src/server/chat-completions.ts +57 -12
  122. package/src/server/claude-messages.ts +85 -13
  123. package/src/server/index.ts +242 -100
  124. package/src/server/lifecycle.ts +155 -25
  125. package/src/server/management/agent-settings-routes.ts +79 -36
  126. package/src/server/management/api-key-usage.ts +167 -0
  127. package/src/server/management/body.ts +35 -0
  128. package/src/server/management/combo-routes.ts +5 -1
  129. package/src/server/management/config-routes.ts +42 -12
  130. package/src/server/management/logs-usage-routes.ts +41 -21
  131. package/src/server/management/model-routes.ts +188 -54
  132. package/src/server/management/oauth-account-routes.ts +115 -26
  133. package/src/server/management/provider-routes.ts +56 -6
  134. package/src/server/management/shared.ts +16 -3
  135. package/src/server/management/sidebar-routes.ts +50 -1
  136. package/src/server/management/system-restart.ts +13 -6
  137. package/src/server/management/system-routes.ts +15 -3
  138. package/src/server/management/usage-summary-cache.ts +86 -0
  139. package/src/server/management-api.ts +39 -5
  140. package/src/server/management-auth.ts +65 -14
  141. package/src/server/port-reclaim.ts +58 -12
  142. package/src/server/ports.ts +2 -0
  143. package/src/server/proxy-liveness.ts +60 -14
  144. package/src/server/relay-eager.ts +20 -4
  145. package/src/server/relay.ts +548 -154
  146. package/src/server/request-decompress.ts +51 -4
  147. package/src/server/request-log.ts +134 -15
  148. package/src/server/responses/collaboration.ts +15 -4
  149. package/src/server/responses/compact.ts +3 -0
  150. package/src/server/responses/core.ts +241 -66
  151. package/src/server/responses-image-gen-repair.ts +19 -5
  152. package/src/server/responses-item-id-repair.ts +23 -5
  153. package/src/server/sse-payload-rewrite.ts +71 -12
  154. package/src/server/startup-health-cache.ts +14 -1
  155. package/src/server/system-env.ts +8 -1
  156. package/src/server/windows-tcp-drop.ts +15 -5
  157. package/src/server/ws-bridge.ts +25 -0
  158. package/src/service.ts +179 -13
  159. package/src/storage/policy-job.ts +93 -23
  160. package/src/storage/policy-worker.ts +6 -0
  161. package/src/storage/restore-job.ts +62 -16
  162. package/src/storage/restore-worker.ts +6 -0
  163. package/src/storage/storage-mutation-coordinator.ts +36 -6
  164. package/src/storage/worker-lifecycle.ts +181 -47
  165. package/src/tray/windows.ts +97 -25
  166. package/src/types.ts +39 -6
  167. package/src/update/index.ts +24 -5
  168. package/src/update/job.ts +598 -73
  169. package/src/usage/log.ts +115 -17
  170. package/src/usage/summary.ts +67 -2
  171. package/src/vision/index.ts +112 -22
  172. package/src/web-search/loop.ts +38 -6
  173. package/src/web-search/progress-stream.ts +14 -3
  174. package/gui/dist/assets/index-BDjpkcRN.js +0 -67
  175. package/gui/dist/assets/index-BHsKRFh9.css +0 -1
@@ -10,6 +10,12 @@
10
10
  * - errors: {type:"error", error:{type,message}}; may arrive mid-stream after HTTP 200.
11
11
  */
12
12
  import { isTransientUpstreamStatus } from "../lib/upstream-retry";
13
+ import {
14
+ isTranslatorBudgetExceededError,
15
+ TRANSLATOR_MAX_TURN_BYTES,
16
+ TranslatorBudgetExceededError,
17
+ type TranslatorBudget,
18
+ } from "../lib/translator-budget";
13
19
 
14
20
  type Rec = Record<string, unknown>;
15
21
 
@@ -38,12 +44,12 @@ export function anthropicErrorType(status: number): string {
38
44
  }
39
45
  }
40
46
 
41
- export function anthropicErrorBody(status: number, message: string, type?: string): Rec {
42
- return { type: "error", error: { type: type ?? anthropicErrorType(status), message } };
47
+ export function anthropicErrorBody(status: number, message: string, type?: string, code?: string): Rec {
48
+ return { type: "error", error: { type: type ?? anthropicErrorType(status), message, ...(code ? { code } : {}) } };
43
49
  }
44
50
 
45
- export function anthropicErrorResponse(status: number, message: string, type?: string): Response {
46
- return new Response(JSON.stringify(anthropicErrorBody(status, message, type)), {
51
+ export function anthropicErrorResponse(status: number, message: string, type?: string, code?: string): Response {
52
+ return new Response(JSON.stringify(anthropicErrorBody(status, message, type, code)), {
47
53
  status,
48
54
  headers: { "Content-Type": "application/json" },
49
55
  });
@@ -76,6 +82,17 @@ function sseFrame(name: string, data: Rec): string {
76
82
  return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
77
83
  }
78
84
 
85
+ function appendedUtf8Bytes(previous: string, previousBytes: number, fragment: string): number {
86
+ let nextBytes = previousBytes + Buffer.byteLength(fragment);
87
+ const previousLast = previous.charCodeAt(previous.length - 1);
88
+ const fragmentFirst = fragment.charCodeAt(0);
89
+ if (previousLast >= 0xd800 && previousLast <= 0xdbff
90
+ && fragmentFirst >= 0xdc00 && fragmentFirst <= 0xdfff) {
91
+ nextBytes -= 2;
92
+ }
93
+ return nextBytes;
94
+ }
95
+
79
96
  /**
80
97
  * Claude Code / Anthropic WebSearch domain filters are optional and mutually exclusive.
81
98
  * Empty arrays are rejected ("ambiguous"); both fields together are rejected. Routed models
@@ -167,19 +184,23 @@ interface OpenBlock {
167
184
  /** Buffer WebSearch args and emit one sanitized input_json_delta on close (#381). */
168
185
  bufferWebSearchArgs?: boolean;
169
186
  argsBuf?: string;
187
+ argsBufBytes?: number;
170
188
  webSearchArgsEmitted?: boolean;
189
+ callId?: string;
171
190
  }
172
191
 
173
192
  /** Streaming: Responses SSE bytes -> Anthropic Messages SSE bytes. */
174
193
  export function responsesSseToAnthropicSse(
175
194
  upstream: ReadableStream<Uint8Array>,
176
195
  model: string,
177
- opts?: { pingIntervalMs?: number },
196
+ opts: { pingIntervalMs?: number; translatorBudget: TranslatorBudget },
178
197
  ): ReadableStream<Uint8Array> {
198
+ const translatorBudget = opts.translatorBudget;
179
199
  const pingIntervalMs = opts?.pingIntervalMs ?? 20_000;
180
200
  const decoder = new TextDecoder();
181
201
  const encoder = new TextEncoder();
182
202
  let buffer = "";
203
+ let bufferBytes = 0;
183
204
  let started = false;
184
205
  let terminated = false;
185
206
  let cancelled = false;
@@ -189,10 +210,32 @@ export function responsesSseToAnthropicSse(
189
210
  let webSearchRequests = 0;
190
211
  let pingTimer: ReturnType<typeof setInterval> | undefined;
191
212
  let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
213
+ const utf8SliceBytes = (value: string, start: number, end: number): number => {
214
+ let bytes = 0;
215
+ for (let index = start; index < end; index++) {
216
+ const codePoint = value.codePointAt(index)!;
217
+ if (codePoint <= 0x7f) bytes += 1;
218
+ else if (codePoint <= 0x7ff) bytes += 2;
219
+ else if (codePoint <= 0xffff) bytes += 3;
220
+ else { bytes += 4; index += 1; }
221
+ }
222
+ return bytes;
223
+ };
224
+ const queuedLiveFrameBytes: number[] = [];
225
+ const releaseDeliveredFrame = () => {
226
+ const bytes = queuedLiveFrameBytes.shift();
227
+ if (bytes !== undefined) translatorBudget.releaseRetained(bytes, { kind: "live_transient" });
228
+ };
192
229
 
193
230
  return new ReadableStream<Uint8Array>({
194
231
  start(controller) {
195
- const emit = (name: string, data: Rec) => controller.enqueue(encoder.encode(sseFrame(name, data)));
232
+ const emit = (name: string, data: Rec) => {
233
+ const frame = encoder.encode(sseFrame(name, data));
234
+ const reservation = translatorBudget.reserveTransient(frame.byteLength, { kind: "live_transient" });
235
+ controller.enqueue(frame);
236
+ reservation.commitRetained();
237
+ queuedLiveFrameBytes.push(frame.byteLength);
238
+ };
196
239
  const ensureStarted = () => {
197
240
  if (started) return;
198
241
  started = true;
@@ -234,6 +277,7 @@ export function responsesSseToAnthropicSse(
234
277
  });
235
278
  }
236
279
  emit("content_block_stop", { type: "content_block_stop", index: open.index });
280
+ if (open.callId) translatorBudget.closeCall(open.callId);
237
281
  open = null;
238
282
  };
239
283
  const ensureBlock = (kind: "text" | "thinking") => {
@@ -265,13 +309,26 @@ export function responsesSseToAnthropicSse(
265
309
  // (devlog/_plan/260716_claudecode_hardening/020). On win32 mid-stream socket
266
310
  // resets reach the reader catch (no failed-tail relay) and stay api_error —
267
311
  // same as today, deliberate residual.
268
- const fail = (status: number, message: string, upstreamDerived = false) => {
312
+ const fail = (status: number, message: string, upstreamDerived = false, code?: string) => {
269
313
  if (terminated) return;
270
314
  terminated = true;
315
+ if (code === "translation_buffer_limit") {
316
+ if (open?.callId) translatorBudget.closeCall(open.callId);
317
+ open = null;
318
+ // No normal close frames are valid after overflow. Emit exactly one bounded
319
+ // typed terminal without consulting the exhausted budget.
320
+ controller.enqueue(encoder.encode(sseFrame("error", anthropicErrorBody(
321
+ 413,
322
+ message,
323
+ "request_too_large",
324
+ "translation_buffer_limit",
325
+ ))));
326
+ return;
327
+ }
271
328
  ensureStarted();
272
329
  closeOpenBlock();
273
330
  const type = upstreamDerived && isTransientUpstreamStatus(status) ? "overloaded_error" : undefined;
274
- emit("error", anthropicErrorBody(status, message, type));
331
+ emit("error", anthropicErrorBody(status, message, type, code));
275
332
  };
276
333
 
277
334
  const handleFrame = (eventName: string, data: Rec) => {
@@ -311,21 +368,25 @@ export function responsesSseToAnthropicSse(
311
368
  const index = blockIndex++;
312
369
  const name = typeof item.name === "string" ? item.name : "";
313
370
  const bufferWebSearchArgs = isClaudeWebSearchToolName(name);
371
+ const callId = typeof item.call_id === "string" ? item.call_id : `toolu_${uuid()}`;
314
372
  emit("content_block_start", {
315
373
  type: "content_block_start", index,
316
374
  content_block: {
317
375
  type: "tool_use",
318
- id: typeof item.call_id === "string" ? item.call_id : `toolu_${uuid()}`,
376
+ id: callId,
319
377
  name,
320
378
  input: {},
321
379
  },
322
380
  });
381
+ translatorBudget.openCall(callId);
323
382
  open = {
324
383
  kind: "tool_use",
325
384
  index,
385
+ callId,
326
386
  itemId: typeof item.id === "string" ? item.id : undefined,
327
387
  bufferWebSearchArgs,
328
388
  argsBuf: "",
389
+ argsBufBytes: 0,
329
390
  webSearchArgsEmitted: false,
330
391
  };
331
392
  break;
@@ -334,7 +395,23 @@ export function responsesSseToAnthropicSse(
334
395
  if (typeof data.delta !== "string" || data.delta.length === 0) break;
335
396
  if (!open || open.kind !== "tool_use") break;
336
397
  if (open.bufferWebSearchArgs) {
337
- open.argsBuf = `${open.argsBuf ?? ""}${data.delta}`;
398
+ const previous = open.argsBuf ?? "";
399
+ const previousBytes = open.argsBufBytes ?? 0;
400
+ const nextBytes = appendedUtf8Bytes(previous, previousBytes, data.delta);
401
+ const scope = {
402
+ kind: "tool_args",
403
+ ...(open.callId ? { callId: open.callId } : {}),
404
+ } as const;
405
+ const reservation = translatorBudget.reserveTransient(nextBytes, scope);
406
+ try {
407
+ open.argsBuf = previous + data.delta;
408
+ open.argsBufBytes = nextBytes;
409
+ reservation.commitRetained();
410
+ translatorBudget.releaseRetained(previousBytes, scope);
411
+ } catch (error) {
412
+ reservation.release();
413
+ throw error;
414
+ }
338
415
  break;
339
416
  }
340
417
  emit("content_block_delta", {
@@ -424,11 +501,21 @@ export function responsesSseToAnthropicSse(
424
501
  const response = isRec(data.response) ? data.response : {};
425
502
  const error = isRec(response.error) ? response.error : {};
426
503
  const message = typeof error.message === "string" ? error.message : "upstream request failed";
427
- const status = typeof error.status === "number" ? error.status : 500;
504
+ const code = typeof error.code === "string" ? error.code : undefined;
505
+ if (code === "translation_buffer_limit") {
506
+ throw new TranslatorBudgetExceededError("live_transient", TRANSLATOR_MAX_TURN_BYTES);
507
+ }
508
+ const status = code === "translation_buffer_limit"
509
+ ? 413
510
+ : typeof error.status === "number" ? error.status : 500;
428
511
  // status-absent response.failed (relaySseWithFailedTail synthetic tail) defaults
429
512
  // to 500, which is in the transient set — the mid-stream reset shape maps to
430
513
  // overloaded_error by design.
431
- fail(status, message, true);
514
+ fail(
515
+ status,
516
+ message,
517
+ true,
518
+ );
432
519
  break;
433
520
  }
434
521
  default:
@@ -442,26 +529,103 @@ export function responsesSseToAnthropicSse(
442
529
  for (;;) {
443
530
  const { done, value } = await reader.read();
444
531
  if (done) break;
445
- buffer += decoder.decode(value, { stream: true });
446
- let sep: number;
447
- while ((sep = buffer.indexOf("\n\n")) !== -1) {
448
- const rawFrame = buffer.slice(0, sep);
449
- buffer = buffer.slice(sep + 2);
450
- let eventName = "";
451
- let dataLine = "";
452
- for (const line of rawFrame.split("\n")) {
453
- if (line.startsWith("event: ")) eventName = line.slice(7).trim();
454
- else if (line.startsWith("data: ")) dataLine += line.slice(6);
532
+ const decodedReservation = translatorBudget.reserveTransient(value.byteLength, { kind: "live_transient" });
533
+ let decodedCommitted = false;
534
+ try {
535
+ const fragment = decoder.decode(value, { stream: true });
536
+ decodedReservation.commitRetained();
537
+ decodedCommitted = true;
538
+ const nextBufferBytes = appendedUtf8Bytes(buffer, bufferBytes, fragment);
539
+ const appendReservation = translatorBudget.reserveTransient(nextBufferBytes, { kind: "live_transient" });
540
+ try {
541
+ buffer += fragment;
542
+ appendReservation.commitRetained();
543
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
544
+ bufferBytes = nextBufferBytes;
545
+ } catch (error) {
546
+ appendReservation.release();
547
+ throw error;
548
+ }
549
+ let sep: number;
550
+ while ((sep = buffer.indexOf("\n\n")) !== -1) {
551
+ const rawFrameBytes = utf8SliceBytes(buffer, 0, sep);
552
+ const residualBytes = bufferBytes - rawFrameBytes - 2;
553
+ const rawReservation = translatorBudget.reserveTransient(rawFrameBytes, { kind: "live_transient" });
554
+ let residualReservation: ReturnType<TranslatorBudget["reserveTransient"]> | undefined;
555
+ try {
556
+ residualReservation = translatorBudget.reserveTransient(residualBytes, { kind: "live_transient" });
557
+ const rawFrame = buffer.slice(0, sep);
558
+ const residual = buffer.slice(sep + 2);
559
+ rawReservation.commitRetained();
560
+ residualReservation.commitRetained();
561
+ residualReservation = undefined;
562
+ buffer = residual;
563
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
564
+ bufferBytes = residualBytes;
565
+ try {
566
+ let eventName = "";
567
+ let dataLine = "";
568
+ let dataLineBytes = 0;
569
+ try {
570
+ let lineStart = 0;
571
+ while (lineStart <= rawFrame.length) {
572
+ const newline = rawFrame.indexOf("\n", lineStart);
573
+ const lineEnd = newline === -1 ? rawFrame.length : newline;
574
+ if (rawFrame.startsWith("event: ", lineStart)) {
575
+ eventName = rawFrame.slice(lineStart + 7, lineEnd).trim();
576
+ } else if (rawFrame.startsWith("data: ", lineStart)) {
577
+ const fragmentStart = lineStart + 6;
578
+ const fragmentBytes = utf8SliceBytes(rawFrame, fragmentStart, lineEnd);
579
+ const fragmentReservation = translatorBudget.reserveTransient(fragmentBytes, { kind: "live_transient" });
580
+ let fragmentCommitted = false;
581
+ try {
582
+ const fragment = rawFrame.slice(fragmentStart, lineEnd);
583
+ fragmentReservation.commitRetained();
584
+ fragmentCommitted = true;
585
+ const nextDataLineBytes = appendedUtf8Bytes(dataLine, dataLineBytes, fragment);
586
+ const dataReservation = translatorBudget.reserveTransient(nextDataLineBytes, { kind: "live_transient" });
587
+ try {
588
+ dataLine += fragment;
589
+ dataReservation.commitRetained();
590
+ translatorBudget.releaseRetained(dataLineBytes, { kind: "live_transient" });
591
+ dataLineBytes = nextDataLineBytes;
592
+ } catch (error) {
593
+ dataReservation.release();
594
+ throw error;
595
+ }
596
+ } finally {
597
+ if (fragmentCommitted) {
598
+ translatorBudget.releaseRetained(fragmentBytes, { kind: "live_transient" });
599
+ } else fragmentReservation.release();
600
+ }
601
+ }
602
+ if (newline === -1) break;
603
+ lineStart = newline + 1;
604
+ }
605
+ if (!dataLine) continue;
606
+ let data: unknown;
607
+ try { data = JSON.parse(dataLine); } catch { continue; }
608
+ if (!isRec(data)) continue;
609
+ // Responses-compatible gateways may omit the optional SSE event field
610
+ // while retaining the event name in the JSON payload's required type.
611
+ const resolvedEventName = eventName || (typeof data.type === "string" ? data.type : "");
612
+ if (!resolvedEventName || terminated) continue;
613
+ handleFrame(resolvedEventName, data);
614
+ } finally {
615
+ translatorBudget.releaseRetained(dataLineBytes, { kind: "live_transient" });
616
+ }
617
+ } finally {
618
+ translatorBudget.releaseRetained(rawFrameBytes, { kind: "live_transient" });
619
+ }
620
+ } catch (error) {
621
+ rawReservation.release();
622
+ residualReservation?.release();
623
+ throw error;
624
+ }
455
625
  }
456
- if (!dataLine) continue;
457
- let data: unknown;
458
- try { data = JSON.parse(dataLine); } catch { continue; }
459
- if (!isRec(data)) continue;
460
- // Responses-compatible gateways may omit the optional SSE event field
461
- // while retaining the event name in the JSON payload's required type.
462
- const resolvedEventName = eventName || (typeof data.type === "string" ? data.type : "");
463
- if (!resolvedEventName || terminated) continue;
464
- handleFrame(resolvedEventName, data);
626
+ } finally {
627
+ if (decodedCommitted) translatorBudget.releaseRetained(value.byteLength, { kind: "live_transient" });
628
+ else decodedReservation.release();
465
629
  }
466
630
  }
467
631
  // EOF without a terminal frame is a TRUNCATION, not success (devlog 100:
@@ -470,16 +634,25 @@ export function responsesSseToAnthropicSse(
470
634
  // with a mid-stream Anthropic error event so the client can retry.
471
635
  if (!cancelled) fail(502, "upstream stream ended before a terminal frame (truncated response)", true);
472
636
  } catch (err) {
473
- fail(500, err instanceof Error ? err.message : String(err));
637
+ if (isTranslatorBudgetExceededError(err)) {
638
+ try { await reader.cancel(err); } catch { /* already closed */ }
639
+ fail(413, "upstream translation buffer exceeded the safe limit", false, "translation_buffer_limit");
640
+ } else fail(500, err instanceof Error ? err.message : String(err));
474
641
  } finally {
642
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
475
643
  if (pingTimer !== undefined) clearInterval(pingTimer);
476
644
  reader.releaseLock();
477
645
  if (!cancelled) controller.close();
478
646
  }
479
647
  })();
480
648
  },
649
+ pull() {
650
+ releaseDeliveredFrame();
651
+ },
481
652
  cancel(reason) {
482
653
  cancelled = true;
654
+ while (queuedLiveFrameBytes.length > 0) releaseDeliveredFrame();
655
+ if (open?.callId) translatorBudget.closeCall(open.callId);
483
656
  if (pingTimer !== undefined) clearInterval(pingTimer);
484
657
  return reader?.cancel(reason);
485
658
  },
@@ -587,7 +760,11 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string): R
587
760
  * (routed adapters do not support non-stream turns), so the translated stream is
588
761
  * aggregated here instead of translating a JSON body.
589
762
  */
590
- export async function collectAnthropicMessage(stream: ReadableStream<Uint8Array>, model: string): Promise<Rec> {
763
+ export async function collectAnthropicMessage(
764
+ stream: ReadableStream<Uint8Array>,
765
+ model: string,
766
+ translatorBudget: TranslatorBudget,
767
+ ): Promise<Rec> {
591
768
  const decoder = new TextDecoder();
592
769
  const reader = stream.getReader();
593
770
  let buffer = "";
@@ -597,6 +774,12 @@ export async function collectAnthropicMessage(stream: ReadableStream<Uint8Array>
597
774
  let stopReason: string | null = "end_turn";
598
775
  let usage: Rec = anthropicUsage(undefined);
599
776
  let error: Rec | null = null;
777
+ const replaceRetained = (previous: string, next: string, kind: "live_transient" | "retained_collectors") => {
778
+ const reservation = translatorBudget.reserveTransient(Buffer.byteLength(next), { kind });
779
+ reservation.commitRetained();
780
+ translatorBudget.releaseRetained(Buffer.byteLength(previous), { kind });
781
+ return next;
782
+ };
600
783
 
601
784
  const closeBlock = () => {
602
785
  if (!openBlock) return;
@@ -604,6 +787,7 @@ export async function collectAnthropicMessage(stream: ReadableStream<Uint8Array>
604
787
  if (openBlock.type === "tool_use" || openBlock.type === "server_tool_use") {
605
788
  try { openBlock.input = toolJson.length > 0 ? JSON.parse(toolJson) : {}; } catch { openBlock.input = {}; }
606
789
  }
790
+ translatorBudget.chargeRetained(Buffer.byteLength(JSON.stringify(openBlock)), { kind: "retained_collectors" });
607
791
  content.push(openBlock);
608
792
  openBlock = null;
609
793
  toolJson = "";
@@ -619,13 +803,15 @@ export async function collectAnthropicMessage(stream: ReadableStream<Uint8Array>
619
803
  const delta = isRec(data.delta) ? data.delta : {};
620
804
  if (!openBlock) break;
621
805
  if (delta.type === "text_delta" && typeof delta.text === "string") {
622
- openBlock.text = `${openBlock.text ?? ""}${delta.text}`;
806
+ const previous = typeof openBlock.text === "string" ? openBlock.text : "";
807
+ openBlock.text = replaceRetained(previous, previous + delta.text, "retained_collectors");
623
808
  } else if (delta.type === "thinking_delta" && typeof delta.thinking === "string") {
624
- openBlock.thinking = `${openBlock.thinking ?? ""}${delta.thinking}`;
809
+ const previous = typeof openBlock.thinking === "string" ? openBlock.thinking : "";
810
+ openBlock.thinking = replaceRetained(previous, previous + delta.thinking, "retained_collectors");
625
811
  } else if (delta.type === "signature_delta" && typeof delta.signature === "string") {
626
812
  openBlock.signature = delta.signature;
627
813
  } else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
628
- toolJson += delta.partial_json;
814
+ toolJson = replaceRetained(toolJson, toolJson + delta.partial_json, "retained_collectors");
629
815
  }
630
816
  break;
631
817
  }
@@ -650,11 +836,11 @@ export async function collectAnthropicMessage(stream: ReadableStream<Uint8Array>
650
836
  for (;;) {
651
837
  const { done, value } = await reader.read();
652
838
  if (done) break;
653
- buffer += decoder.decode(value, { stream: true });
839
+ buffer = replaceRetained(buffer, buffer + decoder.decode(value, { stream: true }), "live_transient");
654
840
  let sep: number;
655
841
  while ((sep = buffer.indexOf("\n\n")) !== -1) {
656
842
  const rawFrame = buffer.slice(0, sep);
657
- buffer = buffer.slice(sep + 2);
843
+ buffer = replaceRetained(buffer, buffer.slice(sep + 2), "live_transient");
658
844
  let eventName = "";
659
845
  let dataLine = "";
660
846
  for (const line of rawFrame.split("\n")) {
@@ -13,19 +13,44 @@
13
13
  * the user.
14
14
  */
15
15
 
16
- /** Env vars set by agent harnesses and CI runners inside the shell they spawn. */
16
+ /**
17
+ * Env vars set by agent harnesses and CI runners inside the shell they spawn.
18
+ *
19
+ * The list errs wide on purpose. A harness that is missed here is a harness
20
+ * whose model answers a consent question with the user's GitHub identity, which
21
+ * is the failure this module exists to prevent; a harness matched by mistake
22
+ * only postpones a prompt to the user's next hand-typed run.
23
+ */
17
24
  const AGENT_ENV_VARS = [
25
+ // Claude Code
18
26
  "CLAUDECODE",
19
27
  "CLAUDE_CODE_ENTRYPOINT",
28
+ "CLAUDE_CODE_SSE_PORT",
29
+ // Codex CLI / App / SDK. `CODEX_HOME` is deliberately NOT here: users export it
30
+ // from their own shell profile, so matching it would suppress the prompt for a
31
+ // person who is sitting right there.
20
32
  "CODEX_THREAD_ID",
21
33
  "CODEX_SHELL",
22
34
  "CODEX_CI",
35
+ "CODEX_SANDBOX",
36
+ "CODEX_SANDBOX_NETWORK_DISABLED",
37
+ // Cursor
23
38
  "CURSOR_TRACE_ID",
24
39
  "CURSOR_SESSION_TOKEN",
40
+ "CURSOR_AGENT",
41
+ // Other coding agents
25
42
  "AIDER_CHAT",
43
+ "OPENCODE_BIN_PATH",
44
+ "GEMINI_CLI",
45
+ // Generic runners and hosted environments that are never a person at a keyboard
26
46
  "REPL_ID",
27
47
  "CI",
28
48
  "GITHUB_ACTIONS",
49
+ "GITLAB_CI",
50
+ "BUILDKITE",
51
+ "JENKINS_URL",
52
+ "TEAMCITY_VERSION",
53
+ "CODESPACES",
29
54
  ] as const;
30
55
 
31
56
  /**
@@ -35,3 +60,11 @@ const AGENT_ENV_VARS = [
35
60
  export function isAgentDriven(env: NodeJS.ProcessEnv = process.env): boolean {
36
61
  return AGENT_ENV_VARS.some(name => (env[name] ?? "").trim() !== "");
37
62
  }
63
+
64
+ /**
65
+ * The env-var names that mark this process as agent-driven. Empty for a real
66
+ * hand-typed run. Callers that refuse an action use it to name the reason.
67
+ */
68
+ export function agentDrivenMarkers(env: NodeJS.ProcessEnv = process.env): string[] {
69
+ return AGENT_ENV_VARS.filter(name => (env[name] ?? "").trim() !== "");
70
+ }
@@ -19,6 +19,9 @@ export function scheduleCatalogPrewarm(deps: CatalogPrewarmDeps = {}): void {
19
19
  const { gatherRoutedModels } = await (deps.importCatalog?.() ?? import("../codex/catalog"));
20
20
  return gatherRoutedModels(load());
21
21
  })
22
- .catch(() => {});
22
+ .catch(error => {
23
+ if (error && typeof error === "object" && (error as { code?: unknown }).code === "catalog_busy") {
24
+ console.warn("[catalog] startup discovery skipped: catalog gather is busy");
25
+ }
26
+ });
23
27
  }
24
-
@@ -10,7 +10,7 @@ import {
10
10
  type DesktopProfile,
11
11
  } from "../claude/desktop-profile";
12
12
  import { writeDesktop3pConfig, type Desktop3pConfigMode, parseDesktop3pModeArgs } from "../claude/desktop-3p";
13
- import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
13
+ import { filterCatalogVisibleModels, desktopVisibleNativeSlugs } from "../codex/catalog";
14
14
  import { buildClaudeDesktopState, fetchAllModels } from "../server/management-api";
15
15
  import { findLiveProxy } from "../server/proxy-liveness";
16
16
 
@@ -42,7 +42,7 @@ async function applyProfile(profile: DesktopProfile, mode: Desktop3pConfigMode):
42
42
  }));
43
43
  const result = writeDesktop3pConfig(
44
44
  live?.port ?? config.port ?? 10100,
45
- [...visibleNativeSlugs(config)],
45
+ [...desktopVisibleNativeSlugs(config)],
46
46
  routed,
47
47
  config.apiKeys?.[0]?.key,
48
48
  mode,
package/src/cli/doctor.ts CHANGED
@@ -149,6 +149,18 @@ export async function collectOAuthDoctorChecks(
149
149
  message:
150
150
  "Codex account health unavailable (proxy not running). Action: start the proxy and re-run `ocx doctor` to inspect live cooldown/reauth",
151
151
  });
152
+ } else if (report.codexHealthSource === "management-auth-failed") {
153
+ checks.push({
154
+ level: "WARN",
155
+ message:
156
+ "Codex account health unavailable (proxy running; management authentication failed). Action: verify the admin token configuration, restart the proxy, and re-run `ocx doctor`",
157
+ });
158
+ } else if (report.codexHealthSource === "management-api-unavailable") {
159
+ checks.push({
160
+ level: "WARN",
161
+ message:
162
+ "Codex account health unavailable (proxy running; management API response failed). Action: inspect the proxy service log, restart the proxy if needed, and re-run `ocx doctor`",
163
+ });
152
164
  }
153
165
  for (const entry of report.entries) {
154
166
  if (entry.health.status === "healthy") continue;