@bitkyc08/opencodex 2.56.0 → 2.58.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 (206) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
  3. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
  8. package/src/adapters/command-code.ts +12 -3
  9. package/src/adapters/cursor/cursor-errors.ts +15 -0
  10. package/src/adapters/cursor/discovery.ts +65 -1
  11. package/src/adapters/cursor/envelope-echo.ts +8 -2
  12. package/src/adapters/cursor/live-transport.ts +5 -1
  13. package/src/adapters/cursor/protobuf-events.ts +110 -11
  14. package/src/adapters/cursor/protobuf-request.ts +19 -1
  15. package/src/adapters/cursor/text-toolcall.ts +230 -0
  16. package/src/adapters/cursor/thread-continuity.ts +67 -0
  17. package/src/adapters/cursor/types.ts +5 -0
  18. package/src/adapters/cursor.ts +55 -5
  19. package/src/adapters/google-http.ts +38 -13
  20. package/src/adapters/google.ts +7 -7
  21. package/src/adapters/kiro/payload.ts +17 -3
  22. package/src/adapters/kiro/reasoning.ts +70 -7
  23. package/src/adapters/kiro/stream.ts +8 -2
  24. package/src/adapters/kiro/wire.ts +2 -1
  25. package/src/adapters/kiro-events.ts +21 -13
  26. package/src/adapters/mimo-free.ts +32 -17
  27. package/src/adapters/ollama-native.ts +42 -8
  28. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  29. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  30. package/src/adapters/openai-chat.ts +8 -8
  31. package/src/adapters/openai-responses/passthrough.ts +62 -5
  32. package/src/adapters/openai-responses/request-strips.ts +43 -0
  33. package/src/adapters/physical-send.ts +50 -0
  34. package/src/bridge/errors.ts +26 -2
  35. package/src/bridge/response-json.ts +8 -2
  36. package/src/bridge/sse.ts +20 -2
  37. package/src/claude/desktop-profile.ts +66 -9
  38. package/src/claude/outbound.ts +32 -4
  39. package/src/cli/account-main.ts +1 -1
  40. package/src/cli/capabilities.ts +2 -2
  41. package/src/cli/combo.ts +10 -1
  42. package/src/cli/config-command.ts +35 -18
  43. package/src/cli/dispatch.ts +17 -4
  44. package/src/cli/index.ts +92 -7
  45. package/src/cli/registry.ts +2 -1
  46. package/src/cli/system-command.ts +74 -5
  47. package/src/cli/uninstall-client-state.ts +12 -0
  48. package/src/clients/config-export.ts +7 -3
  49. package/src/codex/account-label.ts +14 -3
  50. package/src/codex/account-store.ts +113 -26
  51. package/src/codex/account-usability.ts +21 -0
  52. package/src/codex/auth-api/login-flow.ts +14 -2
  53. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  54. package/src/codex/auth-context.ts +199 -15
  55. package/src/codex/catalog/aggregation.ts +80 -1
  56. package/src/codex/catalog/model-visibility.ts +1 -0
  57. package/src/codex/catalog/remote.ts +30 -0
  58. package/src/codex/catalog/retained-sync.ts +9 -1
  59. package/src/codex/catalog/routed-gather.ts +38 -1
  60. package/src/codex/cli-install-provenance.ts +7 -1
  61. package/src/codex/convergence.ts +7 -2
  62. package/src/codex/desktop-app/types.ts +11 -2
  63. package/src/codex/desktop-app/windows.ts +5 -5
  64. package/src/codex/desktop-switches.ts +145 -0
  65. package/src/codex/history-job.ts +5 -1
  66. package/src/codex/history-provider.ts +33 -4
  67. package/src/codex/history-worker.ts +14 -1
  68. package/src/codex/inject/remove.ts +145 -7
  69. package/src/codex/inject/restore.ts +231 -32
  70. package/src/codex/inject.ts +12 -16
  71. package/src/codex/loopback-target.ts +9 -0
  72. package/src/codex/model-entitlements.ts +152 -15
  73. package/src/codex/native-profile-startup.ts +64 -20
  74. package/src/codex/pool-refresh-backoff.ts +12 -3
  75. package/src/codex/quota-rejection.ts +104 -15
  76. package/src/codex/routing/cache-affinity.ts +70 -0
  77. package/src/codex/routing/cooldown-math.ts +10 -0
  78. package/src/codex/routing/selection.ts +79 -2
  79. package/src/codex/routing/thread-affinity.ts +50 -2
  80. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  81. package/src/codex/routing.ts +29 -49
  82. package/src/codex/warmup.ts +1 -1
  83. package/src/combos/failover.ts +85 -0
  84. package/src/combos/request.ts +17 -10
  85. package/src/combos/types.ts +23 -2
  86. package/src/config/atomic-write.ts +83 -8
  87. package/src/config/pending-teardown.ts +31 -0
  88. package/src/config/schema/config-schema.ts +2 -0
  89. package/src/config/schema/leaf-validators.ts +1 -0
  90. package/src/generated/compatibility-version.json +272 -180
  91. package/src/images/loop.ts +1 -1
  92. package/src/lib/bounded-subprocess.ts +62 -10
  93. package/src/lib/errors.ts +17 -0
  94. package/src/lib/request-execution-budget.ts +147 -21
  95. package/src/lib/spend-reservation-ledger.ts +18 -0
  96. package/src/lib/state-store-registrations.ts +6 -2
  97. package/src/lib/test-home-guard.ts +85 -1
  98. package/src/lib/upstream-retry.ts +77 -10
  99. package/src/lib/windows-elevation.ts +76 -14
  100. package/src/lib/windows-secret-acl.ts +151 -15
  101. package/src/lib/windows-user-principal.ts +5 -1
  102. package/src/oauth/index.ts +2 -2
  103. package/src/oauth/key-providers.ts +2 -2
  104. package/src/providers/derive.ts +6 -0
  105. package/src/providers/kiro-models.ts +4 -3
  106. package/src/providers/label.ts +19 -1
  107. package/src/providers/model-discovery.ts +35 -7
  108. package/src/providers/registry/entries-core.ts +18 -0
  109. package/src/providers/registry/entries-extended.ts +59 -28
  110. package/src/providers/registry/model-seeds.ts +71 -17
  111. package/src/providers/registry/types.ts +9 -0
  112. package/src/responses/reasoning-envelope.ts +6 -3
  113. package/src/responses/spill-store.ts +17 -0
  114. package/src/responses/state/body-policy.ts +25 -0
  115. package/src/responses/state/spill-queue.ts +8 -6
  116. package/src/responses/state.ts +3 -22
  117. package/src/router.ts +4 -0
  118. package/src/routing/identity-domains.ts +21 -14
  119. package/src/routing/probe-lease.ts +103 -1
  120. package/src/server/auth-cors.ts +1 -0
  121. package/src/server/chat-completions.ts +3 -1
  122. package/src/server/chat-native.ts +37 -9
  123. package/src/server/index/live-sideband.ts +37 -1
  124. package/src/server/index/websocket-handler.ts +54 -3
  125. package/src/server/index.ts +5 -5
  126. package/src/server/inspection-tee.ts +107 -0
  127. package/src/server/live.ts +46 -1
  128. package/src/server/management/combo-routes.ts +10 -1
  129. package/src/server/management/config-routes.ts +27 -5
  130. package/src/server/models-capabilities.ts +24 -3
  131. package/src/server/relay-eager.ts +2 -0
  132. package/src/server/relay.ts +14 -19
  133. package/src/server/request-log.ts +127 -3
  134. package/src/server/response-log-body.ts +153 -0
  135. package/src/server/responses/account-change-state.ts +74 -0
  136. package/src/server/responses/adapter-continuation.ts +33 -7
  137. package/src/server/responses/adapter-delivery.ts +5 -11
  138. package/src/server/responses/adapter-dispatch.ts +84 -13
  139. package/src/server/responses/codex-ws-exchange.ts +65 -4
  140. package/src/server/responses/codex-ws-wire.ts +5 -0
  141. package/src/server/responses/collaboration.ts +74 -4
  142. package/src/server/responses/combo-session-recall.ts +68 -8
  143. package/src/server/responses/combo-stream-preflight.ts +68 -5
  144. package/src/server/responses/compact.ts +54 -13
  145. package/src/server/responses/core-auth.ts +2 -0
  146. package/src/server/responses/core-codex-account.ts +51 -3
  147. package/src/server/responses/core-combo.ts +129 -23
  148. package/src/server/responses/core-errors.ts +18 -0
  149. package/src/server/responses/core-options.ts +3 -0
  150. package/src/server/responses/core-replay.ts +105 -32
  151. package/src/server/responses/core.ts +3 -3
  152. package/src/server/responses/encrypted-payload.ts +0 -1
  153. package/src/server/responses/fetch-helpers.ts +4 -1
  154. package/src/server/responses/input-admission.ts +126 -6
  155. package/src/server/responses/native-injection-protocol.ts +42 -0
  156. package/src/server/responses/native-injection-replay.ts +105 -0
  157. package/src/server/responses/native-injection.ts +242 -0
  158. package/src/server/responses/native-response-control.ts +56 -0
  159. package/src/server/responses/native-response-json.ts +14 -0
  160. package/src/server/responses/native-response-output.ts +37 -0
  161. package/src/server/responses/native-steering-log.ts +44 -0
  162. package/src/server/responses/native-steering-policy.ts +49 -0
  163. package/src/server/responses/native-steering-replay.ts +126 -0
  164. package/src/server/responses/native-steering-settings.ts +76 -0
  165. package/src/server/responses/native-steering.ts +400 -0
  166. package/src/server/responses/native-tool-results.ts +130 -0
  167. package/src/server/responses/passthrough-delivery.ts +30 -6
  168. package/src/server/responses/passthrough-dispatch.ts +61 -11
  169. package/src/server/responses/passthrough-error.ts +38 -2
  170. package/src/server/responses/request-prepare.ts +173 -22
  171. package/src/server/responses/request-send-budget.ts +97 -2
  172. package/src/server/responses/request-spend.ts +147 -0
  173. package/src/server/responses/request-transport.ts +62 -3
  174. package/src/server/responses/run-turn-execution.ts +59 -31
  175. package/src/server/responses/sidecar-execution.ts +7 -13
  176. package/src/server/responses/terminal-guard.ts +65 -4
  177. package/src/server/responses/ws-upstream.ts +21 -1
  178. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  179. package/src/server/stop-teardown.ts +8 -1
  180. package/src/server/ws-bridge.ts +16 -1
  181. package/src/service/cli.ts +13 -1
  182. package/src/service/windows-ops.ts +210 -16
  183. package/src/service/windows-scheduler.ts +28 -21
  184. package/src/service.ts +1 -1
  185. package/src/types/config.ts +8 -1
  186. package/src/types/provider.ts +13 -0
  187. package/src/types/request.ts +8 -5
  188. package/src/types/tools.ts +24 -0
  189. package/src/types.ts +2 -0
  190. package/src/update/index.ts +10 -0
  191. package/src/update/stop-contract.d.mts +1 -0
  192. package/src/update/stop-contract.mjs +19 -0
  193. package/src/update/stop-decision.d.mts +1 -1
  194. package/src/update/stop-decision.mjs +12 -3
  195. package/src/usage/log.ts +1 -1
  196. package/src/vision/anthropic-describe.ts +1 -1
  197. package/src/vision/describe.ts +5 -5
  198. package/src/web-search/anthropic-executor.ts +1 -1
  199. package/src/web-search/exa-executor.ts +1 -1
  200. package/src/web-search/executor.ts +1 -1
  201. package/src/web-search/gemini-executor.ts +1 -1
  202. package/src/web-search/loop.ts +1 -1
  203. package/src/web-search/ollama-executor.ts +1 -1
  204. package/src/web-search/parse.ts +67 -14
  205. package/src/web-search/passthrough-bridge.ts +64 -31
  206. package/src/web-search/xai-executor.ts +1 -1
@@ -749,6 +749,10 @@ export interface OcxConfig {
749
749
  shutdownTimeoutMs?: number;
750
750
  /** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
751
751
  websockets?: boolean;
752
+ /** Experimental single-lane native OpenAI WebSocket steering; default off. */
753
+ codexNativeSteering?: boolean;
754
+ /** Experimental, default-off saved function-result injection on native multi-agent WebSockets. */
755
+ codexNativeInjection?: boolean;
752
756
  /**
753
757
  * Opt-in auto-cleanup policy for archived Codex sessions (issue #42 Phase 3).
754
758
  * Default OFF (`enabled` false / unset). Never enabled implicitly.
@@ -1011,6 +1015,7 @@ export type OcxAccountPoolQuotaWindow = "five-hour" | "weekly" | "max-utilizatio
1011
1015
 
1012
1016
  export type OcxComboStrategy = "failover" | "round-robin" | "random" | "least-used" | "reset-window";
1013
1017
  export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
1018
+ export type OcxComboDefaultEffortMode = "fallback" | "force";
1014
1019
 
1015
1020
  /**
1016
1021
  * How a combo derives the reasoning ladder it publishes to the picker.
@@ -1046,8 +1051,10 @@ export interface OcxComboConfig {
1046
1051
  cooldownMs?: number;
1047
1052
  /** Maximum wait for an eligible target cooldown to expire before failing closed. Default 0; range 0..600000, per selection attempt. */
1048
1053
  waitForCooldownMs?: number;
1049
- /** Used when the client omits reasoning.effort. null/omitted leaves the target default unchanged. */
1054
+ /** Used as a fallback when the client omits reasoning.effort, or as an override in `force` mode. null/omitted leaves the target default unchanged. */
1050
1055
  defaultEffort?: OcxComboDefaultEffort | null;
1056
+ /** `force` makes the combo default override a valid client effort. Omitted / `fallback` preserves client precedence. */
1057
+ defaultEffortMode?: OcxComboDefaultEffortMode;
1051
1058
  /**
1052
1059
  * Picker-ladder derivation policy. Omitted / `"strict"` keeps the legacy rule where an
1053
1060
  * explicitly empty target ladder suppresses the whole combo's effort control.
@@ -311,6 +311,19 @@ export interface OcxProviderConfig {
311
311
  * preserved after it, and parallel calls stay together with the reasoning turn that produced them.
312
312
  */
313
313
  requiresAdjacentResponsesToolResults?: boolean;
314
+ /**
315
+ * Responses upstream whose parser also rejects a tool call that has no matching output
316
+ * anywhere in the replayed input, not merely one whose result sits out of order. A call left
317
+ * dangling by an interrupted stream is answered with an explicit unknown-status placeholder
318
+ * so the thread can continue.
319
+ *
320
+ * Separate from `requiresAdjacentResponsesToolResults` on purpose: adjacency reorders items a
321
+ * strict parser already accepts in some order, while this synthesizes an item the client never
322
+ * sent. Kimi accepts a dangling call (#4726), so it must not inherit the synthesis.
323
+ * `statelessResponses` implies this, because an upstream that stores nothing cannot resolve
324
+ * the missing half from its own history either.
325
+ */
326
+ requiresPairedResponsesToolResults?: boolean;
314
327
  /**
315
328
  * When enabled, a tool result that is present but empty (no usable text or content
316
329
  * part) is rewritten to an explicit annotation before it reaches the upstream wire,
@@ -154,9 +154,11 @@ export interface OcxAssistantMessage {
154
154
  model?: string;
155
155
  timestamp: number;
156
156
  /**
157
- * Kiro `reasoningContent.redactedContent` for THIS assistant turn — an opaque encrypted blob
158
- * Kiro replays to preserve model reasoning across turns. Provider-specific and unrenderable, so
159
- * it rides the message rather than a content part: any other adapter simply ignores it.
157
+ * Kiro's encrypted reasoning blob for THIS assistant turn — the opaque value from the turn's
158
+ * `reasoningContentEvent` (`signature` for the GPT-5.6 family, `redactedContent` for the base64
159
+ * shape), tagged with the wire field it must be replayed on (see kiro/reasoning.ts). Kiro
160
+ * replays it to preserve model reasoning across turns. Provider-specific and unrenderable, so it
161
+ * rides the message rather than a content part: any other adapter simply ignores it.
160
162
  */
161
163
  kiroRedactedReasoning?: string;
162
164
  }
@@ -317,8 +319,9 @@ export type AdapterEvent =
317
319
  // opaque redacted_thinking blocks. Both must be replayed verbatim or tool-use turns 400.
318
320
  | { type: "thinking_signature"; signature: string }
319
321
  | { type: "redacted_thinking"; data: string }
320
- // Kiro reasoning round-trip: the encrypted `redactedContent` blob for the CURRENT assistant turn.
321
- // Never rendered — it only rides the reasoning item's envelope so the next request can replay it.
322
+ // Kiro reasoning round-trip: the encrypted reasoning blob for the CURRENT assistant turn, tagged
323
+ // with the wire field it arrived on. Never rendered — it only rides the reasoning item's envelope
324
+ // so the next request can replay it verbatim.
322
325
  | { type: "kiro_redacted_reasoning"; data: string }
323
326
  | { type: "reasoning_raw_delta"; text: string }
324
327
  | { type: "tool_call_start"; id: string; name: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata }
@@ -67,6 +67,30 @@ const CODE_MODE_HELPER_TOOL_NAMES = [
67
67
  */
68
68
  export const CODE_MODE_EXEC_TOOL_NAME = "exec";
69
69
 
70
+ /**
71
+ * Spellings that may never be MANUFACTURED as a bare alias for a namespaced tool.
72
+ *
73
+ * A bare alias is an ordinary compatibility affordance -- providers echo a namespaced tool
74
+ * without its prefix, and restoring the identity needs the bare spelling registered. For these
75
+ * six it is also an authorization decision, because a declared-name set is what
76
+ * `normalizeDeclaredToolName` and `declaresCodeModeExec` read: bare `exec` turns nested-helper
77
+ * normalization on for a catalog that never declared the shell, bare `exec_command` or
78
+ * `shell_command` turns it off for one that did, and the rest are accepted as declared calls the
79
+ * caller only ever authorized under a namespace.
80
+ *
81
+ * This is a property of the SPELLING, not of the namespace that declared it and not of the reason
82
+ * the alias was being added. It lives here, beside the names it protects, because every site that
83
+ * builds a declared-name set has to apply the same list -- the two that kept their own copies each
84
+ * drifted, once to a single namespace and once to a single name.
85
+ *
86
+ * A genuine namespace-free declaration is NOT covered: that is the caller declaring the tool, not
87
+ * a namespace being discarded to synthesize a bare name.
88
+ */
89
+ export const NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES: ReadonlySet<string> = new Set<string>([
90
+ CODE_MODE_EXEC_TOOL_NAME,
91
+ ...CODE_MODE_HELPER_TOOL_NAMES,
92
+ ]);
93
+
70
94
  /**
71
95
  * Normalizes provider-emitted tool names against declared tool catalogs.
72
96
  *
package/src/types.ts CHANGED
@@ -16,6 +16,7 @@ export {
16
16
  isAllowedToolChoice,
17
17
  toolChoiceToolPredicate,
18
18
  declaresCodeModeExec,
19
+ NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
19
20
  } from "./types/tools";
20
21
 
21
22
  export type { UpstreamHttpVersion, ReasoningSummaryDelivery, CodexAccountMode } from "./types/wire";
@@ -74,6 +75,7 @@ export type {
74
75
  OcxAccountPoolQuotaWindow,
75
76
  OcxComboStrategy,
76
77
  OcxComboDefaultEffort,
78
+ OcxComboDefaultEffortMode,
77
79
  OcxComboReasoningEffortMode,
78
80
  OcxComboTarget,
79
81
  OcxComboConfig,
@@ -481,6 +481,16 @@ export async function runUpdate(): Promise<void> {
481
481
  " After the update: close the Codex app, run 'ocx doctor', then run 'ocx stop' once to retry.",
482
482
  );
483
483
  }
484
+ if (decision.reason === "history-deferred") {
485
+ // Not the same warning: nothing was restored here. Saying "history metadata is
486
+ // incomplete" would imply config and catalog came back, and an operator who
487
+ // believed that would not know a teardown is still owed.
488
+ console.warn(
489
+ "⚠️ The shared teardown was refused by the Codex history preflight and restored nothing.\n" +
490
+ " Config, catalog, history and provenance were preserved, and the teardown receipt was kept.\n" +
491
+ " The proxy is down, so the update continues; close the Codex app and run 'ocx stop' once afterwards to finish the restore.",
492
+ );
493
+ }
484
494
  }
485
495
 
486
496
  console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
@@ -1,2 +1,3 @@
1
1
  /** Declaration for the plain-ESM stop contract shared with `bin/ocx.mjs`. */
2
2
  export declare const STOP_HISTORY_INCOMPLETE_EXIT_CODE: 79;
3
+ export declare const STOP_HISTORY_DEFERRED_EXIT_CODE: 80;
@@ -13,3 +13,22 @@
13
13
  * the child's code faithfully enough to propagate the confusion.
14
14
  */
15
15
  export const STOP_HISTORY_INCOMPLETE_EXIT_CODE = 79;
16
+
17
+ /**
18
+ * The exit code `ocx stop` uses to say "the proxy is down and the shared teardown was
19
+ * refused before it changed anything" (#4718).
20
+ *
21
+ * This is NOT 79. Seventy-nine means the teardown ran: config and catalog came back to
22
+ * their native values and only the Codex history metadata could not be finalized, so the
23
+ * receipt is discharged. Eighty means the Codex history preflight refused FIRST, so
24
+ * config, catalog, history and provenance are all untouched, the client is still routed
25
+ * at the proxy that just stopped, and the receipt stays outstanding for a later stop.
26
+ *
27
+ * Collapsing the two would be a data-loss bug in the quiet direction: a caller reading 79
28
+ * discharges an obligation that was never performed.
29
+ *
30
+ * Eighty sits in the same unoccupied window as 79 — above `sysexits.h` (64-78), below
31
+ * `128 + signal`, and outside 0, 1, 2, 4, 64 and 130, which are the codes this CLI and its
32
+ * dispatcher already emit.
33
+ */
34
+ export const STOP_HISTORY_DEFERRED_EXIT_CODE = 80;
@@ -6,5 +6,5 @@ export declare function decidePostStopUpdate(input: {
6
6
  teardownOutstanding?: boolean;
7
7
  }): {
8
8
  proceed: boolean;
9
- reason: "stop-failed" | "runtime-state" | "teardown-outstanding" | "proxy-live" | "proxy-unknown" | "history-only" | "ok";
9
+ reason: "stop-failed" | "runtime-state" | "teardown-outstanding" | "proxy-live" | "proxy-unknown" | "history-only" | "history-deferred" | "ok";
10
10
  };
@@ -1,4 +1,4 @@
1
- import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
1
+ import { STOP_HISTORY_DEFERRED_EXIT_CODE, STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
2
2
 
3
3
  /**
4
4
  * May an update replace package files after `ocx stop` returned?
@@ -22,13 +22,22 @@ import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
22
22
  * absence, and replacing files under a live server leaves it running a mix of old and
23
23
  * new modules.
24
24
  * - `ok` / `history-only` — proceed; the second also prints the manifest warning.
25
+ * - `history-deferred` — proceed; the stop is down but restored nothing, because the
26
+ * Codex history preflight refused first (#4718). The receipts it kept are the ONLY
27
+ * obligations it left, which the child proved before choosing this status, so
28
+ * `teardownOutstanding` seeing them is expected rather than disqualifying. Every other
29
+ * gate still applies: runtime records and a live or unreadable endpoint abort exactly
30
+ * as they do for a clean stop, because package replacement under a live server is the
31
+ * danger this function exists to prevent, and a history refusal says nothing about it.
25
32
  */
26
33
  export function decidePostStopUpdate({ status, hasRuntimeState, liveness, teardownOutstanding = false }) {
27
34
  const historyOnly = status === STOP_HISTORY_INCOMPLETE_EXIT_CODE;
28
- if (status !== 0 && !historyOnly) return { proceed: false, reason: "stop-failed" };
35
+ const historyDeferred = status === STOP_HISTORY_DEFERRED_EXIT_CODE;
36
+ if (status !== 0 && !historyOnly && !historyDeferred) return { proceed: false, reason: "stop-failed" };
29
37
  if (hasRuntimeState) return { proceed: false, reason: "runtime-state" };
30
- if (teardownOutstanding) return { proceed: false, reason: "teardown-outstanding" };
38
+ if (teardownOutstanding && !historyDeferred) return { proceed: false, reason: "teardown-outstanding" };
31
39
  if (liveness === "live") return { proceed: false, reason: "proxy-live" };
32
40
  if (liveness !== "dead") return { proceed: false, reason: "proxy-unknown" };
41
+ if (historyDeferred) return { proceed: true, reason: "history-deferred" };
33
42
  return { proceed: true, reason: historyOnly ? "history-only" : "ok" };
34
43
  }
package/src/usage/log.ts CHANGED
@@ -38,7 +38,7 @@ export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated
38
38
  * The old name `CodexUsageAccountLogLabel` is kept as an alias because it is exported and used
39
39
  * across modules; the two predicates below are what callers should choose between.
40
40
  */
41
- export type UsageAccountLogLabel = "main" | `p${string}` | `o${string}`;
41
+ export type UsageAccountLogLabel = "main" | `p${string}` | `o${string}` | `k${string}`;
42
42
  export type CodexUsageAccountLogLabel = UsageAccountLogLabel;
43
43
 
44
44
  /**
@@ -205,7 +205,7 @@ export async function describeImageAnthropic(
205
205
  body: JSON.stringify(body),
206
206
  signal: linkedSignal.signal,
207
207
  }, recovery)),
208
- { abortSignal: linkedSignal.signal, label: "vision-sidecar-anthropic" },
208
+ { replaySafe: true, abortSignal: linkedSignal.signal, label: "vision-sidecar-anthropic" },
209
209
  );
210
210
  if (!res.ok) {
211
211
  // The body is untrusted and only feeds one auth-failure message, so read a bounded prefix.
@@ -87,7 +87,7 @@ export async function describeImage(
87
87
  input: [{ type: "message", role: "user", content }],
88
88
  reasoning: { effort: settings.reasoning },
89
89
  // The ChatGPT (codex) backend rejects `max_output_tokens` ("Unsupported parameter"); the shared
90
- // SSE parser bounds raw response bytes before DESC_MAX_CHARS applies its display clamp.
90
+ // SSE parser bounds wire and decoded payload before DESC_MAX_CHARS applies its display clamp.
91
91
  store: false,
92
92
  stream: true,
93
93
  };
@@ -108,7 +108,7 @@ export async function describeImage(
108
108
  // `session_id`, and `x-codex-turn-metadata` to the redirect target.
109
109
  redirect: "manual",
110
110
  }, recovery)),
111
- { abortSignal: linkedSignal.signal, label: "vision-sidecar" },
111
+ { replaySafe: true, abortSignal: linkedSignal.signal, label: "vision-sidecar" },
112
112
  );
113
113
  const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
114
114
  try {
@@ -121,9 +121,9 @@ export async function describeImage(
121
121
  const parsed = await parseSidecarSSE(res);
122
122
  if (linkedSignal.signal.aborted) throw linkedSignal.signal.reason;
123
123
  recordOutcome?.(res.status);
124
- // The backend can return HTTP 200 then stream a `response.failed`/`error` event with no text;
125
- // surface that as a describe error instead of an empty (silently-blank) description.
126
- if (!parsed.text.trim() && parsed.error) return { text: "", error: parsed.error };
124
+ // Any parser error invalidates decoded text: it may be a prefix from a bounded or incomplete
125
+ // stream and must never be rendered or cached as a complete image description.
126
+ if (parsed.error) return { text: "", error: parsed.error };
127
127
  return { text: parsed.text };
128
128
  } finally {
129
129
  detachBodyGuard();
@@ -215,7 +215,7 @@ export async function runAnthropicWebSearch(
215
215
  body: JSON.stringify(body),
216
216
  signal: linkedSignal.signal,
217
217
  }, recovery)),
218
- { abortSignal: linkedSignal.signal, label: "web-search-sidecar-anthropic" },
218
+ { replaySafe: true, abortSignal: linkedSignal.signal, label: "web-search-sidecar-anthropic" },
219
219
  );
220
220
  // Guard before any branch reads the body: the failure branch's `res.text()` ran ahead of
221
221
  // the success-path guard, reopening the fetch-resolution-to-reader-attach race
@@ -46,7 +46,7 @@ export async function runExaWebSearch(
46
46
  signal: linkedSignal.signal,
47
47
  redirect: "manual",
48
48
  }, recovery)),
49
- { abortSignal: linkedSignal.signal, label: "exa-web-search-sidecar" },
49
+ { replaySafe: true, abortSignal: linkedSignal.signal, label: "exa-web-search-sidecar" },
50
50
  );
51
51
  const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
52
52
  try {
@@ -94,7 +94,7 @@ export async function runWebSearch(
94
94
  // `session_id`, and `x-codex-turn-metadata` to the redirect target.
95
95
  redirect: "manual",
96
96
  }, recovery), forwardProvider)),
97
- { abortSignal: linkedSignal.signal, label: "web-search-sidecar" },
97
+ { replaySafe: true, abortSignal: linkedSignal.signal, label: "web-search-sidecar" },
98
98
  );
99
99
  // Attach the body guard before ANY branch reads it. The success path guarded itself below,
100
100
  // but the failure branch's `res.text()` runs first, so a cancel landing between fetch
@@ -80,7 +80,7 @@ export async function runGeminiWebSearch(
80
80
  signal: linkedSignal.signal,
81
81
  redirect: "manual",
82
82
  }, recovery)),
83
- { abortSignal: linkedSignal.signal, label: "gemini-web-search-sidecar" },
83
+ { replaySafe: true, abortSignal: linkedSignal.signal, label: "gemini-web-search-sidecar" },
84
84
  );
85
85
  const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
86
86
  try {
@@ -512,7 +512,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
512
512
  signal: headerDeadline.signal,
513
513
  }, retryRecovery));
514
514
  },
515
- { abortSignal: headerDeadline.signal, label: "web-search-loop" },
515
+ { replaySafe: true, abortSignal: headerDeadline.signal, label: "web-search-loop" },
516
516
  );
517
517
  }
518
518
  } finally {
@@ -57,7 +57,7 @@ export async function runOllamaWebSearch(
57
57
  // Bun forwards custom headers across redirects, so a redirect would leak the key.
58
58
  redirect: "manual",
59
59
  }, recovery)),
60
- { abortSignal: linkedSignal.signal, label: "ollama-web-search-bridge" },
60
+ { replaySafe: true, abortSignal: linkedSignal.signal, label: "ollama-web-search-bridge" },
61
61
  );
62
62
  const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
63
63
  try {
@@ -12,7 +12,7 @@ export type WebSearchSource = SafeWebSearchSource;
12
12
  export interface WebSearchResult {
13
13
  text: string;
14
14
  sources: WebSearchSource[];
15
- /** Set only when the stream surfaced an error AND produced no usable answer text. */
15
+ /** Set when the stream failed, including when partial text was decoded before failure. */
16
16
  error?: string;
17
17
  }
18
18
 
@@ -31,9 +31,12 @@ interface OutputItem {
31
31
  content?: OutputTextBlock[];
32
32
  }
33
33
 
34
- // ChatGPT's Codex backend does not accept `max_output_tokens` on sidecar requests. Bound the raw
35
- // streamed response here, before decoded text and authoritative/delta copies can accumulate.
34
+ // Keep this compatibility export for non-Responses sidecar executors and bounded error bodies.
36
35
  export const MAX_SIDECAR_RESPONSE_BYTES = 64 * 1024;
36
+ // Responses SSE can spend far more wire bytes on JSON framing than on useful model output. Keep a
37
+ // larger finite wire ceiling while separately bounding the decoded text copies accumulated below.
38
+ export const MAX_SIDECAR_STREAM_BYTES = MAX_SIDECAR_RESPONSE_BYTES * 16;
39
+ export const MAX_SIDECAR_DECODED_CHARS = 64 * 1024;
37
40
 
38
41
  /** Push a `url_citation` annotation as a source, de-duplicated by URL. */
39
42
  function collectAnnotation(ann: AnnotationLike | undefined, sources: WebSearchSource[], seen: Set<string>): void {
@@ -208,10 +211,11 @@ export function cancelReaderWithoutWaiting(
208
211
  * `response.output_text.done` text; falls back to accumulated `response.output_text.delta`. Sources are
209
212
  * collected from EVERY shape they arrive in — `response.output_text.annotation.added` events (the
210
213
  * streaming path, which earlier testing missed → empty citations), `done`-block `annotations[]`, and
211
- * the final output[]. `response.failed`/`error` events surface as `error` when no answer text was produced.
214
+ * the final output[]. A terminal failure, a safety bound, or EOF before a terminal event surfaces as
215
+ * `error` even when partial answer text was decoded.
212
216
  */
213
217
  export async function parseSidecarSSE(response: Response): Promise<WebSearchResult> {
214
- if (!response.body) return { text: "", sources: [] };
218
+ if (!response.body) return { text: "", sources: [], error: "sidecar stream returned no response body" };
215
219
  const reader = response.body.getReader();
216
220
  const decoder = new TextDecoder();
217
221
  let buffer = "";
@@ -224,10 +228,37 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
224
228
  final: WebSearchResult | null;
225
229
  streamSources: WebSearchSource[];
226
230
  error: string | null;
227
- } = { deltaText: "", doneText: "", final: null, streamSources: [], error: null };
231
+ decodedChars: number;
232
+ terminalEvent: boolean;
233
+ limitReached: boolean;
234
+ } = {
235
+ deltaText: "",
236
+ doneText: "",
237
+ final: null,
238
+ streamSources: [],
239
+ error: null,
240
+ decodedChars: 0,
241
+ terminalEvent: false,
242
+ limitReached: false,
243
+ };
244
+
245
+ const acceptDecodedChars = (count: number): boolean => {
246
+ if (count > MAX_SIDECAR_DECODED_CHARS - acc.decodedChars) {
247
+ acc.error = "sidecar response decoded text limit reached";
248
+ acc.limitReached = true;
249
+ return false;
250
+ }
251
+ acc.decodedChars += count;
252
+ return true;
253
+ };
228
254
 
229
255
  const handle = (payload: string): void => {
230
- if (!payload || payload === "[DONE]") return;
256
+ if (!payload) return;
257
+ if (payload === "[DONE]") {
258
+ acc.terminalEvent = true;
259
+ return;
260
+ }
261
+ if (acc.limitReached) return;
231
262
  // Neither warning below copies the frame's content. An upstream SSE payload can carry model
232
263
  // output or credential material, and a malformed frame is exactly the case where the content
233
264
  // is least trustworthy. Length plus a classification separates the two failure modes in a log
@@ -247,19 +278,27 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
247
278
  const data = parsed as Record<string, unknown>;
248
279
  const type = data.type as string | undefined;
249
280
  if (type === "response.output_text.delta" && typeof data.delta === "string") {
250
- acc.deltaText += data.delta;
281
+ if (acceptDecodedChars(data.delta.length)) acc.deltaText += data.delta;
251
282
  } else if (type === "response.output_text.done" && typeof data.text === "string") {
252
283
  // The `done` event carries the full, authoritative text for one content part.
253
- acc.doneText += data.text;
284
+ if (acceptDecodedChars(data.text.length)) acc.doneText += data.text;
254
285
  } else if (type === "response.completed" || type === "response.done") {
286
+ acc.terminalEvent = true;
255
287
  const resp = data.response as { output?: OutputItem[] } | undefined;
256
- if (resp?.output) acc.final = fromOutputArray(resp.output, seen);
288
+ if (resp?.output) {
289
+ const final = fromOutputArray(resp.output, seen);
290
+ if (acceptDecodedChars(final.text.length)) acc.final = final;
291
+ }
257
292
  } else if (type === "response.failed" || type === "response.incomplete" || type === "error") {
293
+ acc.terminalEvent = true;
258
294
  const resp = data.response as { error?: { message?: string } } | undefined;
259
295
  const msg = resp?.error?.message
260
296
  ?? (data.error as { message?: string } | undefined)?.message
261
297
  ?? (typeof data.message === "string" ? data.message : undefined);
262
- if (msg) acc.error = msg;
298
+ acc.error = msg ?? `sidecar stream ended with ${type}`;
299
+ } else if (type?.includes("reasoning") && typeof data.delta === "string") {
300
+ // Reasoning is not returned, but it is still decoded payload retained transiently by JSON.parse.
301
+ acceptDecodedChars(data.delta.length);
263
302
  }
264
303
  // Citations stream as a dedicated `response.output_text.annotation.added` event (singular
265
304
  // `annotation`); capture it regardless of the exact event name so they aren't lost.
@@ -270,7 +309,7 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
270
309
  while (true) {
271
310
  const { done, value } = await reader.read();
272
311
  if (done) break;
273
- const remaining = MAX_SIDECAR_RESPONSE_BYTES - responseBytes;
312
+ const remaining = MAX_SIDECAR_STREAM_BYTES - responseBytes;
274
313
  const accepted = value.byteLength <= remaining ? value : value.subarray(0, remaining);
275
314
  responseBytes += accepted.byteLength;
276
315
  buffer += decoder.decode(accepted, { stream: true });
@@ -280,11 +319,24 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
280
319
  const data = sseFieldValue(line, "data");
281
320
  if (data !== null) handle(data.trim());
282
321
  }
283
- if (responseBytes >= MAX_SIDECAR_RESPONSE_BYTES) {
322
+ if (acc.limitReached) {
323
+ cancelReaderWithoutWaiting(reader, "sidecar response decoded text limit reached");
324
+ buffer = "";
325
+ break;
326
+ }
327
+ if (acc.terminalEvent) {
328
+ cancelReaderWithoutWaiting(reader, "sidecar terminal event received");
329
+ buffer = "";
330
+ break;
331
+ }
332
+ if (responseBytes >= MAX_SIDECAR_STREAM_BYTES) {
284
333
  // Preserve complete events accepted up to the cap, but discard any unterminated line and
285
334
  // TextDecoder carry. Do not let a rejecting/hung cancel turn bounded partial output into
286
335
  // an error or keep this parser waiting on upstream teardown.
287
336
  cancelReaderWithoutWaiting(reader, "sidecar response byte limit reached");
337
+ acc.error = "sidecar response byte limit reached before terminal event";
338
+ acc.limitReached = true;
339
+ buffer = "";
288
340
  break;
289
341
  }
290
342
  }
@@ -310,6 +362,7 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
310
362
  appendSafeWebSearchSource(sources, s);
311
363
  }
312
364
  const finalText = stripped ? body : (typeof text === "string" ? text : "");
313
- if (!finalText.trim() && acc.error) return { text: "", sources, error: acc.error };
365
+ const error = acc.error ?? (!acc.terminalEvent ? "sidecar stream ended before terminal event" : null);
366
+ if (error) return { text: finalText, sources, error };
314
367
  return { text: finalText, sources };
315
368
  }