@bitkyc08/opencodex 2.84.0 → 2.86.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 +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
  5. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  6. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/package.json +1 -2
  9. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  10. package/src/adapters/anthropic/client-identity.ts +1 -1
  11. package/src/adapters/anthropic/passthrough.ts +33 -4
  12. package/src/adapters/azure.ts +19 -1
  13. package/src/adapters/client-fingerprint.ts +24 -0
  14. package/src/adapters/coding-agent/turn.ts +22 -9
  15. package/src/adapters/cursor/arg-codec.ts +17 -5
  16. package/src/adapters/cursor/catalog.ts +8 -0
  17. package/src/adapters/cursor/effort-map.ts +3 -0
  18. package/src/adapters/cursor/freeform-args.ts +75 -0
  19. package/src/adapters/cursor/protobuf-events.ts +36 -23
  20. package/src/adapters/google-antigravity-wire.ts +22 -1
  21. package/src/adapters/google.ts +2 -2
  22. package/src/adapters/openai-chat/response-events.ts +59 -1
  23. package/src/adapters/openai-chat.ts +8 -5
  24. package/src/adapters/qoder/adapter.ts +40 -1
  25. package/src/advisor/consult.ts +220 -0
  26. package/src/advisor/context.ts +226 -0
  27. package/src/advisor/disclosure.ts +20 -0
  28. package/src/advisor/runtime.ts +305 -0
  29. package/src/advisor/settings.ts +135 -0
  30. package/src/advisor/state.ts +369 -0
  31. package/src/advisor/synthetic-tool.ts +33 -0
  32. package/src/bridge/errors.ts +5 -0
  33. package/src/chat/inbound.ts +30 -6
  34. package/src/chat/outbound.ts +5 -0
  35. package/src/claude/desktop-3p-library.ts +43 -3
  36. package/src/claude/desktop-3p.ts +22 -8
  37. package/src/claude/desktop-picker-profile.ts +11 -4
  38. package/src/claude/desktop-profile.ts +10 -1
  39. package/src/claude/desktop-remote-store-io.ts +5 -3
  40. package/src/claude/inbound.ts +121 -50
  41. package/src/claude/intercept/local-ca.ts +8 -1
  42. package/src/claude/model-default-settings.ts +154 -0
  43. package/src/claude/model-info.ts +7 -7
  44. package/src/claude/outbound.ts +13 -6
  45. package/src/cli/advisor.ts +151 -0
  46. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  47. package/src/cli/capabilities-agents-routing.ts +18 -0
  48. package/src/cli/capabilities-base.ts +1 -1
  49. package/src/cli/chatgpt-command.ts +35 -14
  50. package/src/cli/claude.ts +46 -6
  51. package/src/cli/dispatch.ts +5 -1
  52. package/src/cli/doctor.ts +9 -0
  53. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  54. package/src/cli/export-command.ts +1 -1
  55. package/src/cli/help.ts +2 -1
  56. package/src/cli/index.ts +13 -4
  57. package/src/cli/integration-plan-dto.ts +2 -0
  58. package/src/cli/registry.ts +15 -3
  59. package/src/cli/restart-scope.ts +3 -1
  60. package/src/clients/config-export/contracts.ts +1 -0
  61. package/src/clients/config-export/model-metadata.ts +3 -2
  62. package/src/clients/config-export/qoder.ts +93 -0
  63. package/src/clients/config-export.ts +17 -4
  64. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  65. package/src/codex/auth-context.ts +79 -5
  66. package/src/codex/autostart-health.ts +4 -0
  67. package/src/codex/catalog/build-entries.ts +11 -2
  68. package/src/codex/catalog/configured-tiers.ts +50 -0
  69. package/src/codex/catalog/effort.ts +4 -1
  70. package/src/codex/catalog/model-hints.ts +21 -3
  71. package/src/codex/catalog/native-ultrafast.ts +106 -0
  72. package/src/codex/catalog/parsing.ts +5 -0
  73. package/src/codex/catalog/provider-models.ts +4 -0
  74. package/src/codex/catalog/retained-sync.ts +2 -0
  75. package/src/codex/catalog/routed-gather.ts +22 -4
  76. package/src/codex/catalog/vendor-context.ts +82 -0
  77. package/src/codex/config-write-lock.ts +40 -2
  78. package/src/codex/convergence.ts +2 -0
  79. package/src/codex/desktop-app/types.ts +2 -0
  80. package/src/codex/desktop-app/windows.ts +29 -16
  81. package/src/codex/desktop-app-restart.ts +4 -2
  82. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  83. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  84. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  85. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  86. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  87. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  88. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  89. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  90. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  91. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  92. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  93. package/src/codex/desktop-compatibility/service.ts +30 -0
  94. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  95. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  96. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  97. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  98. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  99. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  100. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  101. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  102. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  103. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  104. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  105. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  106. package/src/codex/history-provider.ts +1 -1
  107. package/src/codex/inject/config-write-section.ts +4 -1
  108. package/src/codex/inject/remove.ts +18 -6
  109. package/src/codex/journal.ts +16 -1
  110. package/src/codex/main-account.ts +99 -15
  111. package/src/codex/native-main-refresh-mode.ts +217 -0
  112. package/src/codex/routing/health-store.ts +1 -1
  113. package/src/codex/routing/pool-recovery.ts +79 -0
  114. package/src/codex/shim-diagnostics.ts +6 -0
  115. package/src/codex/shim-overlay.ts +7 -2
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/live-reconcile.ts +13 -0
  118. package/src/config/load-degrade.ts +6 -1
  119. package/src/config/paths.ts +10 -3
  120. package/src/config/schema/config-schema.ts +17 -0
  121. package/src/config/schema/desktop-compatibility.ts +9 -0
  122. package/src/config/schema/leaf-validators.ts +1 -0
  123. package/src/generated/compatibility-version.json +451 -163
  124. package/src/generated/model-metadata.ts +1 -1
  125. package/src/integrations/catalog-refresh.ts +1 -1
  126. package/src/integrations/config-io.ts +20 -2
  127. package/src/integrations/journal.ts +15 -1
  128. package/src/integrations/mutation-plan.ts +1 -0
  129. package/src/integrations/registry.ts +7 -0
  130. package/src/integrations/store.ts +3 -0
  131. package/src/integrations/writer.ts +10 -1
  132. package/src/lib/advisor-activation.ts +16 -0
  133. package/src/lib/background-work.ts +65 -0
  134. package/src/lib/bounded-file-read.ts +58 -0
  135. package/src/lib/bun-path-runtime.d.mts +10 -1
  136. package/src/lib/bun-path-runtime.mjs +62 -3
  137. package/src/lib/desktop-proxy-route.ts +30 -0
  138. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  139. package/src/lib/errors.ts +7 -0
  140. package/src/lib/local-internal-call-capability.ts +55 -0
  141. package/src/lib/package-tree-integrity.ts +2 -2
  142. package/src/lib/socks5-fetch.ts +48 -71
  143. package/src/lib/socks5-handshake.ts +112 -0
  144. package/src/lib/translator-budget.ts +7 -1
  145. package/src/oauth/generic-account-failover.ts +29 -5
  146. package/src/oauth/index.ts +4 -2
  147. package/src/oauth/kiro-device-login.ts +2 -1
  148. package/src/oauth/kiro.ts +7 -1
  149. package/src/protocols/codecs/chat-ir.ts +467 -0
  150. package/src/protocols/codecs/chat.ts +10 -1
  151. package/src/protocols/codecs/messages-ir.ts +500 -0
  152. package/src/protocols/features.ts +10 -2
  153. package/src/protocols/path.ts +11 -1
  154. package/src/protocols/plan-snapshot.ts +39 -16
  155. package/src/protocols/plan.ts +9 -1
  156. package/src/protocols/settings.ts +8 -0
  157. package/src/providers/antigravity-account-roster.ts +102 -0
  158. package/src/providers/antigravity-models.ts +85 -7
  159. package/src/providers/model-rename-fields.ts +2 -0
  160. package/src/providers/model-rename-migration.ts +5 -3
  161. package/src/providers/registry/entries-core.ts +7 -6
  162. package/src/providers/registry/entries-extended.ts +19 -0
  163. package/src/responses/parser.ts +30 -19
  164. package/src/responses/state.ts +32 -29
  165. package/src/routing/history/build-file.ts +92 -0
  166. package/src/routing/history/indexer.ts +248 -216
  167. package/src/routing/history/ingest.ts +238 -0
  168. package/src/routing/history/rebuild-job.ts +111 -0
  169. package/src/routing/history/rebuild-protocol.ts +15 -0
  170. package/src/routing/history/rebuild-worker.ts +34 -0
  171. package/src/server/admission-model-scope.ts +4 -1
  172. package/src/server/antigravity-direct-eligibility.ts +208 -0
  173. package/src/server/auth-cors.ts +4 -0
  174. package/src/server/chat-completions.ts +75 -9
  175. package/src/server/claude-messages.ts +110 -20
  176. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  177. package/src/server/index/serve-options.ts +2 -2
  178. package/src/server/index/startup-warnings.ts +9 -0
  179. package/src/server/index/websocket-handler.ts +9 -0
  180. package/src/server/index.ts +13 -10
  181. package/src/server/inference/client-encoder-delivery.ts +6 -5
  182. package/src/server/login-shell-path.ts +146 -0
  183. package/src/server/management/advisor-routes.ts +191 -0
  184. package/src/server/management/agent-settings-routes.ts +22 -4
  185. package/src/server/management/companion-routes.ts +4 -0
  186. package/src/server/management/config-routes.ts +2 -2
  187. package/src/server/management/context.ts +4 -0
  188. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  189. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  190. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  191. package/src/server/management/model-routes.ts +2 -2
  192. package/src/server/management/protocol-settings-patch.ts +1 -0
  193. package/src/server/management/route-registry.ts +11 -0
  194. package/src/server/management/sibling-guard.ts +1 -0
  195. package/src/server/management-api.ts +6 -0
  196. package/src/server/messages-antigravity-direct.ts +195 -0
  197. package/src/server/messages-native.ts +5 -1
  198. package/src/server/request-log.ts +7 -0
  199. package/src/server/responses/adapter-continuation.ts +35 -18
  200. package/src/server/responses/adapter-delivery.ts +24 -2
  201. package/src/server/responses/adapter-dispatch.ts +29 -2
  202. package/src/server/responses/advisor-plan-slot.ts +27 -0
  203. package/src/server/responses/advisor-slot.ts +336 -0
  204. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  205. package/src/server/responses/core-combo-failure.ts +10 -1
  206. package/src/server/responses/core-combo.ts +21 -0
  207. package/src/server/responses/core-lifetime.ts +2 -0
  208. package/src/server/responses/core-normalize.ts +11 -0
  209. package/src/server/responses/core-options.ts +46 -0
  210. package/src/server/responses/core-replay.ts +7 -4
  211. package/src/server/responses/core.ts +1 -1
  212. package/src/server/responses/passthrough-delivery.ts +16 -10
  213. package/src/server/responses/passthrough-dispatch.ts +14 -3
  214. package/src/server/responses/request-prepare.ts +68 -5
  215. package/src/server/responses/request-transport.ts +16 -0
  216. package/src/server/responses/run-turn-execution.ts +70 -8
  217. package/src/server/responses/sidecar-execution.ts +78 -3
  218. package/src/server/responses/terminal-guard.ts +2 -1
  219. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  220. package/src/server/startup-action-control.ts +16 -2
  221. package/src/server/startup-health-cache.ts +1 -0
  222. package/src/service/desktop-startup.ts +14 -0
  223. package/src/service/health-sentinel-main.ts +23 -4
  224. package/src/service/health-sentinel-probe.ts +24 -7
  225. package/src/service/logging.ts +72 -0
  226. package/src/tray/windows-tray.ps1 +43 -24
  227. package/src/types/config.ts +63 -6
  228. package/src/types/provider.ts +7 -0
  229. package/src/types/request.ts +15 -0
  230. package/src/types/tools.ts +2 -0
  231. package/src/update/async-check.ts +1 -1
  232. package/src/update/badge.ts +2 -2
  233. package/src/update/index.ts +88 -15
  234. package/src/update/job-supervised-restart.ts +107 -0
  235. package/src/update/job.ts +32 -32
  236. package/src/update/notify.ts +1 -1
  237. package/src/update/refresh-scheduler.ts +2 -2
  238. package/src/update/supervised-recovery.d.mts +27 -0
  239. package/src/update/supervised-recovery.mjs +157 -0
  240. package/src/usage/cache-diagnostic.ts +2 -0
  241. package/src/usage/expected-prices.ts +12 -0
  242. package/src/web-search/gemini-executor.ts +2 -2
  243. package/assets/sponsors/packycode.png +0 -0
  244. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  245. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  246. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -10,7 +10,9 @@ import {
10
10
  assertChatCompletionsRoutingBody,
11
11
  ChatCompletionsRequestError,
12
12
  } from "../chat/inbound";
13
- import { chatToResponsesBody } from "../protocols/codecs/chat";
13
+ import { chatSettingsBody, chatToResponsesBody } from "../protocols/codecs/chat";
14
+ import { chatConversationToIr, chatIrRouteEvidence, withDirectChatConversation, type ChatIrConversation } from "../protocols/codecs/chat-ir";
15
+ import { antigravityDirectDeclineReason } from "./antigravity-direct-eligibility";
14
16
  import { normalizeChatImageParts } from "../chat/image-parts";
15
17
  import {
16
18
  chatCompletionsErrorResponse,
@@ -19,7 +21,8 @@ import {
19
21
  responsesJsonToChatCompletion,
20
22
  responsesSseToChatCompletionsSse,
21
23
  } from "../chat/outbound";
22
- import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
24
+ import { ADVISOR_INTERNAL_CAPABILITY_HEADER, isInternalCallCapability } from "../lib/local-internal-call-capability";
25
+ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
23
26
  import { redactSecretString } from "../lib/redact";
24
27
  import { resolveClientRetryAfter } from "../lib/retry-after";
25
28
  import {
@@ -44,6 +47,7 @@ import type { OcxConfig } from "../types";
44
47
  import { readJsonRequestBody, resolveInboundBodyLimitBytes } from "./request-decompress";
45
48
  import {
46
49
  addFinalRequestLog,
50
+ cacheDiagnosticsObserving,
47
51
  httpStatusForRequestLogTerminal,
48
52
  recordFirstOutput,
49
53
  type RequestLogContext,
@@ -272,6 +276,12 @@ async function handleChatCompletionsWithBudget(
272
276
  const protocolSettings = resolveProtocolSettings(config);
273
277
  const nativeChatCombos = protocolSettings.rollout.nativeChatCombos
274
278
  && settledRoute?.combo !== undefined && !effortRow;
279
+ // Antigravity direct lane (off by default): the conversation is decoded straight into the IR
280
+ // and the answer is encoded straight into Chat, with no internal Responses body either way.
281
+ const antigravityDecline = settledRoute && !chatNativeRoute
282
+ ? antigravityDirectDeclineReason(settledRoute, chatBody, config, { cacheDiagnostics: cacheDiagnosticsObserving() })
283
+ : undefined;
284
+ const directRoute = settledRoute && !chatNativeRoute && antigravityDecline === undefined ? settledRoute : null;
275
285
  const envelope = protocolSettings.unrepresentable === "reject" || nativeChatCombos
276
286
  ? createProtocolEnvelope({ inbound: "chat", body: chatBody, translatorBudget })
277
287
  : undefined;
@@ -281,7 +291,7 @@ async function handleChatCompletionsWithBudget(
281
291
  inbound: "chat",
282
292
  requestPath: chatNativeRoute
283
293
  ? requestPathForLane("chat", "native", "chat")
284
- : requestPathForLane("chat", "bridge", upstreamWireForAdapter(settledRoute.provider.adapter)),
294
+ : requestPathForLane("chat", directRoute ? "direct" : "bridge", upstreamWireForAdapter(settledRoute.provider.adapter)),
285
295
  features: envelope.features(),
286
296
  policy: "reject",
287
297
  });
@@ -294,8 +304,15 @@ async function handleChatCompletionsWithBudget(
294
304
  }
295
305
  markProtocolEntry(logCtx, {
296
306
  inbound: "chat",
297
- lane: chatNativeRoute ? "native" : "bridge",
298
- reasonCodes: !chatNativeRoute && nativeDecline ? [nativeDecline] : [],
307
+ lane: chatNativeRoute ? "native" : directRoute ? "direct" : "bridge",
308
+ reasonCodes: [
309
+ ...(!chatNativeRoute && !directRoute && nativeDecline ? [nativeDecline] : []),
310
+ // Only a decline that says something about this route: with the switch off or a
311
+ // non-Antigravity target the path reason already explains the bridge.
312
+ ...(antigravityDecline && antigravityDecline !== "rollout-disabled" && antigravityDecline !== "not-migrated"
313
+ ? [antigravityDecline]
314
+ : []),
315
+ ],
299
316
  features: envelope ? () => envelope.features() : () => featuresFromChatBody(chatBody),
300
317
  });
301
318
  recordProtocolShadowPlan(logCtx, config, { inbound: "chat", model: requestedModel });
@@ -314,10 +331,18 @@ async function handleChatCompletionsWithBudget(
314
331
  }
315
332
 
316
333
  let internalBody: Rec;
334
+ let directConversation: ChatIrConversation | undefined;
317
335
  try {
318
336
  // Validate the full Chat boundary after routing. Native Chat keeps `chatBody` as
319
337
  // its wire source; this Responses projection is used only by the fallback path.
320
- internalBody = chatToResponsesBody(chatBody);
338
+ // The direct lane validates the same boundary while decoding the conversation into the IR,
339
+ // and sends the core only the request settings.
340
+ if (directRoute) {
341
+ directConversation = chatConversationToIr(chatBody, chatBody.model as string);
342
+ internalBody = chatSettingsBody(chatBody);
343
+ } else {
344
+ internalBody = chatToResponsesBody(chatBody);
345
+ }
321
346
  if (effortRow) {
322
347
  internalBody.reasoning = {
323
348
  ...(isRec(internalBody.reasoning) ? internalBody.reasoning : {}),
@@ -370,6 +395,17 @@ async function handleChatCompletionsWithBudget(
370
395
  }
371
396
 
372
397
  const visionDescribeTerminal = req.headers.get("x-opencodex-vision-describe") === "1";
398
+ // Terminal advisor marker: the advisor sidecar's own loopback consultation re-enters through
399
+ // this surface. The bridge rebuilds headers from the FORWARD_HEADERS allowlist, which would
400
+ // drop the raw marker header — so the fact is detected here and carried as an option flag
401
+ // (same structure as the vision-describe fence above, depth cap 1).
402
+ //
403
+ // The header value is NOT evidence by itself: any external caller can send it, and the peer
404
+ // address proves nothing (Docker/WSL/tunnels/port-forwarding end on loopback). Internal
405
+ // authority comes from a process-owned capability minted at random per process and compared in
406
+ // constant time, so a forged header — or a token captured from an older process — is treated
407
+ // as an ordinary external request.
408
+ const advisorInternal = isInternalCallCapability(req.headers.get(ADVISOR_INTERNAL_CAPABILITY_HEADER));
373
409
  // Concrete helper targets must fail before optional stored-main credential enrichment.
374
410
  // Unresolved combos are checked after their concrete child route is selected in Responses.
375
411
  if (settledRoute && !settledRoute.combo && isCanonicalOpenAiForwardProvider(settledRoute.provider)
@@ -455,6 +491,10 @@ async function handleChatCompletionsWithBudget(
455
491
  abortSignal: req.signal,
456
492
  // Body is Responses-shaped by now, but the client spoke Chat Completions.
457
493
  inboundWire: "chat",
494
+ // A Chat caller may send an explicit empty `tools` array — a deny-all declaration the
495
+ // translation drops along with the whole catalog. Carried separately so the
496
+ // undeclared-tool guard can still enforce it; absent/partial catalogs stay deferred.
497
+ clientDeclaredEmptyToolCatalog: Array.isArray(chatBody.tools) && chatBody.tools.length === 0,
458
498
  ...((settledRoute?.combo || settledRoute?.routeKind === "policy") && droidDefaultEffort
459
499
  ? { droidDefaultEffort }
460
500
  : {}),
@@ -468,13 +508,35 @@ async function handleChatCompletionsWithBudget(
468
508
  // headers from the FORWARD_HEADERS allowlist, which would drop the raw
469
509
  // header — so the fact is detected here and carried as an option flag.
470
510
  ...(visionDescribeTerminal ? { visionDescribeTerminal: true } : {}),
511
+ ...(advisorInternal ? { advisorInternal: true } : {}),
471
512
  translatorBudget,
472
513
  ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
473
514
  onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
474
515
  onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
475
- ...(directEncodersApply(config, settledRoute)
476
- ? { clientEncoder: { protocol: "chat" as const, stream, model: requestedModel } }
477
- : {}),
516
+ // The direct lane answers through the direct encoder whatever `directEncoders` says: the
517
+ // point of the lane is that neither direction passes through the internal Responses body.
518
+ ...(directRoute && directConversation
519
+ ? {
520
+ clientEncoder: { protocol: "chat" as const, stream, model: requestedModel, requestLane: "direct" as const },
521
+ directRequest: {
522
+ protocol: "chat" as const,
523
+ providerName: directRoute.providerName,
524
+ modelId: directRoute.modelId,
525
+ evidence: chatIrRouteEvidence(directConversation),
526
+ decode: base => withDirectChatConversation(base, chatBody, directConversation!),
527
+ bridgeConversation: () => {
528
+ const bridge = chatToResponsesBody(chatBody);
529
+ return {
530
+ input: bridge.input,
531
+ ...(bridge.instructions !== undefined ? { instructions: bridge.instructions } : {}),
532
+ ...(bridge.tools !== undefined ? { tools: bridge.tools } : {}),
533
+ };
534
+ },
535
+ },
536
+ }
537
+ : directEncodersApply(config, settledRoute)
538
+ ? { clientEncoder: { protocol: "chat" as const, stream, model: requestedModel } }
539
+ : {}),
478
540
  });
479
541
  // Already in the Chat wire: no conversion. A direct-encoder body (PF-09) reports its own log
480
542
  // facts to the deferred request log. A native combo child (PF-07) carries none: its row is
@@ -543,6 +605,10 @@ async function handleChatCompletionsWithBudget(
543
605
  // Structured model_not_found must win over classifyError's generic remaps.
544
606
  classified.code = "model_not_found";
545
607
  classified.type = "invalid_request_error";
608
+ } else if (upstreamCode === UNDECLARED_TOOL_CALL_ERROR_CODE) {
609
+ // The guard's deterministic refusal keeps its wire identity — same code the
610
+ // streamed `response.failed` frame and the Anthropic surface carry.
611
+ classified.code = UNDECLARED_TOOL_CALL_ERROR_CODE;
546
612
  } else if (upstreamCode !== undefined && upstreamCode !== null && classified.code == null) {
547
613
  classified.code = upstreamCode;
548
614
  }
@@ -21,6 +21,13 @@ import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize"
21
21
  import { createToolCallIdAllocator } from "../adapters/tool-call-id";
22
22
  import { openAIChatSerializesThinking } from "../adapters/openai-chat/messages";
23
23
  import { messagesToResponsesTranslation } from "../protocols/codecs/messages";
24
+ import {
25
+ messagesDirectPlanHolds,
26
+ messagesDirectRequestOption,
27
+ messagesDirectTraceDecline,
28
+ planMessagesAntigravityDirect,
29
+ type MessagesDirectPlan,
30
+ } from "./messages-antigravity-direct";
24
31
  import { AnthropicRequestError, DesktopModelMappingUnavailableError, extractOcxEffortDirective, extractOcxRouteDirective, nativeAnthropicProjection, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
25
32
  import { nativeReasoningTag, type NativeReasoningOwner } from "../responses/reasoning-replay-cache";
26
33
  import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
@@ -40,6 +47,7 @@ import {
40
47
  REPLAY_REFUSED_STATUS,
41
48
  UPSTREAM_RESET_REPLAY_REFUSED_CODE,
42
49
  } from "../lib/upstream-retry";
50
+ import { UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
43
51
  import { resolveClientRetryAfter } from "../lib/retry-after";
44
52
  import { anthropicRateLimitHeaders } from "./anthropic-rate-limit-headers";
45
53
  import { upstreamMessagesRequestIdHeaders } from "./messages-response-headers";
@@ -850,6 +858,21 @@ export async function handleClaudeMessages(
850
858
  }
851
859
  }
852
860
 
861
+ /** The client answer for a Messages body the translation (or the direct decode) refused. */
862
+ function translationFailure(err: unknown, logCtx: RequestLogContext, logIds: { requestId: string; start: number } | undefined): Response {
863
+ const overflow = isTranslatorBudgetExceededError(err);
864
+ const unavailable = err instanceof DesktopModelMappingUnavailableError;
865
+ const status = overflow ? 413 : unavailable ? 503 : err instanceof AnthropicRequestError ? 400 : 500;
866
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
867
+ if (unavailable) return desktopMappingUnavailableResponse(err);
868
+ return anthropicErrorResponse(
869
+ status,
870
+ overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
871
+ overflow ? "request_too_large" : undefined,
872
+ overflow ? "translation_buffer_limit" : undefined,
873
+ );
874
+ }
875
+
853
876
  /**
854
877
  * Translate a Claude Messages request, route it through the Responses pipeline,
855
878
  * and translate the reply back. Runs under a translator budget owned by the
@@ -885,6 +908,11 @@ async function handleClaudeMessagesWithBudget(
885
908
  let effortRow: ParsedEffortRowId | null = null;
886
909
  let fastRow: ParsedFastRowId | null = null;
887
910
  let requestedModel = "";
911
+ // Antigravity direct lane (off by default): planned before translation, kept only if routing
912
+ // settles on the route the plan was made for.
913
+ let directPlan: MessagesDirectPlan | undefined;
914
+ /** The retained charge for the settings-only body, handed back if the plan is left. */
915
+ let directSettingsBodyBytes = 0;
888
916
  // Built only under the reject policy; the legacy default leaves this request untouched.
889
917
  let envelope: ProtocolEnvelope | undefined;
890
918
  let messagesFeatures: () => Iterable<ProtocolFeature> = () => [];
@@ -1034,9 +1062,16 @@ async function handleClaudeMessagesWithBudget(
1034
1062
  };
1035
1063
  delete anthropicBody.thinking;
1036
1064
  }
1037
- let translation;
1065
+ // On the direct lane the core receives only the request settings; the conversation is
1066
+ // already decoded into the IR. Without a plan the translation below runs as before.
1067
+ directPlan = planMessagesAntigravityDirect(anthropicBody, cc, config, translatorBudget);
1068
+ let translation = directPlan && {
1069
+ body: directPlan.settingsBody,
1070
+ cacheKeySource: directPlan.cacheKeySource,
1071
+ nativeReasoningReplay: directPlan.conversation.nativeReasoningReplay,
1072
+ };
1038
1073
  try {
1039
- translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
1074
+ translation ??= messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
1040
1075
  } catch (err) {
1041
1076
  // A native Messages route can discard an undecodable proxy envelope. Keep the
1042
1077
  // original error for any route that ultimately translates to Responses.
@@ -1052,21 +1087,13 @@ async function handleClaudeMessagesWithBudget(
1052
1087
  // fields only, so the caller intent is applied to the TRANSLATED body rather than the
1053
1088
  // inbound one.
1054
1089
  if (fastRow) internalBody.service_tier = "priority";
1055
- translatorBudget.chargeRetained(jsonUtf8Bytes(internalBody), { kind: "request_copies" });
1090
+ const internalBodyBytes = jsonUtf8Bytes(internalBody);
1091
+ translatorBudget.chargeRetained(internalBodyBytes, { kind: "request_copies" });
1092
+ if (directPlan) directSettingsBodyBytes = internalBodyBytes;
1056
1093
  cacheKeySource = translation.cacheKeySource;
1057
1094
  nativeReasoningReplay = translation.nativeReasoningReplay;
1058
1095
  } catch (err) {
1059
- const overflow = isTranslatorBudgetExceededError(err);
1060
- const unavailable = err instanceof DesktopModelMappingUnavailableError;
1061
- const status = overflow ? 413 : unavailable ? 503 : err instanceof AnthropicRequestError ? 400 : 500;
1062
- if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
1063
- if (unavailable) return desktopMappingUnavailableResponse(err);
1064
- return anthropicErrorResponse(
1065
- status,
1066
- overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
1067
- overflow ? "request_too_large" : undefined,
1068
- overflow ? "translation_buffer_limit" : undefined,
1069
- );
1096
+ return translationFailure(err, logCtx, logIds);
1070
1097
  }
1071
1098
 
1072
1099
  if (!requestedModel) requestedModel = (anthropicBody as Rec).model as string;
@@ -1119,8 +1146,30 @@ async function handleClaudeMessagesWithBudget(
1119
1146
  }
1120
1147
  return requestTokenFloor;
1121
1148
  };
1149
+ /** Leave the direct plan: the core gets the full bridge translation, as without the switch. */
1150
+ const leaveDirectPlan = (): unknown => {
1151
+ // Hand back everything the plan retained: its decode and the settings-only body it replaces.
1152
+ directPlan?.releaseDecodeCharges();
1153
+ directPlan = undefined;
1154
+ nativeReasoningReplay = undefined;
1155
+ translatorBudget.releaseRetained(directSettingsBodyBytes, { kind: "request_copies" });
1156
+ directSettingsBodyBytes = 0;
1157
+ try {
1158
+ const translation = messagesToResponsesTranslation(anthropicBody, cc, translatorBudget);
1159
+ internalBody = translation.body;
1160
+ if (fastRow) internalBody.service_tier = "priority";
1161
+ internalBody.stream = true;
1162
+ translatorBudget.chargeRetained(jsonUtf8Bytes(internalBody), { kind: "request_copies" });
1163
+ cacheKeySource = translation.cacheKeySource;
1164
+ nativeReasoningReplay = translation.nativeReasoningReplay;
1165
+ return undefined;
1166
+ } catch (err) {
1167
+ return err;
1168
+ }
1169
+ };
1170
+ let directLeaveError: unknown;
1122
1171
  try {
1123
- const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
1172
+ const route = routeModel(config, internalBody.model as string, directPlan ? directPlan.evidence : evidenceFromBody(internalBody));
1124
1173
  // Settle the wire once so the sampling decision below reads the effective
1125
1174
  // adapter rather than the provider-wide default (#404).
1126
1175
  route.staticPolicy = captureRouteStaticPolicy(
@@ -1138,6 +1187,8 @@ async function handleClaudeMessagesWithBudget(
1138
1187
  route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
1139
1188
  logCtx.routeDecision = route.routeDecision;
1140
1189
  settledRoute = route;
1190
+ // Before any route-specific shaping, so a request that leaves the lane is shaped as the bridge's.
1191
+ if (directPlan && !messagesDirectPlanHolds(directPlan, route, anthropicBody as Rec, config)) directLeaveError = leaveDirectPlan();
1141
1192
  if (directEncodersApply(config, route)) {
1142
1193
  clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor() };
1143
1194
  }
@@ -1183,6 +1234,14 @@ async function handleClaudeMessagesWithBudget(
1183
1234
  }
1184
1235
  /* unknown model: let handleResponses shape the 404 */
1185
1236
  }
1237
+ if (directPlan && !settledRoute) directLeaveError ??= leaveDirectPlan();
1238
+ if (directLeaveError !== undefined) return translationFailure(directLeaveError, logCtx, logIds);
1239
+ const directRoute = directPlan && settledRoute ? settledRoute : undefined;
1240
+ // The direct lane answers through the direct encoder whatever `directEncoders` says: neither
1241
+ // direction passes through the internal Responses body.
1242
+ if (directRoute) {
1243
+ clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor(), requestLane: "direct" };
1244
+ }
1186
1245
 
1187
1246
  // PF-08: a managed-key Anthropic route sends its Messages body natively. The caller-forward
1188
1247
  // passthrough above was decided on the caller's own credential and never reaches this point.
@@ -1206,11 +1265,20 @@ async function handleClaudeMessagesWithBudget(
1206
1265
  return anthropicErrorResponse(400, nativeProjectionOnlyError.message, "invalid_request_error");
1207
1266
  }
1208
1267
  // With the switch on, a declined route says why on its bridge mark, as native Chat does.
1209
- if (nativeDecline && nativeDecline !== "rollout-disabled") {
1268
+ const antigravityDecline = settledRoute && !nativeMessagesRoute && !directRoute
1269
+ ? messagesDirectTraceDecline(settledRoute, anthropicBody, String(internalBody.model ?? ""), config)
1270
+ : undefined;
1271
+ if (directRoute) {
1272
+ markProtocolEntry(logCtx, { inbound: "messages", lane: "direct", features: messagesFeatures });
1273
+ } else if ((nativeDecline && nativeDecline !== "rollout-disabled") || antigravityDecline) {
1210
1274
  markProtocolEntry(logCtx, {
1211
1275
  inbound: "messages",
1212
1276
  lane: "bridge",
1213
- reasonCodes: [...(effortRow ? ["effort-row" as const] : fastRow ? ["fast-row" as const] : []), nativeDecline],
1277
+ reasonCodes: [
1278
+ ...(effortRow ? ["effort-row" as const] : fastRow ? ["fast-row" as const] : []),
1279
+ ...(nativeDecline && nativeDecline !== "rollout-disabled" ? [nativeDecline] : []),
1280
+ ...(antigravityDecline ? [antigravityDecline] : []),
1281
+ ],
1214
1282
  features: messagesFeatures,
1215
1283
  });
1216
1284
  }
@@ -1218,7 +1286,11 @@ async function handleClaudeMessagesWithBudget(
1218
1286
  if (envelope && settledRoute && !settledRoute.combo && settledRoute.routeKind !== "policy") {
1219
1287
  const verdict = checkRepresentable({
1220
1288
  inbound: "messages",
1221
- requestPath: requestPathForLane("messages", nativeMessagesRoute ? "native" : "bridge", upstreamWireForAdapter(settledRoute.provider.adapter)),
1289
+ requestPath: requestPathForLane(
1290
+ "messages",
1291
+ nativeMessagesRoute ? "native" : directRoute ? "direct" : "bridge",
1292
+ upstreamWireForAdapter(settledRoute.provider.adapter),
1293
+ ),
1222
1294
  features: envelope.features(),
1223
1295
  policy: "reject",
1224
1296
  });
@@ -1348,6 +1420,12 @@ async function handleClaudeMessagesWithBudget(
1348
1420
  // Without this the replay would look native and a Responses-scoped wire default
1349
1421
  // would fire, disagreeing with the pre-flight decision above.
1350
1422
  inboundWire: "anthropic",
1423
+ // An explicit empty `tools` array is a deny-all declaration the translation drops with
1424
+ // the rest of the catalog; carried separately so the undeclared-tool guard can still
1425
+ // enforce it while partial and absent catalogs stay deferred.
1426
+ clientDeclaredEmptyToolCatalog: isRec(anthropicBody)
1427
+ && Array.isArray(anthropicBody.tools)
1428
+ && anthropicBody.tools.length === 0,
1351
1429
  nativeReasoningReplay,
1352
1430
  nativeReasoningMint,
1353
1431
  claudeGoAffinity: { sessionLane: claudeGoSessionLane },
@@ -1362,6 +1440,9 @@ async function handleClaudeMessagesWithBudget(
1362
1440
  onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
1363
1441
  onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }),
1364
1442
  ...(clientEncoder ? { clientEncoder } : {}),
1443
+ ...(directRoute && directPlan
1444
+ ? { directRequest: messagesDirectRequestOption(directPlan, anthropicBody, cc, translatorBudget) }
1445
+ : {}),
1365
1446
  });
1366
1447
  const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;
1367
1448
  // Already in the Messages wire (direct encoder): no conversion.
@@ -1374,11 +1455,13 @@ async function handleClaudeMessagesWithBudget(
1374
1455
  // Re-shape the OpenAI-style error envelope into the Anthropic one, preserving status.
1375
1456
  let message = `upstream error (${response.status})`;
1376
1457
  let contextError = false;
1458
+ let catalogRefusal = false;
1377
1459
  try {
1378
1460
  const text = await response.text();
1379
1461
  try {
1380
1462
  const parsed = JSON.parse(text) as { error?: { message?: string; type?: string; code?: unknown } | string; message?: string };
1381
1463
  contextError = typeof parsed?.error === "object" && parsed.error?.code === "context_length_exceeded";
1464
+ catalogRefusal = typeof parsed?.error === "object" && parsed.error?.code === UNDECLARED_TOOL_CALL_ERROR_CODE;
1382
1465
  const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error.message : undefined;
1383
1466
  const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
1384
1467
  message = nested || flat || (text ? `upstream error (${response.status}): ${text.slice(0, 400)}` : message);
@@ -1407,7 +1490,10 @@ async function handleClaudeMessagesWithBudget(
1407
1490
  const nativeMainFence = response.status === 503
1408
1491
  && upstreamRetryAfter?.trim() === "1"
1409
1492
  && message === CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE;
1410
- const transient = !replayRefusal && !nativeMainFence && !contextError && isTransientUpstreamStatus(response.status);
1493
+ // The undeclared-tool guard's refusal is deterministic: the same request produces the
1494
+ // same verdict, so it must not arrive as a retryable overload like a transient 5xx.
1495
+ const transient = !replayRefusal && !nativeMainFence && !contextError && !catalogRefusal
1496
+ && isTransientUpstreamStatus(response.status);
1411
1497
  const outStatus = replayRefusal
1412
1498
  ? REPLAY_REFUSED_STATUS
1413
1499
  : nativeMainFence ? 503 : contextError ? 400 : transient ? 529 : response.status;
@@ -1419,7 +1505,11 @@ async function handleClaudeMessagesWithBudget(
1419
1505
  outStatus,
1420
1506
  message,
1421
1507
  undefined,
1422
- replayRefusal ? UPSTREAM_RESET_REPLAY_REFUSED_CODE : contextError ? "context_length_exceeded" : undefined,
1508
+ replayRefusal
1509
+ ? UPSTREAM_RESET_REPLAY_REFUSED_CODE
1510
+ : contextError
1511
+ ? "context_length_exceeded"
1512
+ : catalogRefusal ? UNDECLARED_TOOL_CALL_ERROR_CODE : undefined,
1423
1513
  )), {
1424
1514
  status: outStatus,
1425
1515
  headers: outHeaders,
@@ -0,0 +1,56 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { siblingOfLivePort } from "../../codex/sibling-start";
3
+ import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
4
+ import type { DesktopCompatibilityRuntime } from "../../codex/desktop-compatibility/runtime";
5
+ import { bindNativeCompatibilityOwner } from "../../codex/desktop-compatibility/routing-binding";
6
+ import type { ReadinessGate } from "../readiness";
7
+
8
+ type RuntimeModule = { getDesktopCompatibilityRuntime(): DesktopCompatibilityRuntime; shutdownDesktopCompatibility(): Promise<void> };
9
+ interface StartupIo {
10
+ platform?: string;
11
+ testGuard?: boolean;
12
+ sibling?: boolean;
13
+ load?: () => Promise<RuntimeModule>;
14
+ warn?: (message: string) => void;
15
+ boundPort?: number;
16
+ boundHostname?: string;
17
+ loopbackPort?: number;
18
+ readiness?: Pick<ReadinessGate, "getStatus">;
19
+ readinessTimeoutMs?: number;
20
+ }
21
+ /** Core-safe gate: off installs do not load the optional runtime, read credentials or start timers. */
22
+ export function scheduleDesktopCompatibilityStartup(config: OcxConfig, io: StartupIo = {}): { shutdown(): Promise<void> } {
23
+ let stopped = false, module: RuntimeModule | undefined;
24
+ let cancelReadiness: (() => void) | undefined;
25
+ const waitForReadiness = () => !io.readiness ? Promise.resolve(true) : new Promise<boolean>(resolve => {
26
+ const deadline = Date.now() + (io.readinessTimeoutMs ?? 120000);
27
+ let timer: ReturnType<typeof setTimeout> | undefined;
28
+ const finish = (ready: boolean) => { if (timer) clearTimeout(timer); cancelReadiness = undefined; resolve(ready); };
29
+ cancelReadiness = () => finish(false);
30
+ const check = () => {
31
+ const status = io.readiness!.getStatus();
32
+ if (stopped || status !== "pending" || Date.now() >= deadline) { finish(!stopped && status === "ready"); return; }
33
+ timer = setTimeout(check, 100); timer.unref();
34
+ };
35
+ check();
36
+ });
37
+ const unbind = io.boundPort === undefined || io.boundHostname === undefined ? () => {} : bindNativeCompatibilityOwner({ config, hostname: io.boundHostname, port: io.boundPort, loopbackPort: io.loopbackPort });
38
+ const enabled = config.desktopCompatibility?.startOnProxyStart === true && (io.platform ?? process.platform) === "win32"
39
+ && !(io.testGuard ?? isTestHomeGuardArmed()) && !(io.sibling ?? siblingOfLivePort() !== null)
40
+ && config.runtimeRole !== "client";
41
+ const pending = enabled ? Promise.resolve().then(async () => {
42
+ if (stopped) return;
43
+ if (io.readiness) {
44
+ const ready = await waitForReadiness();
45
+ if (stopped) return;
46
+ if (!ready) throw new Error("desktop_compatibility_startup_not_ready");
47
+ }
48
+ module = await (io.load?.() ?? import("../../codex/desktop-compatibility/service"));
49
+ if (stopped) return;
50
+ await module.getDesktopCompatibilityRuntime().start();
51
+ }).catch(() => { (io.warn ?? console.warn)("Desktop compatibility observation did not start. Inspect Desktop compatibility status; no automatic trust or correction was applied."); }) : Promise.resolve();
52
+ return { async shutdown() {
53
+ stopped = true; cancelReadiness?.(); unbind(); await pending;
54
+ if (module) await module.shutdownDesktopCompatibility();
55
+ } };
56
+ }
@@ -1055,7 +1055,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1055
1055
  );
1056
1056
  const { buildAnthropicModelInfos } = await import("../../claude/model-info");
1057
1057
  const { resolveAutoContext } = await import("../../claude/context-windows");
1058
- const { activeDesktop3pAlias } = await import("../../claude/desktop-3p");
1058
+ const { resolvableDesktop3pAlias } = await import("../../claude/desktop-3p");
1059
1059
  // Per-surface id family (devlog 050): explicit ?ids= wins; otherwise the
1060
1060
  // Claude Code CLI discovery UA (`claude-code/<version>`, binary n_()) gets
1061
1061
  // readable claude-ocx ids and every other client (Desktop 3P) keeps the
@@ -1071,7 +1071,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1071
1071
  goOrdered,
1072
1072
  resolveAutoContext(config.claudeCode),
1073
1073
  idStyle,
1074
- activeDesktop3pAlias,
1074
+ resolvableDesktop3pAlias,
1075
1075
  desktopInputs.nativeContextCap,
1076
1076
  config.fastMode,
1077
1077
  // Explicit opt-out omits the Fast predicate.
@@ -277,3 +277,12 @@ export function warnPlaintextV2AgentMessagesStartup(config: { plaintextV2AgentMe
277
277
  console.warn(" Eligible ChatGPT collaboration calls may carry plaintext message arguments. HTTPS remains encrypted, but task text may be retained in Codex history, selected providers, and local response/debug state.");
278
278
  console.warn(" This depends on undocumented ChatGPT and Codex behavior; it does not decrypt existing tasks.");
279
279
  }
280
+
281
+ export function logProxyEndpoints(actualPort: number): void {
282
+ console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
283
+ console.log(` POST /v1/responses → provider translation`);
284
+ console.log(` POST /v1/chat/completions → OpenAI-compatible clients`);
285
+ console.log(` GET /healthz → health check`);
286
+ console.log(` GET /api/* → management API`);
287
+ console.log(` GET / → GUI dashboard`);
288
+ }
@@ -30,7 +30,9 @@ import {
30
30
  } from "../ws-bridge";
31
31
  import {
32
32
  CodexAccountCooldownError,
33
+ CodexMainAccountHardLockError,
33
34
  cooldownErrorMessage,
35
+ cooldownErrorResponse,
34
36
  } from "../../codex/auth-context";
35
37
  import { codexAccountNamespaceForModel } from "../../codex/account-namespace-match";
36
38
  import {
@@ -368,6 +370,13 @@ export function createWebsocketHandler(
368
370
  const accountSelector = typeof payload.model === "string"
369
371
  ? codexAccountNamespaceForModel(config.codexAccountNamespaces, payload.model)
370
372
  : undefined;
373
+ if (err instanceof CodexMainAccountHardLockError) {
374
+ // Same terminal usage-limit body and Retry-After as HTTP (#7046).
375
+ const refusal = cooldownErrorResponse(err, Date.now(), accountSelector);
376
+ const body = await refusal.json() as { error: Record<string, unknown> };
377
+ sendJsonFrame(ws, buildWsErrorFrame(429, body.error, refusal.headers));
378
+ return;
379
+ }
371
380
  sendJsonFrame(ws, buildWsErrorFrame(429, {
372
381
  type: "rate_limit_error",
373
382
  message: cooldownErrorMessage(err, accountSelector),
@@ -1,4 +1,5 @@
1
1
  import { remoteWorkspaceEnabled } from "../remote-control/workspace-activation";
2
+ import { scheduleDesktopCompatibilityStartup } from "./index/desktop-compatibility-startup";
2
3
  import { AuxiliaryListenerBindError } from "./ports";
3
4
  import { runAdmittedBodyWork } from "./inbound-body-admission";
4
5
  import {
@@ -24,6 +25,7 @@ import {
24
25
  websocketsEnabled,
25
26
  } from "../config";
26
27
  import { flushConfigDirHardeningAndReaps } from "../config/paths";
28
+ import { trackBackgroundWork } from "../lib/background-work";
27
29
  import { migrateStartupSubagentModels } from "./subagent-models-startup";
28
30
  import { migrateStartupXaiResponses } from "./xai-responses-startup";
29
31
  import { migrateStartupZaiResponses } from "./zai-responses-startup";
@@ -62,6 +64,7 @@ import {
62
64
  } from "../lib/app-owned-memory-stores";
63
65
  import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
64
66
  import { startPackageRefresh, stopPackageRefresh } from "../update/refresh-scheduler";
67
+ import { activateAdvisor } from "../lib/advisor-activation";
65
68
  import { activateLab, labActivationRequired } from "../lib/lab-activation";
66
69
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
67
70
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
@@ -198,7 +201,7 @@ import { createReadinessGate, type ReadinessGate } from "./readiness";
198
201
  import { createServeOptions, type ServerIngress } from "./index/serve-options";
199
202
  import { createOptionalListenerSet, LINK_INGRESS_HOSTNAME } from "./index/optional-listeners";
200
203
  import { createPackageTreeIntegrityGuardForServer } from "./index/package-tree-guard";
201
- import { inspectStartupOwnership, resolveInboundBodyLimitWithWarning, setStartupCacheInvalidationWrite, warnAgentTaskRecoveryStartup, warnPlaintextV2AgentMessagesStartup, type StartServerDeps } from "./index/startup-warnings";
204
+ import { inspectStartupOwnership, logProxyEndpoints, resolveInboundBodyLimitWithWarning, setStartupCacheInvalidationWrite, warnAgentTaskRecoveryStartup, warnPlaintextV2AgentMessagesStartup, type StartServerDeps } from "./index/startup-warnings";
202
205
  import { acquireSpendLedgerServerLifecycle, recordFailedStartRollback, type SpendLedgerServerLifecycle } from "./index/spend-ledger-lifecycle";
203
206
  export { waitForFailedStartRollback } from "./index/spend-ledger-lifecycle";
204
207
 
@@ -638,11 +641,14 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
638
641
  let unregisterQuotaAutoRefresh: (() => void) | null = null;
639
642
  let remoteWorkspaceStopping = false;
640
643
  let remoteWorkspaceShutdown: (() => Promise<void>) | undefined;
644
+ let desktopCompatibilityStartup: ReturnType<typeof scheduleDesktopCompatibilityStartup> | undefined;
645
+ let desktopCompatibilityShutdown: (() => Promise<void>) | undefined;
641
646
  const managementApiDeps: ManagementApiDeps = {
642
647
  ...deps.managementApi,
643
648
  listLowQuotaEvents: limit => backgroundLifecycle?.listLowQuotaEvents(limit) ?? [],
644
649
  remoteWorkspaceStopping: () => remoteWorkspaceStopping,
645
650
  onRemoteWorkspaceShutdown: shutdown => { remoteWorkspaceShutdown = shutdown; }, linkSupervisor: () => optionalListeners.linkSupervisor(), linkListener: () => optionalListeners,
651
+ onDesktopCompatibilityShutdown: shutdown => { desktopCompatibilityShutdown = shutdown; },
646
652
  };
647
653
  let workspaceRuntimeFlight: Promise<typeof import("../remote-control/workspace-runtime")> | undefined;
648
654
  const loadRemoteWorkspaceRuntime = () => {
@@ -772,6 +778,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
772
778
  : []),
773
779
  () => optionalListeners.stop(),
774
780
  async () => { await remoteWorkspaceShutdown?.(); },
781
+ async () => { await desktopCompatibilityStartup?.shutdown(); await desktopCompatibilityShutdown?.(); },
775
782
  async () => {
776
783
  try {
777
784
  userCostOverlayReconciler?.stop();
@@ -801,13 +808,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
801
808
  boundPort = actualPort;
802
809
  setCorsOrigin(actualPort);
803
810
 
804
- console.log(`🚀 opencodex proxy running on http://localhost:${actualPort}`);
805
- console.log(` POST /v1/responses → provider translation`);
806
- console.log(` POST /v1/chat/completions → OpenAI-compatible clients`);
807
- console.log(` GET /healthz → health check`);
808
- console.log(` GET /api/* → management API`);
809
- console.log(` GET / → GUI dashboard`);
810
-
811
+ logProxyEndpoints(actualPort);
811
812
  if (loopbackServer) {
812
813
  // Loud on every start, not once at enable time. An operator who inherits a config, or
813
814
  // who forgot, has to be able to see that an unauthenticated surface is live without
@@ -843,7 +844,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
843
844
  && isCanonicalOpenAiForwardProvider(openAiProvider)
844
845
  && providerCodexAccountMode("openai", openAiProvider) === "pool"
845
846
  ) {
846
- import("../codex/plan-from-token")
847
+ void trackBackgroundWork("codex-pool-quota-prime-startup", import("../codex/plan-from-token")
847
848
  .then(({ reconcileCodexPlansFromTokens }) => {
848
849
  try {
849
850
  reconcileCodexPlansFromTokens(config);
@@ -853,7 +854,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
853
854
  return import("../codex/auth-api");
854
855
  })
855
856
  .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "startup"))
856
- .catch(() => {});
857
+ .catch(() => {}));
857
858
  }
858
859
 
859
860
  // Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
@@ -864,6 +865,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
864
865
  // startServer returns, in the same turn as Bun.serve, so a policy route can never be
865
866
  // evaluated before its evidence provider is registered. That ordering is load-bearing:
866
867
  // the subagent-fallback chain routes synchronously and has nowhere to await.
868
+ activateAdvisor(config);
867
869
  const labConfigDir = getConfigDir();
868
870
  if (labActivationRequired(config, labConfigDir)) {
869
871
  activateLab(config, labConfigDir);
@@ -879,6 +881,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
879
881
  }
880
882
 
881
883
  startPackageRefresh();
884
+ desktopCompatibilityStartup = scheduleDesktopCompatibilityStartup(config, { boundHostname: bindHost, boundPort: actualPort, loopbackPort: loopbackServer?.port ?? undefined, readiness: deps.readinessGate });
882
885
  return server;
883
886
  }
884
887