@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -19,7 +19,10 @@ import {
19
19
  websocketsEnabled,
20
20
  } from "../config";
21
21
  import { reconcileOAuthProviders } from "../oauth";
22
- import { invalidateCodexModelsCache } from "../codex/catalog";
22
+ import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
23
+ import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
24
+ import { getCodexHome } from "../codex/paths";
25
+ import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
23
26
  import { startMemoryWatchdog } from "./memory-watchdog";
24
27
  import {
25
28
  reconcileLiveStateStores,
@@ -48,6 +51,7 @@ import {
48
51
  CodexAccountCooldownError,
49
52
  cooldownErrorMessage,
50
53
  } from "../codex/auth-context";
54
+ import { codexAccountNamespaceForModel } from "../codex/account-namespace-match";
51
55
  export {
52
56
  clearThreadAccountMap,
53
57
  formatCodexProviderForLog,
@@ -56,7 +60,7 @@ export {
56
60
  import { formatCodexProviderForLog } from "../codex/routing";
57
61
  import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
58
62
  import { registerCodexWebSocket, tryReserveCodexWebSocket, unregisterCodexWebSocket, updateCodexWebSocketAuthContext } from "../codex/websocket-registry";
59
- import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile } from "./gui-static";
63
+ import { resolveGuiFilePath, rootFallbackPayload, serveGuiFile, serveSessionBootstrap } from "./gui-static";
60
64
  export { resolveGuiFilePath, rootFallbackPayload } from "./gui-static";
61
65
  export { resolveAdapter } from "./adapter-resolve";
62
66
  import { formatErrorResponse, type ResponsesTerminalStatus } from "../bridge";
@@ -153,24 +157,95 @@ import { handleChatCompletions } from "./chat-completions";
153
157
  import { anthropicErrorResponse } from "../claude/outbound";
154
158
  import { buildDesktop3pRegistry } from "../claude/desktop-3p";
155
159
  import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
160
+ import {
161
+ bindNativeMainStartupLifecycle,
162
+ releaseNativeMainStartupLifecycle,
163
+ startNativeMainStartupLifecycle,
164
+ type NativeMainStartupGateDeps,
165
+ } from "../codex/native-profile-startup";
156
166
  import { handleImages } from "./images";
157
167
  import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
158
168
  import { handleSearch } from "./search";
159
- import { fetchAllModels, handleManagementAPI, VERSION } from "./management-api";
160
- import { initializeManagementAuthState, issueGuiSession, requireManagementAuth } from "./management-auth";
169
+ import { fetchAllModels, handleManagementAPI, VERSION, type ManagementApiDeps } from "./management-api";
170
+ import {
171
+ initializeManagementAuthState,
172
+ issueGuiSession,
173
+ managementPrincipal,
174
+ requireManagementAuth,
175
+ type ManagementAuthState,
176
+ } from "./management-auth";
161
177
 
162
178
  const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
163
179
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
164
180
  const LIVE_SIDEBAND_PENDING_MAX = 32;
181
+ const LIVE_SIDEBAND_CLOSE_FALLBACK_MS = 1_000;
165
182
 
166
- function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
167
- try {
168
- ws.data.liveUpstream?.close(code, reason);
169
- } catch {
170
- /* upstream already gone */
183
+ type LiveSidebandWebSocketFactory = (
184
+ url: string,
185
+ headers: Record<string, string>,
186
+ ) => WebSocket;
187
+
188
+ function releaseLiveSidebandAdmission(ws: ServerWebSocket<WsData>): void {
189
+ ws.data.liveTurnAdmissionLease?.release();
190
+ ws.data.liveTurnAdmissionLease = undefined;
191
+ }
192
+
193
+ function finalizeLiveSideband(ws: ServerWebSocket<WsData>, upstream?: WebSocket): void {
194
+ if (upstream && ws.data.liveUpstream !== upstream) return;
195
+ if (ws.data.liveCloseFallback !== undefined) {
196
+ clearTimeout(ws.data.liveCloseFallback);
197
+ ws.data.liveCloseFallback = undefined;
171
198
  }
172
199
  ws.data.liveUpstream = undefined;
173
200
  ws.data.livePending = undefined;
201
+ ws.data.cancel = undefined;
202
+ releaseLiveSidebandAdmission(ws);
203
+ }
204
+
205
+ function armLiveSidebandCloseFallback(ws: ServerWebSocket<WsData>, upstream: WebSocket): void {
206
+ if (ws.data.liveCloseFallback !== undefined) return;
207
+ ws.data.liveCloseFallback = setTimeout(() => {
208
+ ws.data.liveCloseFallback = undefined;
209
+ if (ws.data.liveUpstream !== upstream) return;
210
+ if (upstream.readyState === WebSocket.CLOSED) {
211
+ finalizeLiveSideband(ws, upstream);
212
+ return;
213
+ }
214
+ // A close frame was already sent below. Retry once, but never surrender
215
+ // native-main ownership while the authenticated transport remains live.
216
+ try {
217
+ upstream.close(1000, "upstream close timeout");
218
+ } catch {
219
+ /* upstream is already unusable */
220
+ }
221
+ // Some implementations transition synchronously without delivering the
222
+ // close event. That is still an observed CLOSED transport and is safe to
223
+ // finalize. CONNECTING/CLOSING peers keep the lease so profile switching
224
+ // fails at its own bounded drain deadline instead of racing live traffic.
225
+ if (upstream.readyState === WebSocket.CLOSED) finalizeLiveSideband(ws, upstream);
226
+ }, LIVE_SIDEBAND_CLOSE_FALLBACK_MS);
227
+ }
228
+
229
+ function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""): void {
230
+ if (ws.data.liveClosing) return;
231
+ ws.data.liveClosing = true;
232
+ ws.data.livePending = undefined;
233
+ ws.data.cancel = undefined;
234
+ const upstream = ws.data.liveUpstream;
235
+ if (!upstream || upstream.readyState === WebSocket.CLOSED) {
236
+ finalizeLiveSideband(ws, upstream);
237
+ } else {
238
+ // The sideband holds a native-main admission lease. Do not release it just
239
+ // because the downstream left: its authenticated upstream remains live
240
+ // until the close event arrives or the transport is observed CLOSED. The
241
+ // bounded fallback only retries close; it does not release ownership.
242
+ armLiveSidebandCloseFallback(ws, upstream);
243
+ try {
244
+ upstream.close(code, reason);
245
+ } catch {
246
+ /* the fallback retries close without releasing ownership */
247
+ }
248
+ }
174
249
  try {
175
250
  if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
176
251
  ws.close(code, reason);
@@ -180,7 +255,12 @@ function closeLiveSideband(ws: ServerWebSocket<WsData>, code = 1000, reason = ""
180
255
  }
181
256
  }
182
257
 
183
- function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
258
+ function attachLiveSidebandUpstream(
259
+ ws: ServerWebSocket<WsData>,
260
+ createWebSocket: LiveSidebandWebSocketFactory = (url, headers) => (
261
+ new WebSocket(url, { headers } as unknown as string[])
262
+ ),
263
+ ): void {
184
264
  const url = ws.data.liveUpstreamUrl;
185
265
  if (!url) {
186
266
  closeLiveSideband(ws, 1011, "missing upstream");
@@ -189,21 +269,17 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
189
269
  let upstream: WebSocket;
190
270
  try {
191
271
  // Bun accepts per-handshake headers; the DOM lib types only list protocol arrays.
192
- upstream = new WebSocket(url, { headers: ws.data.liveUpstreamHeaders ?? {} } as unknown as string[]);
272
+ upstream = createWebSocket(url, ws.data.liveUpstreamHeaders ?? {});
193
273
  } catch {
194
274
  closeLiveSideband(ws, 1011, "upstream connect failed");
195
275
  return;
196
276
  }
197
277
  ws.data.liveUpstream = upstream;
198
- ws.data.cancel = () => {
199
- try {
200
- upstream.close(1000, "client closed");
201
- } catch {
202
- /* ignore */
203
- }
204
- };
278
+ ws.data.liveClosing = false;
279
+ ws.data.cancel = () => closeLiveSideband(ws, 1000, "client closed");
205
280
 
206
281
  upstream.addEventListener("open", () => {
282
+ if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
207
283
  ws.data.liveOpened = true;
208
284
  const pending = ws.data.livePending ?? [];
209
285
  ws.data.livePending = undefined;
@@ -217,6 +293,7 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
217
293
  }
218
294
  });
219
295
  upstream.addEventListener("message", (event) => {
296
+ if (ws.data.liveUpstream !== upstream || ws.data.liveClosing) return;
220
297
  try {
221
298
  logLiveSidebandFrame("u2c", event.data);
222
299
  if (typeof event.data === "string") ws.send(event.data);
@@ -229,14 +306,17 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
229
306
  }
230
307
  });
231
308
  upstream.addEventListener("close", (event) => {
309
+ if (ws.data.liveUpstream !== upstream) return;
310
+ ws.data.liveClosing = true;
311
+ finalizeLiveSideband(ws, upstream);
232
312
  try {
233
313
  ws.close(event.code || 1000, event.reason || "");
234
314
  } catch {
235
315
  /* ignore */
236
316
  }
237
- ws.data.liveUpstream = undefined;
238
317
  });
239
318
  upstream.addEventListener("error", () => {
319
+ if (ws.data.liveUpstream !== upstream) return;
240
320
  closeLiveSideband(ws, 1011, "upstream error");
241
321
  });
242
322
  }
@@ -250,18 +330,15 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
250
330
  // if (isEventStream && upstreamResponse.body) {
251
331
  // const repairConfig = route.provider.responsesItemIdRepair;
252
332
  // const needsClientRewrite = imageGenCallAliases.size > 0
253
- // #314 gated shape: win32 no-rewrite traffic follows runtime/config policy; darwin no-rewrite
254
- // traffic requires explicit config-eager opt-in (`auto` always stays tee on darwin). Default OFF
255
- // on the bundled known-bad runtime; policy lives in 260731_macos_rss_retention phase 100.
333
+ // #314 gated shape: win32 always uses the terminal-aware eager relay so a keep-alive
334
+ // upstream cannot hold Codex open after response.completed; darwin no-rewrite traffic
335
+ // requires explicit config-eager opt-in (`auto` always stays tee on darwin).
256
336
  // selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
257
337
  // relaySseEagerBounded(upstreamResponse.body, turnAc,
258
338
  // new Response(eagerBody,
259
339
  // Default shape (tee + background inspection):
260
340
  // upstreamResponse.body.tee()
261
341
  // const repairedBody = hasResponsesItemIdRepair(repairConfig)
262
- // process.platform === "win32"
263
- // && !needsClientRewrite
264
- // ? nativeBody
265
342
  // relaySseWithFailedTail(repairedBody, upstream)
266
343
  // new Response(clientBody
267
344
  // markNativePassthroughSseResponse
@@ -271,12 +348,25 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
271
348
  // trackSseForRequestLog(
272
349
  // export function relaySseWithHeartbeat
273
350
 
274
- export function startServer(port?: number) {
351
+ export interface StartServerDeps {
352
+ /** Test-only seam; production always initializes its own management credential state. */
353
+ managementAuthState?: ManagementAuthState;
354
+ /** Test-only route dependencies, forwarded only after management admission succeeds. */
355
+ managementApi?: ManagementApiDeps;
356
+ /** Test-only native-main recovery dependencies; production constructs the normal manager. */
357
+ nativeMainStartup?: NativeMainStartupGateDeps;
358
+ /** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
359
+ liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
360
+ }
361
+
362
+ export function startServer(port?: number, deps: StartServerDeps = {}) {
275
363
  const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
276
364
  setLiveStateStoreConfig(config);
277
365
  applyProxyEnv(config);
278
366
  assertServerAuthConfig(config);
279
- const managementAuth = initializeManagementAuthState(config);
367
+ const managementAuth = deps.managementAuthState ?? initializeManagementAuthState(config);
368
+ // Arm synchronously before listen. A pending journal therefore makes __main__ unusable
369
+ // before any request can resolve its physical credential, while health/management/Pool stay live.
280
370
  // Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
281
371
  // adding/dropping models reaches existing configs on start — not just fresh installs.
282
372
  reconcileOAuthProviders(config);
@@ -310,7 +400,16 @@ export function startServer(port?: number) {
310
400
  if (migrated) saveConfig(config);
311
401
  }
312
402
  }
313
- invalidateCodexModelsCache();
403
+ // Startup cache invalidation is best-effort and must never block the server from
404
+ // serving. It now takes K so it cannot race a convergence commit, but both the
405
+ // home resolution and the acquisition can fail on a machine with no Codex home —
406
+ // `getCodexHome()` THROWS when CODEX_HOME names a missing directory, which would
407
+ // otherwise turn "no Codex installed" into "proxy will not start".
408
+ try {
409
+ const startupCodexHome = getCodexHome();
410
+ withCatalogWriteSerialization(startupCodexHome, permit =>
411
+ invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
412
+ } catch { /* no readable Codex home: nothing to invalidate */ }
314
413
  // Arm the `claudeCode` hand-edit guard (devlog 260726_claude_auth_auto/040 H1) BEFORE
315
414
  // the server can serve a request, and AFTER the startup migrations above — those run
316
415
  // against a config nobody else holds and are the documented exception to the save
@@ -329,6 +428,7 @@ export function startServer(port?: number) {
329
428
  registerAppOwnedMemorySweepFallback();
330
429
  configureAppOwnedMemoryBudget(resolveAppOwnedMemoryBudgetBytes(config.appOwnedMemoryBudgetMb));
331
430
  enforceAppOwnedMemoryBudget();
431
+ registerCodexCooldownRecoveryProbeWorker(config);
332
432
  startStateStoreSweeper();
333
433
  // Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
334
434
  // tick for daily/weekly; startup evaluation is fire-and-forget after listen.
@@ -388,7 +488,10 @@ export function startServer(port?: number) {
388
488
  return response;
389
489
  }
390
490
 
391
- const server: Server<WsData> = Bun.serve<WsData>({
491
+ const nativeMainLifecycle = startNativeMainStartupLifecycle(deps.nativeMainStartup);
492
+ let server: Server<WsData>;
493
+ try {
494
+ server = Bun.serve<WsData>({
392
495
  port: listenPort,
393
496
  hostname: bindHost,
394
497
  idleTimeout: 255,
@@ -448,7 +551,11 @@ export function startServer(port?: number) {
448
551
  if (url.pathname.startsWith("/api/")) {
449
552
  const apiAuthError = requireManagementAuth(req, managementAuth, config);
450
553
  if (apiAuthError) return withManagementCors(apiAuthError, req, config);
451
- const mgmtResponse = await handleManagementAPI(req, url, config);
554
+ // Which credential passed the gate, resolved from the same session table the
555
+ // gate used. Consent-bearing routes need this: request headers are forgeable
556
+ // by anything holding the admin token, the credential is not.
557
+ const principal = managementPrincipal(req, managementAuth, config) ?? undefined;
558
+ const mgmtResponse = await handleManagementAPI(req, url, config, deps.managementApi, principal);
452
559
  if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
453
560
  return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
454
561
  }
@@ -474,8 +581,15 @@ export function startServer(port?: number) {
474
581
  }
475
582
  throw error;
476
583
  }
477
- const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
478
- const nativeSlugs = nativeOpenAiSlugs();
584
+ const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
585
+ const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
586
+ const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
587
+ const nativeSlugs = includeNativeOpenAi ? nativeOpenAiSlugs() : [];
588
+ const disabledNatives = disabledNativeSlugs(config);
589
+ const disabledModels = new Set(config.disabledModels ?? []);
590
+ const accountSelectors = includeAccountBoundNativeOpenAi
591
+ ? visibleCodexAccountSelectors(config)
592
+ : [];
479
593
  const goEnabled = filterCatalogVisibleModels(goModels, config);
480
594
  const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
481
595
  // Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
@@ -519,8 +633,20 @@ export function startServer(port?: number) {
519
633
  // Disabled natives stay in the catalog shape with visibility "hide" (mirrors the
520
634
  // on-disk sync; codex-rs keeps them out of the picker itself).
521
635
  const maMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
522
- const entries = buildCatalogEntries(loadCatalogTemplate(), nativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config));
523
- return jsonResponse({ models: applyNativeVisibility(entries, disabledNativeSlugs(config)) }, 200, req, config);
636
+ // Account rows use the same hidden-inclusive supported set as on-disk sync. This lets a
637
+ // newly re-enabled native reappear under each selector before the next sync, while the
638
+ // no-selector path keeps nativeOpenAiSlugs()'s existing visibility-sensitive behavior.
639
+ const catalogNativeSlugs = accountSelectors.length > 0
640
+ ? NATIVE_OPENAI_MODELS
641
+ : nativeSlugs;
642
+ const entries = buildCatalogEntries(loadCatalogTemplate(), catalogNativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config), accountSelectors);
643
+ return jsonResponse({
644
+ models: applyNativeVisibility(
645
+ entries,
646
+ disabledModels,
647
+ accountSelectors.length > 0,
648
+ ),
649
+ }, 200, req, config);
524
650
  }
525
651
  // OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
526
652
  // (pure availability list — disabled natives are omitted entirely).
@@ -548,14 +674,34 @@ export function startServer(port?: number) {
548
674
  reasoning_efforts: efforts.map(effort => grokEffortOption(effort, effort === defaultEffort)),
549
675
  };
550
676
  };
551
- const data = [
552
- ...visibleNativeSlugs(config).map(id => ({
677
+ const nativeModelRow = (id: string, metadataId = id) => ({
553
678
  id,
554
679
  object: "model",
555
680
  created: 0,
556
681
  owned_by: "openai",
557
- ...grokEffortFields(nativeReasoningEfforts(id), nativeDefaultReasoningEffort(id)),
558
- })),
682
+ ...grokEffortFields(
683
+ nativeReasoningEfforts(metadataId),
684
+ nativeDefaultReasoningEffort(metadataId),
685
+ ),
686
+ });
687
+ // Selector-active discovery follows the same complete supported set as the Codex catalog
688
+ // for both bare and qualified rows. Without selectors, the live catalog continues to own
689
+ // bare availability.
690
+ const selectorNativeSlugs = accountSelectors.length > 0
691
+ ? NATIVE_OPENAI_MODELS.filter(slug => !disabledNatives.has(slug))
692
+ : [];
693
+ const visibleNatives = includeNativeOpenAi
694
+ ? accountSelectors.length > 0 ? selectorNativeSlugs : visibleNativeSlugs(config)
695
+ : [];
696
+ const visibleAccountNatives = accountSelectors.flatMap(selector =>
697
+ selectorNativeSlugs.flatMap(metadataId => {
698
+ const id = `${selector}/${metadataId}`;
699
+ return disabledModels.has(id) ? [] : [{ id, metadataId }];
700
+ })
701
+ );
702
+ const data = [
703
+ ...visibleNatives.map(id => nativeModelRow(id)),
704
+ ...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
559
705
  ...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
560
706
  id: m.alias ?? `${m.provider}/${m.id}`,
561
707
  object: "model",
@@ -587,10 +733,10 @@ export function startServer(port?: number) {
587
733
  ...admissionFields(admission),
588
734
  inboundProtocol: "responses",
589
735
  };
590
- return runAdmittedHttpTurn(req, async () => {
736
+ return runAdmittedHttpTurn(req, async turnAdmissionLease => {
591
737
  let response: Response;
592
738
  try {
593
- response = await handleResponsesCompact(req, config, logCtx);
739
+ response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease);
594
740
  } catch {
595
741
  response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
596
742
  }
@@ -621,8 +767,8 @@ export function startServer(port?: number) {
621
767
  ...admissionFields(admission),
622
768
  };
623
769
  const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
624
- return runAdmittedHttpTurn(req, async () => {
625
- const response = await handleImages(req, config, endpoint, logCtx);
770
+ return runAdmittedHttpTurn(req, async turnAdmissionLease => {
771
+ const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease);
626
772
  addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
627
773
  return withCors(response, req, config);
628
774
  });
@@ -675,8 +821,8 @@ export function startServer(port?: number) {
675
821
  provider: "unknown",
676
822
  ...admissionFields(admission),
677
823
  };
678
- return runAdmittedHttpTurn(req, async () => {
679
- const response = await handleSearch(req, config, logCtx);
824
+ return runAdmittedHttpTurn(req, async turnAdmissionLease => {
825
+ const response = await handleSearch(req, config, logCtx, turnAdmissionLease);
680
826
  addFinalRequestLog(requestId, start, logCtx, response.status,
681
827
  response.status === 499 ? { closeReason: "client_cancel" } : undefined);
682
828
  return withCors(response, req, config);
@@ -825,8 +971,8 @@ export function startServer(port?: number) {
825
971
  provider: "unknown",
826
972
  ...admissionFields(admission),
827
973
  };
828
- return runAdmittedHttpTurn(req, async () => {
829
- const response = await handleLive(req, config, logCtx);
974
+ return runAdmittedHttpTurn(req, async turnAdmissionLease => {
975
+ const response = await handleLive(req, config, logCtx, turnAdmissionLease);
830
976
  addFinalRequestLog(
831
977
  requestId,
832
978
  start,
@@ -859,8 +1005,17 @@ export function startServer(port?: number) {
859
1005
  provider: "unknown",
860
1006
  ...admissionFields(admission),
861
1007
  };
862
- const resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget);
1008
+ const turnAdmissionLease = tryAdmitTurn();
1009
+ if (!turnAdmissionLease) return serverBusyResponse(req, "active turns");
1010
+ let resolved;
1011
+ try {
1012
+ resolved = await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease);
1013
+ } catch (error) {
1014
+ turnAdmissionLease.release();
1015
+ throw error;
1016
+ }
863
1017
  if (resolved instanceof Response) {
1018
+ turnAdmissionLease.release();
864
1019
  addFinalRequestLog(requestId, start, logCtx, resolved.status);
865
1020
  return withCors(resolved, req, config);
866
1021
  }
@@ -872,8 +1027,10 @@ export function startServer(port?: number) {
872
1027
  liveUpstreamHeaders: resolved.headers,
873
1028
  livePending: [],
874
1029
  liveOpened: false,
1030
+ liveTurnAdmissionLease: turnAdmissionLease,
875
1031
  } satisfies WsData,
876
1032
  })) return undefined as unknown as Response;
1033
+ turnAdmissionLease.release();
877
1034
  return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, config);
878
1035
  }
879
1036
 
@@ -888,6 +1045,11 @@ export function startServer(port?: number) {
888
1045
  const guiSessionCandidate = req.method === "GET" && (url.pathname === "/" || !url.pathname.includes("."))
889
1046
  ? issueGuiSession(req, config, managementAuth)
890
1047
  : null;
1048
+ // Dedicated bootstrap path: answer without requiring a packaged GUI build, so the
1049
+ // Vite dev server can mint an origin-bound loopback session on a fresh checkout.
1050
+ if (url.pathname === "/opencodex-session" && guiSessionCandidate) {
1051
+ return serveSessionBootstrap(guiSessionCandidate);
1052
+ }
891
1053
  const guiFile = serveGuiFile(url.pathname, undefined, guiSessionCandidate ?? undefined);
892
1054
  if (guiFile) return guiFile;
893
1055
  if (url.pathname === "/" && req.method === "GET") {
@@ -904,7 +1066,11 @@ export function startServer(port?: number) {
904
1066
  // Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
905
1067
  open(ws: ServerWebSocket<WsData>) {
906
1068
  if (ws.data.kind === "live-sideband") {
907
- attachLiveSidebandUpstream(ws);
1069
+ if (!ws.data.liveTurnAdmissionLease) {
1070
+ closeLiveSideband(ws, 1013, "server busy");
1071
+ return;
1072
+ }
1073
+ attachLiveSidebandUpstream(ws, deps.liveSidebandWebSocketFactory);
908
1074
  return;
909
1075
  }
910
1076
  if (!ws.data.admissionLease) {
@@ -916,6 +1082,7 @@ export function startServer(port?: number) {
916
1082
  },
917
1083
  message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
918
1084
  if (ws.data.kind === "live-sideband") {
1085
+ if (ws.data.liveClosing) return;
919
1086
  logLiveSidebandFrame("c2u", raw);
920
1087
  const upstream = ws.data.liveUpstream;
921
1088
  if (!upstream || upstream.readyState === WebSocket.CONNECTING || !ws.data.liveOpened) {
@@ -1030,6 +1197,7 @@ export function startServer(port?: number) {
1030
1197
  let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
1031
1198
  const response = await handleResponses(req, config, logCtx, {
1032
1199
  forceEmptyResponseId: true,
1200
+ inboundTransport: "websocket",
1033
1201
  abortSignal: turnAbort.signal,
1034
1202
  turnAdmissionLease,
1035
1203
  onFirstOutput: () => recordFirstOutput(logCtx, start),
@@ -1057,9 +1225,12 @@ export function startServer(port?: number) {
1057
1225
  finalizeLog(429);
1058
1226
  // Codex Desktop rides this WS transport, so it must carry the same
1059
1227
  // actionable text as HTTP; a frame has no headers, hence message-only.
1228
+ const accountSelector = typeof payload.model === "string"
1229
+ ? codexAccountNamespaceForModel(config.codexAccountNamespaces, payload.model)
1230
+ : undefined;
1060
1231
  sendJsonFrame(ws, buildWsErrorFrame(429, {
1061
1232
  type: "rate_limit_error",
1062
- message: cooldownErrorMessage(err),
1233
+ message: cooldownErrorMessage(err, accountSelector),
1063
1234
  }));
1064
1235
  return;
1065
1236
  }
@@ -1080,8 +1251,7 @@ export function startServer(port?: number) {
1080
1251
  },
1081
1252
  close(ws: ServerWebSocket<WsData>) {
1082
1253
  if (ws.data.kind === "live-sideband") {
1083
- ws.data.cancel?.();
1084
- ws.data.liveUpstream = undefined;
1254
+ closeLiveSideband(ws);
1085
1255
  return;
1086
1256
  }
1087
1257
  unregisterCodexWebSocket(ws);
@@ -1090,8 +1260,24 @@ export function startServer(port?: number) {
1090
1260
  ws.data.cancel?.(); // RC2: abort the upstream when the client disconnects
1091
1261
  },
1092
1262
  },
1093
- });
1263
+ });
1264
+ } catch (error) {
1265
+ void nativeMainLifecycle.release();
1266
+ throw error;
1267
+ }
1094
1268
 
1269
+ bindNativeMainStartupLifecycle(server, nativeMainLifecycle);
1270
+ const nativeStop = server.stop.bind(server);
1271
+ Object.defineProperty(server, "stop", {
1272
+ configurable: true,
1273
+ value: async (closeActiveConnections?: boolean): Promise<void> => {
1274
+ try {
1275
+ await nativeStop(closeActiveConnections);
1276
+ } finally {
1277
+ await releaseNativeMainStartupLifecycle(server);
1278
+ }
1279
+ },
1280
+ });
1095
1281
  setServerRef(server);
1096
1282
  const actualPort = server.port ?? listenPort;
1097
1283
  setCorsOrigin(actualPort);