@bitkyc08/opencodex 2.87.0 → 2.89.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 (246) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/index-uchD-ngf.js +94 -0
  7. package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  13. package/src/adapters/anthropic-replay.ts +135 -0
  14. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  15. package/src/adapters/anthropic.ts +86 -48
  16. package/src/adapters/coding-agent/protocol.ts +6 -0
  17. package/src/adapters/google-http.ts +3 -2
  18. package/src/adapters/google-progress-history.ts +53 -0
  19. package/src/adapters/google-vertex-endpoint.ts +67 -0
  20. package/src/adapters/google.ts +52 -30
  21. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  22. package/src/adapters/openai-responses/passthrough.ts +17 -12
  23. package/src/adapters/openai-responses/reasoning.ts +68 -1
  24. package/src/adapters/registry.ts +1 -1
  25. package/src/adapters/upstream-http-error.ts +3 -2
  26. package/src/adapters/zed.ts +7 -4
  27. package/src/bridge/internal.ts +17 -3
  28. package/src/bridge/response-json.ts +4 -4
  29. package/src/bridge/sse.ts +17 -10
  30. package/src/claude/inbound-content-options.ts +66 -0
  31. package/src/claude/inbound.ts +12 -26
  32. package/src/claude/outbound.ts +18 -5
  33. package/src/cli/account-api.ts +13 -1
  34. package/src/cli/account-extended.ts +17 -5
  35. package/src/cli/account-key-quota.ts +4 -1
  36. package/src/cli/account.ts +1 -1
  37. package/src/cli/capabilities-agents-routing.ts +45 -8
  38. package/src/cli/capabilities-base.ts +5 -5
  39. package/src/cli/capabilities-provider-models.ts +5 -4
  40. package/src/cli/combo-result.ts +8 -2
  41. package/src/cli/combo.ts +17 -0
  42. package/src/cli/cross-home-owner.ts +16 -2
  43. package/src/cli/help.ts +1 -1
  44. package/src/cli/loopback-family.ts +31 -0
  45. package/src/cli/message-command.ts +4 -1
  46. package/src/cli/message-remote-args.ts +69 -0
  47. package/src/cli/message-remote-command.ts +77 -0
  48. package/src/cli/provider-runtime.ts +69 -1
  49. package/src/cli/registry.ts +3 -3
  50. package/src/codex/account-pause.ts +6 -1
  51. package/src/codex/account-runtime-state.ts +5 -0
  52. package/src/codex/account-store.ts +30 -3
  53. package/src/codex/auth-api/account-list.ts +3 -0
  54. package/src/codex/auth-api/main-account-probe.ts +3 -2
  55. package/src/codex/auth-context.ts +9 -4
  56. package/src/codex/catalog/aggregation.ts +15 -0
  57. package/src/codex/catalog/auto-review.ts +2 -1
  58. package/src/codex/catalog/build-entries.ts +17 -7
  59. package/src/codex/catalog/derive-entry.ts +3 -1
  60. package/src/codex/catalog/metadata.ts +2 -0
  61. package/src/codex/catalog/model-visibility.ts +0 -2
  62. package/src/codex/catalog/ownership.ts +71 -0
  63. package/src/codex/catalog/parsing.ts +12 -3
  64. package/src/codex/catalog/retained-sync.ts +2 -1
  65. package/src/codex/catalog/routed-gather.ts +3 -1
  66. package/src/codex/catalog/routed-removal.ts +4 -4
  67. package/src/codex/convergence.ts +2 -1
  68. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  69. package/src/codex/low-quota-protection.ts +57 -31
  70. package/src/codex/main-account-cache.ts +77 -3
  71. package/src/codex/main-account.ts +6 -2
  72. package/src/codex/native-residue.ts +2 -3
  73. package/src/codex/quota.ts +26 -2
  74. package/src/codex/websocket-activity.ts +161 -0
  75. package/src/codex/websocket-registry.ts +25 -1
  76. package/src/combos/failover.ts +2 -1
  77. package/src/combos/jev-model-backend.ts +30 -7
  78. package/src/combos/jev-model-request.ts +20 -0
  79. package/src/combos/jev-quota-activation.ts +153 -0
  80. package/src/combos/jev-quota-config.ts +62 -0
  81. package/src/combos/jev-quota-route.ts +26 -0
  82. package/src/combos/jev-quota.ts +88 -0
  83. package/src/combos/jev.ts +25 -12
  84. package/src/combos/types.ts +27 -0
  85. package/src/compatibility/index.ts +3 -0
  86. package/src/compatibility/openai-responses.ts +8 -1
  87. package/src/compatibility/opengateway-responses.ts +61 -0
  88. package/src/config/account-auto-pause-revisions.ts +136 -0
  89. package/src/config/diagnostics.ts +13 -0
  90. package/src/config/live-reconcile.ts +9 -0
  91. package/src/config/opencode-zen-upgrade.ts +9 -8
  92. package/src/config/persist-unlocked.ts +10 -1
  93. package/src/config/removed-opencode-free.ts +135 -0
  94. package/src/config/schema/account-auto-pause.ts +14 -0
  95. package/src/config/schema/config-schema.ts +15 -0
  96. package/src/config/schema/leaf-validators.ts +7 -0
  97. package/src/config.ts +10 -3
  98. package/src/generated/compatibility-version.json +411 -175
  99. package/src/images/loop.ts +101 -27
  100. package/src/lib/account-auto-pause-response.ts +16 -0
  101. package/src/lib/bun-runtime.ts +18 -1
  102. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  103. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  104. package/src/lib/decision-quota-sink.ts +76 -0
  105. package/src/lib/request-failure-model.ts +2 -0
  106. package/src/lib/state-store-registrations.ts +25 -0
  107. package/src/link/ssh-argv.ts +3 -0
  108. package/src/messaging/envelope.ts +6 -3
  109. package/src/messaging/remote-auth.ts +83 -0
  110. package/src/messaging/remote-bridge.ts +270 -0
  111. package/src/messaging/remote-contract.ts +108 -0
  112. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  113. package/src/messaging/remote-enrollment.ts +244 -0
  114. package/src/messaging/remote-envelope.ts +40 -0
  115. package/src/messaging/remote-files.ts +117 -0
  116. package/src/messaging/remote-owner.ts +169 -0
  117. package/src/messaging/remote-ports.ts +56 -0
  118. package/src/messaging/remote-process.ts +131 -0
  119. package/src/messaging/remote-replies.ts +98 -0
  120. package/src/messaging/remote-rpc-admission.ts +32 -0
  121. package/src/messaging/remote-send.ts +72 -0
  122. package/src/messaging/remote-store.ts +111 -0
  123. package/src/messaging/remote-tunnels.ts +65 -0
  124. package/src/messaging/rpc.ts +13 -1
  125. package/src/oauth/account-auto-pause-policy.ts +66 -0
  126. package/src/oauth/account-auto-pause.ts +171 -0
  127. package/src/oauth/account-quota-rank.ts +2 -1
  128. package/src/oauth/anthropic-account-refusal.ts +6 -0
  129. package/src/oauth/anthropic-model-family.ts +8 -0
  130. package/src/oauth/anthropic-model-quota.ts +2 -6
  131. package/src/oauth/api-key-pool-preserve.ts +37 -0
  132. package/src/oauth/generic-account-failover.ts +7 -4
  133. package/src/oauth/index.ts +5 -35
  134. package/src/oauth/store.ts +35 -8
  135. package/src/oauth/types.ts +2 -0
  136. package/src/plugins/upstream-hooks.ts +14 -1
  137. package/src/protocols/encoders/adapter-events.ts +5 -5
  138. package/src/protocols/encoders/messages.ts +2 -2
  139. package/src/providers/api-key-pause.ts +32 -0
  140. package/src/providers/api-key-resolve.ts +10 -4
  141. package/src/providers/api-key-selection.ts +6 -1
  142. package/src/providers/api-keys.ts +8 -4
  143. package/src/providers/claude-cli-usage.ts +5 -0
  144. package/src/providers/key-failover.ts +11 -11
  145. package/src/providers/model-rename-fields.ts +1 -0
  146. package/src/providers/opencode-zen-rate-limit.ts +12 -73
  147. package/src/providers/quota/account-cache.ts +15 -1
  148. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  149. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  150. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  151. package/src/providers/quota/antigravity.ts +39 -6
  152. package/src/providers/quota/report-cache.ts +14 -0
  153. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  154. package/src/providers/quota-decision-publication.ts +77 -0
  155. package/src/providers/quota-decision-snapshot.ts +145 -0
  156. package/src/providers/quota-routing-cache.ts +3 -0
  157. package/src/providers/quota-types.ts +6 -1
  158. package/src/providers/quota.ts +3 -2
  159. package/src/providers/registry/entries-extended.ts +8 -52
  160. package/src/providers/registry/model-seeds.ts +1 -1
  161. package/src/providers/registry.ts +1 -2
  162. package/src/providers/stale-vision-classification-migration.ts +1 -1
  163. package/src/responses/parser.ts +18 -10
  164. package/src/responses/reasoning-replay-cache.ts +50 -4
  165. package/src/responses/web-search-replay.ts +138 -0
  166. package/src/routing/analytics.ts +7 -15
  167. package/src/routing/compatibility/behavior.ts +4 -1
  168. package/src/server/auth-cors.ts +7 -0
  169. package/src/server/background-lifecycle.ts +2 -1
  170. package/src/server/chat-native.ts +28 -13
  171. package/src/server/claude-messages.ts +4 -2
  172. package/src/server/index/live-sideband.ts +3 -2
  173. package/src/server/index/optional-listeners.ts +6 -0
  174. package/src/server/index/serve-options.ts +11 -5
  175. package/src/server/index/websocket-handler.ts +18 -4
  176. package/src/server/index.ts +7 -7
  177. package/src/server/lifecycle.ts +13 -1
  178. package/src/server/management/combo-routes.ts +17 -2
  179. package/src/server/management/oauth-account-routes.ts +10 -1
  180. package/src/server/management/provider-completion-test.ts +289 -0
  181. package/src/server/management/provider-routes.ts +9 -1
  182. package/src/server/management/route-registry.ts +2 -0
  183. package/src/server/messages-native.ts +34 -7
  184. package/src/server/messages-shared.ts +4 -1
  185. package/src/server/request-log.ts +7 -2
  186. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  187. package/src/server/responses/adapter-continuation.ts +34 -18
  188. package/src/server/responses/adapter-delivery.ts +53 -6
  189. package/src/server/responses/adapter-dispatch.ts +104 -29
  190. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  191. package/src/server/responses/combo-session-recall.ts +25 -0
  192. package/src/server/responses/combo-stream-preflight.ts +6 -3
  193. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  194. package/src/server/responses/compact.ts +8 -5
  195. package/src/server/responses/compaction-recovery.ts +2 -0
  196. package/src/server/responses/context-overflow.ts +141 -2
  197. package/src/server/responses/core-codex-account.ts +9 -3
  198. package/src/server/responses/core-combo.ts +34 -7
  199. package/src/server/responses/core-lifetime.ts +3 -2
  200. package/src/server/responses/core-options.ts +2 -0
  201. package/src/server/responses/core-replay.ts +5 -1
  202. package/src/server/responses/core.ts +12 -9
  203. package/src/server/responses/empty-completion-guard.ts +2 -0
  204. package/src/server/responses/fetch-helpers.ts +35 -4
  205. package/src/server/responses/jev-model-invoke.ts +13 -14
  206. package/src/server/responses/native-steering-log.ts +2 -2
  207. package/src/server/responses/passthrough-delivery.ts +3 -2
  208. package/src/server/responses/passthrough-dispatch.ts +30 -33
  209. package/src/server/responses/passthrough-execution.ts +3 -2
  210. package/src/server/responses/passthrough-observations.ts +38 -0
  211. package/src/server/responses/request-compression.ts +98 -0
  212. package/src/server/responses/request-prepare.ts +1 -0
  213. package/src/server/responses/request-transport.ts +32 -3
  214. package/src/server/responses/response-effects.ts +1 -0
  215. package/src/server/responses/response-tool-ownership.ts +21 -0
  216. package/src/server/responses/run-turn-execution.ts +2 -0
  217. package/src/server/responses/sidecar-execution.ts +59 -10
  218. package/src/server/responses/sidecar-send-budget.ts +2 -2
  219. package/src/server/responses/terminal-guard.ts +2 -0
  220. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  221. package/src/server/startup-action-control.ts +2 -2
  222. package/src/server/startup-health-cache.ts +2 -2
  223. package/src/server/windows-tray-control.ts +2 -2
  224. package/src/server/ws-bridge.ts +2 -0
  225. package/src/service/definition.ts +9 -1
  226. package/src/types/account-auto-pause.ts +15 -0
  227. package/src/types/config.ts +23 -1
  228. package/src/types/opencode-zen.ts +9 -3
  229. package/src/types/provider.ts +9 -5
  230. package/src/types/request.ts +30 -4
  231. package/src/types/wire.ts +4 -2
  232. package/src/types.ts +2 -0
  233. package/src/usage/cost.ts +17 -1
  234. package/src/usage/expected-prices.ts +3 -1
  235. package/src/usage/jev-stats.ts +4 -0
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/summary.ts +16 -3
  238. package/src/usage/telemetry-contract.ts +1 -0
  239. package/src/web-search/anthropic-history.ts +59 -0
  240. package/src/web-search/loop.ts +212 -73
  241. package/src/web-search/progress-stream.ts +14 -1
  242. package/src/web-search/run-turn-loop.ts +9 -3
  243. package/src/web-search/sidecar-search-replay.ts +96 -0
  244. package/gui/dist/assets/App-ZfYbLHkB.js +0 -58
  245. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
  246. package/gui/dist/assets/index-CfYDMj34.js +0 -94
@@ -0,0 +1,38 @@
1
+ import { readCodexWsStage } from "./codex-ws-wire";
2
+ import type { RequestLogContext } from "../request-log";
3
+
4
+ /**
5
+ * #4191: a Codex WS exchange pins its content-free stage record on the
6
+ * Response it resolves (markCodexWsStage). Adopting the record here, at
7
+ * the single funnel every physical upstream response passes through,
8
+ * binds it to the attempt that actually served it — including the 502/504
9
+ * pre-response JSON settles that never reach the SSE relay.
10
+ */
11
+ export function adoptCodexWsStage(response: Response, logCtx: RequestLogContext): void {
12
+ const stage = readCodexWsStage(response);
13
+ if (stage && logCtx.activeAttempt) logCtx.activeAttempt.codexWsStage = stage;
14
+ }
15
+
16
+ import type { CodexAuthContext } from "../../codex/auth-context";
17
+ import type { RouteResult } from "../../router";
18
+ import { captureCodexAffinityDiagnostic } from "../../codex/affinity-debug";
19
+ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
20
+ import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
21
+ import { isFixedCodexAccount } from "./core-codex-account";
22
+
23
+ export function capturePassthroughAffinity(response: Response, inboundHeaders: Headers, outboundHeaders: Record<string, string>,
24
+ captureAuthCtx: CodexAuthContext, route: Pick<RouteResult, "provider" | "modelId" | "codexAccountMode">,
25
+ parsedModelId: string, credentialSubstituted: boolean): void {
26
+ if (!isCanonicalOpenAiForwardProvider(route.provider)) return;
27
+ captureCodexAffinityDiagnostic({
28
+ inboundHeaders: inboundHeaders,
29
+ outboundHeaders: outboundHeaders,
30
+ authKind: captureAuthCtx.kind,
31
+ accountMode: route.codexAccountMode,
32
+ fixedAccount: isFixedCodexAccount(captureAuthCtx),
33
+ credentialSubstituted,
34
+ accountGatedModel: ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId),
35
+ wireModelNormalized: parsedModelId !== route.modelId,
36
+ status: response.status,
37
+ });
38
+ }
@@ -0,0 +1,98 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { constants, gzipSync, zstdCompressSync } from "node:zlib";
3
+ import type { OcxConfig } from "../../types";
4
+
5
+ /**
6
+ * Opt-in compression of outbound native ChatGPT Responses HTTP request bodies (#7247).
7
+ *
8
+ * Codex re-sends the whole conversation on every turn. When that turn travels over HTTP the body
9
+ * reaches `chatgpt.com` identity-encoded, which on a metered egress is the dominant cost of
10
+ * running Codex through the proxy. The backend already accepts coded request bodies: Codex CLI
11
+ * itself sends `content-encoding: zstd` there (see `src/server/request-decompress.ts`).
12
+ *
13
+ * Applied only at the final HTTP send (`sendWithConnectionPolicy`), after the WebSocket decision
14
+ * and plugin rewrites, so WebSocket frames and every other destination are untouched.
15
+ *
16
+ * Compression is synchronous on purpose. An await at the physical-send boundary would open a gap
17
+ * between dispatch admission (credential selection, attempt receipts, live reserve policy) and the
18
+ * send itself. The cost is bounded instead: bodies above {@link MAX_COMPRESSIBLE_BODY_BYTES} are sent
19
+ * as they are, which keeps the event-loop block to tens of milliseconds on ordinary hardware.
20
+ */
21
+ export type UpstreamRequestCompression = "off" | "gzip" | "zstd";
22
+
23
+ /** Below this the header overhead outweighs any saving. */
24
+ export const MIN_COMPRESSIBLE_BODY_BYTES = 1024;
25
+ /** Above this the synchronous compression block is no longer negligible; send identity. */
26
+ export const MAX_COMPRESSIBLE_BODY_BYTES = 16 * 1024 * 1024;
27
+
28
+ const CANONICAL_CODEX_RESPONSES = /^https:\/\/chatgpt\.com\/backend-api\/codex\/responses(?:\/compact)?$/;
29
+
30
+ /** True for the native ChatGPT Responses and compact endpoints, the only compressed destinations. */
31
+ export function isCanonicalCodexResponsesUrl(url: string): boolean {
32
+ return CANONICAL_CODEX_RESPONSES.test(url);
33
+ }
34
+
35
+ /** Anything other than an explicit codec, including a malformed hand edit, means off. */
36
+ export function resolveUpstreamRequestCompression(value: unknown): UpstreamRequestCompression {
37
+ return value === "gzip" || value === "zstd" ? value : "off";
38
+ }
39
+
40
+ // The live server config object, installed once at startup by `setLiveStateStoreConfig`. Live
41
+ // reconciliation mutates that object in place, so the field is read per send rather than copied.
42
+ let boundConfig: Pick<OcxConfig, "upstreamRequestCompression"> | null = null;
43
+
44
+ export function bindUpstreamRequestCompressionConfig(config: Pick<OcxConfig, "upstreamRequestCompression"> | null): void {
45
+ boundConfig = config;
46
+ }
47
+
48
+ export function currentUpstreamRequestCompression(): UpstreamRequestCompression {
49
+ return resolveUpstreamRequestCompression(boundConfig?.upstreamRequestCompression);
50
+ }
51
+
52
+ // ArrayBuffer-backed, which is what node:zlib returns and what a fetch BodyInit accepts.
53
+ type CodedBytes = Buffer<ArrayBuffer>;
54
+ type Compressor = (mode: Exclude<UpstreamRequestCompression, "off">, bytes: Buffer) => CodedBytes;
55
+
56
+ const defaultCompressor: Compressor = (mode, bytes) => mode === "gzip"
57
+ ? gzipSync(bytes, { level: 1 }) as CodedBytes
58
+ : zstdCompressSync(bytes, { params: { [constants.ZSTD_c_compressionLevel]: 3 } }) as CodedBytes;
59
+
60
+ let compressor: Compressor = defaultCompressor;
61
+ let maxBodyBytes = MAX_COMPRESSIBLE_BODY_BYTES;
62
+
63
+ /** Test seam: replace the codec (failure injection) and the size ceiling; null restores defaults. */
64
+ export function setUpstreamRequestCompressionForTests(next: { compressor?: Compressor | null; maxBodyBytes?: number | null }): void {
65
+ if (next.compressor !== undefined) compressor = next.compressor ?? defaultCompressor;
66
+ if (next.maxBodyBytes !== undefined) maxBodyBytes = next.maxBodyBytes ?? MAX_COMPRESSIBLE_BODY_BYTES;
67
+ }
68
+
69
+ export interface CompressedUpstreamBody {
70
+ body: CodedBytes;
71
+ encoding: Exclude<UpstreamRequestCompression, "off">;
72
+ }
73
+
74
+ /**
75
+ * Compress one final HTTP send body when the operator opted in and the send is eligible.
76
+ *
77
+ * Returns null — send unchanged — for any other destination, a non-string body (an already coded
78
+ * buffer or a stream, which also makes nested passes idempotent), a request that already declares
79
+ * a `content-encoding`, a body outside the size window, or a codec failure. A failure never fails
80
+ * the request: identity is what the proxy sent before this option existed.
81
+ */
82
+ export function compressUpstreamRequestBody(
83
+ target: string,
84
+ body: unknown,
85
+ headers: Headers,
86
+ mode: UpstreamRequestCompression = currentUpstreamRequestCompression(),
87
+ ): CompressedUpstreamBody | null {
88
+ if (mode === "off" || typeof body !== "string" || !isCanonicalCodexResponsesUrl(target)) return null;
89
+ if (headers.has("content-encoding")) return null;
90
+ const size = Buffer.byteLength(body, "utf8");
91
+ if (size < MIN_COMPRESSIBLE_BODY_BYTES || size > maxBodyBytes) return null;
92
+ try {
93
+ return { body: compressor(mode, Buffer.from(body, "utf8")), encoding: mode };
94
+ } catch {
95
+ // Deliberately silent: the error could only describe the body, and identity is still correct.
96
+ return null;
97
+ }
98
+ }
@@ -476,6 +476,7 @@ export async function prepareResponsesRequest(
476
476
  parsed._nativeReasoningReplay = options.nativeReasoningReplay;
477
477
  parsed._nativeReasoningMint = options.nativeReasoningMint;
478
478
  parsed._anthropicServerTools = options.anthropicServerTools;
479
+ parsed._anthropicServerReplay = options.anthropicServerReplay;
479
480
  }
480
481
  parsed._promptCacheKeyIsSharedCohort = options.promptCacheKeyIsSharedCohort;
481
482
  // The body may have been rebuilt since the inbound observation (previous-response
@@ -71,7 +71,7 @@ import {
71
71
  resolveCurrentProviderApiKeyTransport,
72
72
  } from "../../providers/api-key-selection";
73
73
  import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
74
- import { providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
74
+ import { physicalSendObserverInit, providerFetch, sendWithConnectionPolicy } from "./fetch-helpers";
75
75
  import type { ProviderFetchOptions } from "./fetch-helpers";
76
76
  import { captureConfigGeneration } from "../../lib/state-store-sweeper";
77
77
  import { recordAnthropicAccountQuotaFromHeadersForInstance, hasPassiveAccountQuota } from "../../providers/quota";
@@ -80,6 +80,7 @@ import { formatErrorResponse } from "../../bridge";
80
80
  import { bindRouteReasoningReplayScope } from "./core-replay";
81
81
  import { sessionIdHeaderFromRequest, normalizeLogConversationId } from "../request-log-conversation";
82
82
  import { redactSecretString } from "../../lib/redact";
83
+ import { captureAccountAutoPauseAttempt } from "../../oauth/account-auto-pause";
83
84
  import { selectProactiveApiKeyTransport } from "../../providers/key-failover";
84
85
  import { distributedKeyPhysicalFetch } from "../../providers/api-key-response-ownership";
85
86
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
@@ -612,6 +613,10 @@ export async function prepareResponsesTransport(
612
613
  // Pacing may outlive admission. Preserve local pause/cooldown reasons even when
613
614
  // a strict route rejects the old proposal before another bearer can be resolved.
614
615
  let candidate = servingOAuthSnapshot;
616
+ if (anthropicInstance === undefined && candidate && getAccountSet(route.providerName)?.accounts.find(row => row.id === candidate!.accountId)?.paused) {
617
+ oauthSelection = captureOAuthAccountSelection(route.providerName);
618
+ candidate = oauthSelection ? await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true }) : undefined;
619
+ }
615
620
  if (anthropicInstance !== undefined) {
616
621
  oauthSelection = captureOAuthAccountSelection(route.providerName);
617
622
  const accountId = await anthropicRouting!.resolveAnthropicDispatchAccountId(config, anthropicSessionKey, anthropicRouteDecision, route.modelId);
@@ -644,6 +649,24 @@ export async function prepareResponsesTransport(
644
649
  invalidateSameTargetRequest();
645
650
  return adapter;
646
651
  };
652
+ const recoverAutoPausedSelection = async (requestParsed: OcxParsedRequest): Promise<boolean> => {
653
+ if (route.provider.authMode === "forward") return false;
654
+ const available = route.provider.authMode === "oauth"
655
+ ? getAccountSet(route.providerName)?.accounts.some(row => !row.paused && !row.needsReauth)
656
+ : !!config.providers[route.providerName]?.apiKey;
657
+ if (!available) return false;
658
+ await refreshDispatchAdapter(requestParsed);
659
+ // Bind before the recovery caller rebuilds. A terminal retry can be a clone;
660
+ // the outer request also owns continuation persistence and replay publication.
661
+ for (const replayParsed of new Set([parsed, requestParsed])) {
662
+ bindRouteReasoningReplayScope({
663
+ parsed: replayParsed, providerName: route.providerName, provider: route.provider,
664
+ adapterName: activeAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot,
665
+ forwardHeaders: requestState.selectedForwardHeaders,
666
+ });
667
+ }
668
+ return true;
669
+ };
647
670
  const refreshRunTurnAdapter = async (requestParsed: OcxParsedRequest): Promise<ProviderAdapter> => {
648
671
  requestParsed._cursorIdentityScope = undefined;
649
672
  requestParsed._cursorConversationId = undefined;
@@ -704,6 +727,7 @@ export async function prepareResponsesTransport(
704
727
  providerName: route.providerName, modelId: route.modelId, pacingSlotAcquired: true,
705
728
  pacingSlot: attemptSlot,
706
729
  turnScopedPacing: true,
730
+ accountAutoPause: (name, provider, headers) => captureAccountAutoPauseAttempt(name, provider, { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, headers),
707
731
  beforeDispatch: beforePhysicalSend,
708
732
  });
709
733
  await run(requestParsed, { ...incoming, pacingSlot: attemptSlot, providerFetch: fetch, beforePhysicalSend }, event => { if (!refused) emit(event); });
@@ -756,6 +780,8 @@ export async function prepareResponsesTransport(
756
780
  // apply a host-scoped decision to a different host.
757
781
  const releaseFamily = snapshot ? anthropicQuota!.claimAnthropicFamilyRevalidation(snapshot.accountId, route.modelId) : () => {};
758
782
  if (!releaseFamily) throw new AnthropicAccountCooldownError(1);
783
+ const observeAutoPause = captureAccountAutoPauseAttempt(route.providerName, binding?.kind === "api-key" ? binding.provider : route.provider,
784
+ { config, oauth: binding?.kind === "oauth" ? binding.snapshot : undefined }, new Headers(dispatchInit.headers));
759
785
  let response: Response;
760
786
  let physicalOwner: AnthropicPhysicalSendOwnership | null = null;
761
787
  try {
@@ -775,7 +801,8 @@ export async function prepareResponsesTransport(
775
801
  response = await sendWithConnectionPolicy(
776
802
  distributedKeyPhysicalFetch(anthropicBillingPhysicalFetch(fetchImpl, route.providerName, route.provider), route.providerName, route.provider, target),
777
803
  destination,
778
- { ...dispatchInit, redirect: "manual" },
804
+ { ...dispatchInit, redirect: "manual",
805
+ ...physicalSendObserverInit(observeAutoPause, destination instanceof Request ? destination.url : String(destination)) },
779
806
  { providerName: route.providerName, provider: route.provider },
780
807
  );
781
808
  } finally { releaseFamily(); }
@@ -790,11 +817,12 @@ export async function prepareResponsesTransport(
790
817
  && current.credential.access === snapshot.accessToken
791
818
  && credentialGeneration(current.credential) === snapshot.generation) {
792
819
  bindAnthropicRefusalCredentialForSend(response, physicalOwner);
793
- recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId);
820
+ recordAnthropicAccountQuotaFromHeadersForInstance(anthropicInstance!, snapshot.accountId, response.headers, writerGeneration, response.status, route.modelId, snapshot.generation);
794
821
  }
795
822
  } catch { /* best-effort observation cannot fail the response */ }
796
823
  }
797
824
  if (!response.ok) await recordKeyAttemptFailure(logCtx, response, dispatchInit.signal ?? options.abortSignal);
825
+ if (observeAutoPause) response = await observeAutoPause(response);
798
826
  return response;
799
827
  }
800
828
  let nextAdapter: ProviderAdapter;
@@ -1280,6 +1308,7 @@ export async function prepareResponsesTransport(
1280
1308
  oauthDispatch,
1281
1309
  noteRoutedAttemptSend,
1282
1310
  commitKeyAttemptSend,
1311
+ recoverAutoPausedSelection,
1283
1312
  bindKeyUsageFromBridge,
1284
1313
  anthropicSessionKey,
1285
1314
  isPassthrough,
@@ -90,6 +90,7 @@ export function createResponsesEffects(
90
90
  delete parsed.context.tools;
91
91
  delete parsed._webSearch;
92
92
  delete parsed._anthropicServerTools;
93
+ delete parsed._anthropicServerReplay;
93
94
  delete parsed.options.toolChoice;
94
95
  delete parsed.options.parallelToolCalls;
95
96
  // The compaction turn is a plain prose summary; a surviving structured-output format
@@ -0,0 +1,21 @@
1
+ import type { ProviderExecutedCallType } from "../responses-undeclared-tool-guard";
2
+
3
+ // Route-authorized execution ownership follows only the response selected for delivery.
4
+ // It never becomes a wire header or mutable state shared by competing route attempts.
5
+ const providerCalls = new WeakMap<Response, ReadonlySet<ProviderExecutedCallType>>();
6
+
7
+ export function attachResponseToolOwnership(response: Response, types: ReadonlySet<ProviderExecutedCallType>): Response {
8
+ if (types.size > 0) providerCalls.set(response, new Set(types));
9
+ return response;
10
+ }
11
+
12
+ export function responseToolOwnership(response: Response): ReadonlySet<ProviderExecutedCallType> | undefined {
13
+ return providerCalls.get(response);
14
+ }
15
+
16
+ /** Carry ownership only when rebuilding the same selected response body. */
17
+ export function carryResponseToolOwnership(source: Response, target: Response): Response {
18
+ const types = providerCalls.get(source);
19
+ if (types) providerCalls.set(target, types);
20
+ return target;
21
+ }
@@ -53,6 +53,7 @@ import { emptyToolCatalogRefusal, guardEmptyToolCatalogEvents, guardEmptyToolCat
53
53
  // LOCAL PATCH (runturn-websearch)
54
54
  import { planWebSearch } from "../../web-search";
55
55
  import { runTurnWebSearchInitialParsed, runTurnWebSearchLoop } from "../../web-search/run-turn-loop";
56
+ import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
56
57
  import { WEB_SEARCH_TOOL_NAME } from "../../web-search/synthetic-tool";
57
58
  import { orderDevinMessagesOutput } from "../../claude/devin-output-order";
58
59
  import { unboundPoolSpendRefusalMessage } from "../workflow-refusal";
@@ -217,6 +218,7 @@ export async function executeResponsesRunTurn(
217
218
  releaseSearchProbeLease();
218
219
  throw error;
219
220
  }
221
+ if (!wsPlan) stripReplayedSearchPairs(parsed);
220
222
 
221
223
  const runTurnAbort = new AbortController();
222
224
  const cleanupRequestAbort = linkAbortSignal(runTurnAbort, req.signal);
@@ -1,3 +1,4 @@
1
+ import { createAnthropicThinkingRecovery } from "./anthropic-thinking-recovery";
1
2
  import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
2
3
  import type { ResponsesRequestContext } from "./core-options";
3
4
  import type { PreparedResponsesRequest } from "./request-prepare";
@@ -20,7 +21,7 @@ import {
20
21
  runWithImageBridge,
21
22
  clampImageMaxRounds,
22
23
  } from "../../images";
23
- import type { ProviderAdapter } from "../../adapters/base";
24
+ import type { AdapterRequest, ProviderAdapter } from "../../adapters/base";
24
25
  import type { OcxParsedRequest, OcxUsage } from "../../types";
25
26
  import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
26
27
  import {
@@ -32,12 +33,13 @@ import {
32
33
  failoverAccountSnapshot,
33
34
  } from "../../oauth/generic-account-failover";
34
35
  import { classifyKiroRefusal } from "../../adapters/kiro-refusal";
35
- import { isNonReplayableResponse } from "../../lib/upstream-retry";
36
+ import { isNonReplayableResponse, SendBudgetExhaustedError } from "../../lib/upstream-retry";
36
37
  import { unwrapUpstreamRetryEvidenceError } from "../../codex/auth-context";
37
38
  import { AdmissionModelDeniedError, admissionModelDeniedResponse } from "../admission-model-scope";
38
39
  import { noteKiroMonthlyRefusal, noteKiroServedSuccess } from "../../providers/kiro-usage";
39
40
  import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
40
41
  import { anthropicPoolCooldownResponse, readDisplaySafeErrorText } from "./core-errors";
42
+ import { sidecarContextOverflowResponse } from "./context-overflow";
41
43
  import {
42
44
  ANTHROPIC_POOL_MAX_FAILOVERS_PER_REQUEST,
43
45
  anthropicRoutingFor,
@@ -55,6 +57,7 @@ import { rememberResponseState } from "../../responses/state";
55
57
  import { trackStreamLifetime } from "../lifecycle";
56
58
  import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
57
59
  import { createSidecarSendBudget } from "./sidecar-send-budget";
60
+ import { stripReplayedSearchPairs } from "../../web-search/sidecar-search-replay";
58
61
 
59
62
  /** One responsibility of the Responses request pipeline; state owners are explicit. */
60
63
  export async function executeResponsesSidecars(
@@ -100,7 +103,7 @@ export async function executeResponsesSidecars(
100
103
  | "notifyResponseComplete"
101
104
  | "cancelResponseCompletion"
102
105
  >,
103
- sendBudgetState: Pick<ResponsesSendBudget, "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
106
+ sendBudgetState: Pick<ResponsesSendBudget, "sendBudgetExhausted" | "reserveCredentialHop" | "refundableAdapterDispatchBudget" | "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
104
107
  ) {
105
108
  const { config, options, logCtx } = requestContext;
106
109
  const antigravityPoolActivated = requestState.route.providerName === "google-antigravity"
@@ -215,6 +218,8 @@ export async function executeResponsesSidecars(
215
218
  })
216
219
  : undefined;
217
220
  const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
221
+ // Replayed search pairs call the synthetic web_search tool only the sidecar declares (#7269).
222
+ if (!wsPlan) stripReplayedSearchPairs(parsed);
218
223
  const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
219
224
  const canRunWebSearch = !!wsPlan && !transportState.adapter.runTurn;
220
225
  let sidecarOutputStarted = false;
@@ -379,7 +384,7 @@ export async function executeResponsesSidecars(
379
384
  recoveryKind = "oauth-account-429";
380
385
  if (!options.comboInitialSend && canRunWebSearch && route.providerName === "google-antigravity")
381
386
  sendBudgetState.pendingHopPermit = hop.permit;
382
- else sidecarBudget.ownCredentialHop(hop.permit);
387
+ else sidecarBudget.ownCredentialHop(hop.permit, (canRunWebSearch || !!imgPlan || !!vidPlan) && transportState.adapter.name === "anthropic");
383
388
  } else if (
384
389
  // Anthropic's pool is excluded from generic failover, so without this arm a 429 inside a
385
390
  // web-search or image-bridge turn was terminal even with the pool fully enabled -- while
@@ -421,7 +426,7 @@ export async function executeResponsesSidecars(
421
426
  }
422
427
  recoveryKind = originalResponse?.status === 401 ? "oauth-401"
423
428
  : originalResponse?.status === 403 ? "oauth-account-403" : "anthropic-oauth-429";
424
- sidecarBudget.ownCredentialHop(hop.permit);
429
+ sidecarBudget.ownCredentialHop(hop.permit, (canRunWebSearch || !!imgPlan || !!vidPlan) && transportState.adapter.name === "anthropic");
425
430
  } else {
426
431
  // No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
427
432
  // credential. The 429 is terminal for this sidecar turn.
@@ -454,6 +459,35 @@ export async function executeResponsesSidecars(
454
459
  });
455
460
  return { adapter: rotatedAdapter, recoveryKind };
456
461
  };
462
+ // One guard follows the active sidecar across all iterations. Enforced spend has no
463
+ // authoritative receipt for earlier legacy sends, so recovery cannot claim headroom there.
464
+ const thinkingRecovery = createAnthropicThinkingRecovery(parsed,
465
+ options.abortSignal ?? new AbortController().signal, { attempted: false },
466
+ () => !sendBudgetState.sendBudgetExhausted()
467
+ && !!sendBudgetState.adapterDispatchBudget
468
+ && sendBudgetState.adapterDispatchBudget.spendEnforced !== true);
469
+ const thinkingReceipt = (request: AdapterRequest, adapter: ProviderAdapter, recovery?: AttemptRecoveryKind) =>
470
+ adapter.name !== "anthropic" ? {} : { onPhysicalDispatch: () => {
471
+ if (recovery === "opaque-blob-rejection") {
472
+ const decision = sendBudgetState.adapterDispatchBudget?.reserveDispatch({
473
+ sendClass: "repair", targetKey: `${route.providerName}|${request.url}|${route.modelId}`,
474
+ });
475
+ if (sendBudgetState.adapterDispatchBudget?.spendEnforced || !decision?.allowed) {
476
+ if (decision?.allowed) decision.permit.release();
477
+ throw new SendBudgetExhaustedError("sidecar thinking recovery");
478
+ }
479
+ try { sendBudgetState.noteInitialDispatch(decision.permit); }
480
+ finally { decision.permit.release(); }
481
+ } else {
482
+ const existing = sidecarBudget.fetchOptions(adapter).onPhysicalDispatch;
483
+ if (existing) existing();
484
+ else {
485
+ const prepaid = sendBudgetState.pendingHopPermit;
486
+ sendBudgetState.pendingHopPermit = undefined;
487
+ sendBudgetState.noteInitialDispatch(prepaid);
488
+ }
489
+ }
490
+ } };
457
491
  // A loop's hidden rounds before a rotation were spent by the account it left. Bill them to that
458
492
  // account's attempt while it is still open; the next send seals it. Declined when that send
459
493
  // keeps the account (no new attempt), so the loop's final total still carries them.
@@ -518,8 +552,9 @@ export async function executeResponsesSidecars(
518
552
  options.codexWsRuntimeIdentity,
519
553
  { providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(transportState.adapter) },
520
554
  );
521
- const imgResponse = await runWithImageBridge({
555
+ const imgLoopResponse = await runWithImageBridge({
522
556
  parsed, adapter: transportState.adapter,
557
+ ...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
523
558
  clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
524
559
  incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
525
560
  onProducerStart: sidecarBudget.takeProducerOwnership,
@@ -540,10 +575,11 @@ export async function executeResponsesSidecars(
540
575
  stallTimeoutSec: config.stallTimeoutSec,
541
576
  waitForRequestSlot: imageProviderFetch.waitForPacing,
542
577
  fetchImpl: imageProviderFetch.unpacedFetch ?? imageProviderFetch,
543
- fetchForRequest: (request, iterParsed, adapter) => {
578
+ fetchForRequest: (request, iterParsed, adapter, recovery) => {
544
579
  const fetch = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
545
580
  dispatchOverride: oauthDispatch(request, iterParsed),
546
581
  providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
582
+ ...thinkingReceipt(request, adapter, recovery),
547
583
  });
548
584
  return fetch.unpacedFetch ?? fetch;
549
585
  },
@@ -569,6 +605,7 @@ export async function executeResponsesSidecars(
569
605
  ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
570
606
  terminalRefusalAsFailed: inboundWire !== "chat" && inboundWire !== "anthropic",
571
607
  onCompletedResponse: (response, providerState) => {
608
+ if (!options.abortSignal?.aborted) thinkingRecovery.rememberSuccess(response);
572
609
  const served = transportState.replayOAuthCredentialSnapshot;
573
610
  if (route.providerName === "kiro" && response.status === "completed" && served
574
611
  && noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
@@ -586,8 +623,12 @@ export async function executeResponsesSidecars(
586
623
  notifyResponseComplete(response);
587
624
  },
588
625
  });
589
- const imgScopeRefusal = sidecarScopeRefusal(imgResponse);
626
+ const imgScopeRefusal = sidecarScopeRefusal(imgLoopResponse);
590
627
  if (imgScopeRefusal) return imgScopeRefusal;
628
+ // A provider overflow on the first iteration answers as the main path's context failure (#7316).
629
+ const imgResponse = options.comboAttempt ? imgLoopResponse : sidecarContextOverflowResponse(imgLoopResponse, {
630
+ stream: parsed.stream, modelId: parsed._responseModelId ?? parsed.modelId, translatorBudget,
631
+ });
591
632
  if (imgResponse.body) {
592
633
  const imgTurnAc = new AbortController();
593
634
  imgTurnAc.signal.addEventListener("abort", cancelResponseCompletion, { once: true });
@@ -618,11 +659,13 @@ export async function executeResponsesSidecars(
618
659
  })(input, init)) as typeof globalThis.fetch;
619
660
  const loopResponse = await runWithWebSearch({
620
661
  parsed, adapter: transportState.adapter,
662
+ ...(transportState.adapter.name === "anthropic" ? { thinkingRecovery } : {}),
621
663
  clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
622
664
  onIterationEnd: sidecarBudget.releaseUnsentHop,
623
- fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
665
+ fetchForRequest: (request, iterParsed, adapter, recovery) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
624
666
  dispatchOverride: oauthDispatch(request, iterParsed),
625
667
  providerName: route.providerName, modelId: route.modelId, ...sidecarBudget.fetchOptions(adapter),
668
+ ...thinkingReceipt(request, adapter, recovery),
626
669
  }),
627
670
  incomingMeta: {
628
671
  headers: requestState.selectedForwardHeaders,
@@ -683,6 +726,7 @@ export async function executeResponsesSidecars(
683
726
  if (route.providerName === "kiro" && response.status === "completed" && served
684
727
  && noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
685
728
  commitReasoningReplayServingRoute();
729
+ if (!options.abortSignal?.aborted) thinkingRecovery.rememberSuccess(response);
686
730
  notifyResponseComplete(response);
687
731
  },
688
732
  });
@@ -696,7 +740,12 @@ export async function executeResponsesSidecars(
696
740
  ? anthropicRoutingFor(anthropicInstance).anthropicPoolUsageLimitAfterRefusal(Date.now(), transportState.anthropicRouteDecision, route.modelId)
697
741
  : null;
698
742
  if (poolUsageLimit) void loopResponse.body?.cancel().catch(() => {});
699
- const wsResponse = poolUsageLimit ? anthropicPoolCooldownResponse(poolUsageLimit, { logCtx }) : loopResponse;
743
+ // A provider overflow on the first iteration answers as the main path's context failure (#7316).
744
+ const wsResponse = poolUsageLimit ? anthropicPoolCooldownResponse(poolUsageLimit, { logCtx })
745
+ : options.comboAttempt ? loopResponse
746
+ : sidecarContextOverflowResponse(loopResponse, {
747
+ stream: parsed.stream, modelId: parsed._responseModelId ?? parsed.modelId, translatorBudget,
748
+ });
700
749
  // Register the sidecar stream as an active turn so drainAndShutdown waits for (or aborts)
701
750
  // in-flight web-search turns instead of skipping them during graceful shutdown.
702
751
  if (wsResponse.body) {
@@ -22,8 +22,8 @@ export function createSidecarSendBudget(
22
22
  };
23
23
  return {
24
24
  /** Retain a Combo hop until dispatch; direct requests keep eager settlement. */
25
- ownCredentialHop(permit?: SingleUseDispatchPermit): void {
26
- if (!initial) { permit?.use(); return; } // Preserve direct callers' reporting contract.
25
+ ownCredentialHop(permit?: SingleUseDispatchPermit, retainUntilDispatch = false): void {
26
+ if (!initial && !retainUntilDispatch) { permit?.use(); return; } // Preserve direct callers' reporting contract.
27
27
  releaseHop();
28
28
  hopPermit = permit;
29
29
  budget.pendingHopPermit = permit;
@@ -170,6 +170,7 @@ export function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undef
170
170
  const cachedInputTokens = sumOptional("cachedInputTokens");
171
171
  const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
172
172
  const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
173
+ const cacheCreation1hInputTokens = sumOptional("cacheCreation1hInputTokens");
173
174
  const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
174
175
  const providerCredits = sumOptional("providerCredits");
175
176
  const inputTokens = first.inputTokens + second.inputTokens;
@@ -181,6 +182,7 @@ export function mergeUsage(first: OcxUsage | undefined, second: OcxUsage | undef
181
182
  ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
182
183
  ...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
183
184
  ...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
185
+ ...(cacheCreation1hInputTokens !== undefined ? { cacheCreation1hInputTokens } : {}),
184
186
  ...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
185
187
  ...(providerCredits !== undefined ? { providerCredits } : {}),
186
188
  ...(first.estimated || second.estimated ? { estimated: true } : {}),
@@ -298,7 +298,7 @@ export function collectProviderExecutedCallTypes(body: unknown): Set<ProviderExe
298
298
  return callTypes;
299
299
  }
300
300
 
301
- function isAuthorizedProviderExecutedCall(
301
+ export function isAuthorizedProviderExecutedCall(
302
302
  item: Record<string, unknown>,
303
303
  callTypes: ProviderExecutedCallTypes,
304
304
  ): boolean {
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { execFile } from "node:child_process";
3
3
  import { join } from "node:path";
4
- import { durableBunPath } from "../lib/bun-runtime";
4
+ import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
5
5
  import { selfLaunchArgv } from "../lib/self-launch-argv";
6
6
  import { isDesktopSupervised } from "../lib/system-restart-contract";
7
7
  import { withLoginShellPath } from "./login-shell-path";
@@ -196,7 +196,7 @@ async function runCliInstall(
196
196
  isStandaloneExecutable: installLaunch.isStandaloneExecutable,
197
197
  });
198
198
  const run = installLaunch.execFile ?? execFile;
199
- const env = await installChildEnv(action);
199
+ const env = durableBunChildEnv(await installChildEnv(action));
200
200
  return new Promise((resolve, reject) => {
201
201
  const timeout = process.platform === "win32" && action === "install-service"
202
202
  ? 0
@@ -4,7 +4,7 @@ import { codexAutoStartEnabled } from "../config";
4
4
  import { deriveStartupHealth, startupHealthProbeBudgetMs, type StartupHealth } from "../codex/autostart-health";
5
5
  import { getCodexRoutingKind } from "../codex/inject";
6
6
  import { diagnoseCodexShim } from "../codex/shim";
7
- import { durableBunPath } from "../lib/bun-runtime";
7
+ import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
8
8
  import { selfLaunchArgv } from "../lib/self-launch-argv";
9
9
  import { desktopStartupOwnership } from "../service/desktop-startup";
10
10
  import type { OcxConfig } from "../types";
@@ -129,7 +129,7 @@ function runProbe(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHea
129
129
  return new Promise(resolve => {
130
130
  execFile(bun, selfLaunchArgv(["__startup-health"], { sourceEntrypoint: cli }), {
131
131
  encoding: "utf8",
132
- env: process.env,
132
+ env: durableBunChildEnv(process.env),
133
133
  timeout: PROBE_TIMEOUT_MS,
134
134
  windowsHide: true,
135
135
  maxBuffer: 256 * 1024,
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { join } from "node:path";
3
- import { durableBunPath } from "../lib/bun-runtime";
3
+ import { durableBunChildEnv, durableBunPath } from "../lib/bun-runtime";
4
4
  import type { WindowsTrayStatus } from "../tray/windows";
5
5
 
6
6
  export type WindowsTrayAction = "install" | "start" | "stop" | "status" | "uninstall";
@@ -15,7 +15,7 @@ export function runWindowsTrayAction(action: WindowsTrayAction): Promise<Windows
15
15
  return new Promise((resolve, reject) => {
16
16
  execFile(bun, [cli, "tray", action, "--json"], {
17
17
  encoding: "utf8",
18
- env: process.env,
18
+ env: durableBunChildEnv(process.env),
19
19
  timeout: 30_000,
20
20
  windowsHide: true,
21
21
  maxBuffer: 256 * 1024,
@@ -11,6 +11,7 @@ import { BoundedSseFrameBuffer } from "./sse-frame-buffer";
11
11
  import { sseDataPayload as parseSseBlock } from "./sse-payload-rewrite";
12
12
  import { safeResponseHeaders } from "./safe-response-headers";
13
13
  import type { AudioSocketTarget } from "./audio-dictation";
14
+ import type { CodexWebSocketActivity } from "../codex/websocket-activity";
14
15
 
15
16
  export { safeResponseHeaders } from "./safe-response-headers";
16
17
 
@@ -19,6 +20,7 @@ type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
19
20
  type ResponsesPayloadObserver = (payload: string) => void;
20
21
 
21
22
  export interface WsData {
23
+ responsesActivity?: CodexWebSocketActivity;
22
24
  nativeControl?: NativeResponseControl;
23
25
  /** Content-free per-turn explanation; never a model capability assertion. */
24
26
  nativeSteeringUnavailable?: string;
@@ -90,8 +90,16 @@ export function parseBakedListenPort(read: () => string): number | null {
90
90
  export function resolvedProxyEnv(env: NodeJS.ProcessEnv = process.env): { name: string; value: string }[] {
91
91
  const resolved: { name: string; value: string }[] = [];
92
92
  for (const key of PROXY_ENV_KEYS) {
93
- const value = env[key]?.trim() || env[key.toLowerCase()]?.trim();
93
+ const value = [env[key]?.trim(), env[key.toLowerCase()]?.trim()].find(v => v && !OWN_CLAUDE_INTERCEPT_PROXY.test(v));
94
94
  if (value) resolved.push({ name: key, value });
95
95
  }
96
96
  return resolved;
97
97
  }
98
+
99
+ /**
100
+ * The URL Claude first-party writes into `~/.claude/settings.json` (`claudeInterceptProxyUrl`),
101
+ * which every process Claude Code starts inherits. Baked into the service, it would send the
102
+ * proxy's own outbound traffic back through its intercept listener. Only the token-bearing form
103
+ * is matched: a plain `http://127.0.0.1:<port>` is a user's local proxy client and is kept.
104
+ */
105
+ const OWN_CLAUDE_INTERCEPT_PROXY = /^http:\/\/opencodex:[^@/]+@127\.0\.0\.1:\d{1,5}\/?$/i;
@@ -0,0 +1,15 @@
1
+ export const ACCOUNT_PAUSE_REASONS = [
2
+ "oauth_organization_disabled", "account_permission_denied", "subscription_inactive",
3
+ "subscription_expired", "plan_downgraded",
4
+ ] as const;
5
+ export type AccountPauseReason = typeof ACCOUNT_PAUSE_REASONS[number];
6
+ export interface AccountPauseDetails {
7
+ reason: AccountPauseReason;
8
+ pausedAt: number;
9
+ }
10
+ export interface AccountAutoPausePolicy {
11
+ /** Explicit opt-in. Omitted and false both leave account eligibility unchanged. */
12
+ enabled?: boolean;
13
+ /** Consecutive permanent refusals; defaults to 2. */
14
+ consecutiveFailures?: number;
15
+ }