@bitkyc08/opencodex 2.80.0 → 2.82.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 (376) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +166 -73
  3. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  4. package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
  5. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  6. package/gui/dist/assets/index-D3FBCNiP.js +94 -0
  7. package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/claude-green.svg +1 -0
  10. package/package.json +3 -2
  11. package/src/adapters/anthropic-model-contract.ts +8 -2
  12. package/src/adapters/anthropic.ts +8 -3
  13. package/src/adapters/codebuddy/mcp-server.ts +2 -25
  14. package/src/adapters/codebuddy/tool-bridge.ts +1 -7
  15. package/src/adapters/coding-agent/mcp-server.ts +33 -0
  16. package/src/adapters/coding-agent/protocol.ts +122 -17
  17. package/src/adapters/coding-agent/tool-catalog.ts +75 -0
  18. package/src/adapters/coding-agent/turn.ts +101 -44
  19. package/src/adapters/cursor/catalog.ts +8 -0
  20. package/src/adapters/cursor/effort-map.ts +2 -0
  21. package/src/adapters/devin/live-models.ts +2 -0
  22. package/src/adapters/kiro-retry.ts +9 -4
  23. package/src/adapters/openai-chat/messages.ts +7 -6
  24. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  25. package/src/adapters/openai-responses/passthrough.ts +9 -4
  26. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  27. package/src/adapters/physical-send.ts +8 -5
  28. package/src/adapters/qoder/adapter.ts +41 -6
  29. package/src/adapters/qoder/scaffold-guard.ts +2 -3
  30. package/src/adapters/zed.ts +16 -1
  31. package/src/claude/agents-inject.ts +14 -3
  32. package/src/claude/context-windows.ts +19 -17
  33. package/src/claude/desktop-3p.ts +88 -12
  34. package/src/claude/desktop-profile.ts +2 -1
  35. package/src/claude/gateway-cache.ts +18 -1
  36. package/src/claude/inbound-cache-stabilize.ts +4 -9
  37. package/src/claude/inbound.ts +78 -3
  38. package/src/claude/intercept/cli-catalog.ts +3 -0
  39. package/src/claude/intercept/model-bindings.ts +1 -0
  40. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  41. package/src/claude/intercept/picker-listener.ts +71 -4
  42. package/src/claude/intercept/picker-models.ts +14 -6
  43. package/src/claude/intercept/settings.ts +3 -0
  44. package/src/claude/long-context.ts +66 -0
  45. package/src/claude/model-info.ts +19 -16
  46. package/src/claude/outbound.ts +94 -9
  47. package/src/claude/subagent-model.ts +40 -11
  48. package/src/cli/account-anthropic-threshold.ts +14 -5
  49. package/src/cli/account-api.ts +3 -1
  50. package/src/cli/account-extended.ts +13 -9
  51. package/src/cli/account-policy-dto.ts +1 -1
  52. package/src/cli/account-policy.ts +13 -4
  53. package/src/cli/account.ts +3 -3
  54. package/src/cli/agent.ts +9 -5
  55. package/src/cli/aside-profile-recovery.ts +33 -0
  56. package/src/cli/capabilities-accounts.ts +2 -2
  57. package/src/cli/capabilities-agents-routing.ts +24 -0
  58. package/src/cli/capabilities-base.ts +5 -5
  59. package/src/cli/capabilities-integrations.ts +18 -2
  60. package/src/cli/cli-path-diagnostics.ts +132 -0
  61. package/src/cli/codex-shim-autorestore.ts +3 -0
  62. package/src/cli/desktop-runtime-guidance.ts +65 -0
  63. package/src/cli/dispatch.ts +15 -1
  64. package/src/cli/doctor.ts +16 -3
  65. package/src/cli/help.ts +4 -1
  66. package/src/cli/index.ts +25 -14
  67. package/src/cli/integration-aside-sync.ts +2 -1
  68. package/src/cli/integration-plan-dto.ts +1 -0
  69. package/src/cli/integration-preview.ts +8 -1
  70. package/src/cli/integrations.ts +48 -6
  71. package/src/cli/message-args.ts +34 -0
  72. package/src/cli/message-command.ts +43 -0
  73. package/src/cli/message-runtime.ts +6 -0
  74. package/src/cli/observe.ts +3 -1
  75. package/src/cli/provider.ts +29 -6
  76. package/src/cli/registry.ts +24 -2
  77. package/src/cli/resolve.ts +25 -2
  78. package/src/cli/root.ts +4 -2
  79. package/src/cli/status.ts +64 -12
  80. package/src/cli/update-restart-child.ts +4 -2
  81. package/src/cli/update-restart-home.ts +28 -10
  82. package/src/cli/update-restart-service-record.ts +116 -0
  83. package/src/cli/update-restart-supervision.ts +96 -0
  84. package/src/cli/update-restart.ts +59 -13
  85. package/src/cli/v2.ts +27 -20
  86. package/src/cli/version-skew-notice.ts +77 -0
  87. package/src/clients/config-export/commandcode.ts +131 -0
  88. package/src/clients/config-export/contracts.ts +12 -0
  89. package/src/clients/config-export.ts +64 -1
  90. package/src/clients/lazycodex.ts +1 -1
  91. package/src/clients/omo-role-models.ts +32 -7
  92. package/src/codex/auth-api/account-list.ts +11 -3
  93. package/src/codex/auth-api/account-selection.ts +107 -0
  94. package/src/codex/auth-api/main-account-probe.ts +25 -8
  95. package/src/codex/auth-api/pool-mode-gate.ts +33 -9
  96. package/src/codex/auth-api/pool-quota-probe.ts +15 -1
  97. package/src/codex/auth-api/routes.ts +11 -49
  98. package/src/codex/auth-context.ts +20 -1
  99. package/src/codex/autostart-health.ts +12 -3
  100. package/src/codex/catalog/auto-review.ts +13 -6
  101. package/src/codex/catalog/build-entries.ts +33 -7
  102. package/src/codex/catalog/bundled.ts +155 -26
  103. package/src/codex/catalog/combo-member.ts +3 -2
  104. package/src/codex/catalog/control-plane.ts +45 -0
  105. package/src/codex/catalog/effort.ts +37 -6
  106. package/src/codex/catalog/gather-capture.ts +10 -3
  107. package/src/codex/catalog/model-hints.ts +15 -8
  108. package/src/codex/catalog/model-visibility.ts +3 -1
  109. package/src/codex/catalog/operator-tiers.ts +38 -0
  110. package/src/codex/catalog/parsing.ts +2 -1
  111. package/src/codex/catalog/provider-models.ts +58 -44
  112. package/src/codex/catalog/retained-sync.ts +7 -5
  113. package/src/codex/catalog/routed-gather.ts +15 -1
  114. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  115. package/src/codex/catalog-auto-refresh.ts +19 -0
  116. package/src/codex/catalog.ts +1 -1
  117. package/src/codex/codex-home-owner.ts +3 -0
  118. package/src/codex/config-write-lock.ts +333 -0
  119. package/src/codex/control-plane-models.ts +6 -0
  120. package/src/codex/convergence.ts +2 -1
  121. package/src/codex/features.ts +257 -200
  122. package/src/codex/inject/config-write-section.ts +34 -0
  123. package/src/codex/inject/multi-agent-v2.ts +14 -6
  124. package/src/codex/inject/remove.ts +36 -11
  125. package/src/codex/inject/restore.ts +85 -20
  126. package/src/codex/inject-coordination.ts +15 -6
  127. package/src/codex/inject.ts +62 -20
  128. package/src/codex/journal.ts +62 -17
  129. package/src/codex/main-account-cache.ts +43 -0
  130. package/src/codex/paths.ts +2 -0
  131. package/src/codex/prepared-home.ts +18 -0
  132. package/src/codex/prompt-journal.ts +25 -12
  133. package/src/codex/prompt-layers.ts +49 -26
  134. package/src/codex/prompt-lock-claim.ts +134 -0
  135. package/src/codex/prompt-lock-evidence.ts +26 -0
  136. package/src/codex/prompt-lock-io.ts +16 -0
  137. package/src/codex/prompt-lock-owner.ts +130 -0
  138. package/src/codex/prompt-lock.ts +84 -53
  139. package/src/codex/routing/failure-window.ts +146 -0
  140. package/src/codex/routing/selection.ts +4 -1
  141. package/src/codex/routing.ts +13 -17
  142. package/src/codex/runtime.ts +106 -18
  143. package/src/codex/shim.ts +45 -19
  144. package/src/combos/jev-decision-contract.ts +25 -2
  145. package/src/combos/jev-service-exchange.ts +281 -0
  146. package/src/combos/jev.ts +34 -219
  147. package/src/combos/types.ts +1 -1
  148. package/src/config/atomic-write.ts +1 -1
  149. package/src/config/codex-ws-reuse-setting.ts +10 -0
  150. package/src/config/diagnostics.ts +27 -16
  151. package/src/config/live-reconcile.ts +17 -0
  152. package/src/config/load-degrade.ts +42 -0
  153. package/src/config/rebase-provenance.ts +11 -0
  154. package/src/config/schema/anthropic-account-pool.ts +143 -0
  155. package/src/config/schema/config-schema.ts +11 -1
  156. package/src/config/schema/leaf-validators.ts +4 -0
  157. package/src/config.ts +2 -0
  158. package/src/generated/compatibility-version.json +566 -298
  159. package/src/generated/model-metadata.ts +7 -6
  160. package/src/grok/inject.ts +50 -4
  161. package/src/grok/sync.ts +2 -1
  162. package/src/images/loop.ts +30 -11
  163. package/src/integrations/aside-profiles.ts +10 -3
  164. package/src/integrations/catalog-refresh.ts +19 -3
  165. package/src/integrations/mutation-plan.ts +7 -0
  166. package/src/integrations/registry.ts +8 -0
  167. package/src/integrations/state.ts +9 -2
  168. package/src/integrations/writer.ts +17 -5
  169. package/src/lib/bun-path-runtime.d.mts +32 -0
  170. package/src/lib/bun-path-runtime.mjs +95 -0
  171. package/src/lib/bun-runtime-preflight.ts +140 -0
  172. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  173. package/src/lib/desktop-cli-handoff.mjs +92 -0
  174. package/src/lib/desktop-cli-record.d.mts +29 -0
  175. package/src/lib/desktop-cli-record.mjs +102 -0
  176. package/src/lib/lab-live-route-production.ts +9 -1
  177. package/src/lib/request-execution-budget.ts +244 -58
  178. package/src/lib/spend-pool-alias-validation.ts +79 -0
  179. package/src/lib/spend-pool-continuity.ts +34 -0
  180. package/src/lib/spend-reservation-ledger.ts +412 -50
  181. package/src/lib/state-store-registrations.ts +20 -3
  182. package/src/lib/synced-state-location.ts +46 -6
  183. package/src/lib/test-home-guard.ts +87 -1
  184. package/src/lib/translator-budget.ts +17 -4
  185. package/src/lib/upstream-retry.ts +17 -5
  186. package/src/lib/windows-atomic-replace.ts +38 -16
  187. package/src/lib/windows-elevation.ts +6 -0
  188. package/src/lib/windows-secret-acl.ts +92 -25
  189. package/src/lib/winsw.ts +2 -3
  190. package/src/lib/workflow-budget.ts +53 -11
  191. package/src/messaging/budget.ts +45 -0
  192. package/src/messaging/discovery.ts +57 -0
  193. package/src/messaging/envelope.ts +49 -0
  194. package/src/messaging/input.ts +37 -0
  195. package/src/messaging/rpc.ts +196 -0
  196. package/src/messaging/send.ts +58 -0
  197. package/src/messaging/socket.ts +72 -0
  198. package/src/messaging/types.ts +29 -0
  199. package/src/oauth/anthropic-account-refusal.ts +90 -18
  200. package/src/oauth/anthropic-account-threshold.ts +8 -2
  201. package/src/oauth/anthropic-continuity.ts +15 -1
  202. package/src/oauth/anthropic-model-quota.ts +120 -48
  203. package/src/oauth/anthropic-model-routes.ts +11 -3
  204. package/src/oauth/anthropic-oauth-definitions.ts +98 -0
  205. package/src/oauth/anthropic-pool-config.ts +30 -0
  206. package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
  207. package/src/oauth/anthropic-routing.ts +1202 -866
  208. package/src/oauth/anthropic-send-ownership.ts +41 -0
  209. package/src/oauth/anthropic.ts +4 -1
  210. package/src/oauth/chatgpt.ts +113 -41
  211. package/src/oauth/generic-account-failover.ts +3 -1
  212. package/src/oauth/health.ts +8 -6
  213. package/src/oauth/index.ts +96 -175
  214. package/src/oauth/kiro.ts +22 -9
  215. package/src/oauth/login-cli.ts +2 -0
  216. package/src/oauth/model-discovery-auth.ts +43 -0
  217. package/src/oauth/pool-kernel.ts +11 -5
  218. package/src/oauth/pool-settings-capability.ts +5 -3
  219. package/src/oauth/store-anthropic-instance.ts +87 -0
  220. package/src/oauth/store.ts +68 -10
  221. package/src/oauth/token-guardian.ts +2 -0
  222. package/src/oauth/types.ts +2 -0
  223. package/src/protocols/plan-snapshot.ts +5 -0
  224. package/src/protocols/settings.ts +20 -5
  225. package/src/providers/anthropic-instance-id.ts +47 -0
  226. package/src/providers/anthropic-instance.ts +48 -0
  227. package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
  228. package/src/providers/azure-model-metadata.ts +115 -0
  229. package/src/providers/derive.ts +23 -0
  230. package/src/providers/kiro-models.ts +3 -0
  231. package/src/providers/label.ts +1 -1
  232. package/src/providers/model-rename-fields.ts +2 -0
  233. package/src/providers/quota/account-cache.ts +37 -11
  234. package/src/providers/quota/anthropic-account-quota.ts +93 -0
  235. package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
  236. package/src/providers/quota/report-cache.ts +24 -1
  237. package/src/providers/quota/vendor-probes-key.ts +127 -17
  238. package/src/providers/quota/vendor-probes-oauth.ts +46 -14
  239. package/src/providers/quota.ts +23 -33
  240. package/src/providers/registry/entries-core.ts +35 -30
  241. package/src/providers/registry/model-ids.ts +1 -0
  242. package/src/providers/registry/model-seeds.ts +7 -4
  243. package/src/providers/registry/types.ts +3 -0
  244. package/src/providers/registry.ts +3 -3
  245. package/src/responses/reasoning-envelope.ts +29 -1
  246. package/src/responses/reasoning-replay-cache.ts +17 -1
  247. package/src/responses/spill-store.ts +20 -3
  248. package/src/responses/state/metrics.ts +95 -0
  249. package/src/responses/state/snapshot-codec.ts +6 -0
  250. package/src/responses/state/spill-failure.ts +12 -0
  251. package/src/responses/state/spill-queue.ts +13 -5
  252. package/src/responses/state/unforced-store-false.ts +55 -0
  253. package/src/responses/state.ts +68 -76
  254. package/src/responses/task-input.ts +17 -0
  255. package/src/router.ts +28 -4
  256. package/src/routing/compatibility/assemble.ts +1 -1
  257. package/src/routing/quota.ts +7 -3
  258. package/src/server/auth-cors.ts +5 -0
  259. package/src/server/background-lifecycle.ts +27 -0
  260. package/src/server/chat-completions.ts +7 -1
  261. package/src/server/chat-native.ts +64 -28
  262. package/src/server/claude-messages.ts +59 -8
  263. package/src/server/gui-static.ts +38 -4
  264. package/src/server/inbound-body-admission.ts +1 -0
  265. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  266. package/src/server/index/serve-options.ts +10 -2
  267. package/src/server/index/spend-ledger-lifecycle.ts +12 -5
  268. package/src/server/index/startup-warnings.ts +19 -0
  269. package/src/server/index.ts +5 -2
  270. package/src/server/inference/context.ts +1 -1
  271. package/src/server/lifecycle.ts +1 -1
  272. package/src/server/management/agent-settings-routes.ts +123 -38
  273. package/src/server/management/anthropic-account-threshold.ts +12 -9
  274. package/src/server/management/anthropic-pool-settings.ts +72 -0
  275. package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
  276. package/src/server/management/codex-agent-role-routes.ts +4 -2
  277. package/src/server/management/codex-prompt-routes.ts +1 -0
  278. package/src/server/management/codex-role-auto-assign.ts +13 -2
  279. package/src/server/management/config-routes.ts +85 -14
  280. package/src/server/management/context.ts +2 -2
  281. package/src/server/management/decision-routes.ts +2 -1
  282. package/src/server/management/integration-routes.ts +25 -2
  283. package/src/server/management/oauth-account-routes.ts +79 -98
  284. package/src/server/management/provider-instance-ownership.ts +44 -0
  285. package/src/server/management/provider-routes.ts +21 -28
  286. package/src/server/management/route-registry.ts +3 -1
  287. package/src/server/management/routing-profile-routes.ts +4 -4
  288. package/src/server/management/shared.ts +3 -3
  289. package/src/server/management/system-restart.ts +44 -14
  290. package/src/server/management/system-routes.ts +24 -2
  291. package/src/server/management/vision-sidecar-options.ts +9 -6
  292. package/src/server/management/web-search-sidecar-options.ts +51 -2
  293. package/src/server/management-auth.ts +6 -0
  294. package/src/server/messages-native-eligibility.ts +3 -2
  295. package/src/server/messages-native-oauth.ts +91 -45
  296. package/src/server/messages-native-selector.ts +39 -0
  297. package/src/server/messages-native.ts +133 -38
  298. package/src/server/messages-response-headers.ts +14 -0
  299. package/src/server/request-log-conversation.ts +48 -5
  300. package/src/server/request-log.ts +36 -2
  301. package/src/server/responses/adapter-continuation.ts +31 -19
  302. package/src/server/responses/adapter-dispatch.ts +75 -24
  303. package/src/server/responses/antigravity-validation-refusal.ts +20 -6
  304. package/src/server/responses/codex-ws-exchange.ts +17 -2
  305. package/src/server/responses/codex-ws-pool.ts +100 -13
  306. package/src/server/responses/codex-ws-wire.ts +9 -0
  307. package/src/server/responses/combo-requested-effort.ts +3 -1
  308. package/src/server/responses/compact.ts +25 -8
  309. package/src/server/responses/compaction-recovery.ts +1 -1
  310. package/src/server/responses/core-codex-account.ts +22 -1
  311. package/src/server/responses/core-combo-native.ts +24 -4
  312. package/src/server/responses/core-combo.ts +38 -25
  313. package/src/server/responses/core-lifetime.ts +3 -36
  314. package/src/server/responses/core-normalize.ts +1 -0
  315. package/src/server/responses/core-options.ts +8 -1
  316. package/src/server/responses/core-replay.ts +78 -0
  317. package/src/server/responses/core.ts +3 -3
  318. package/src/server/responses/fetch-helpers.ts +26 -3
  319. package/src/server/responses/passthrough-delivery.ts +30 -4
  320. package/src/server/responses/passthrough-dispatch.ts +39 -9
  321. package/src/server/responses/request-prepare.ts +41 -5
  322. package/src/server/responses/request-send-budget.ts +150 -50
  323. package/src/server/responses/request-spend.ts +325 -33
  324. package/src/server/responses/request-transport.ts +132 -52
  325. package/src/server/responses/response-effects.ts +1 -0
  326. package/src/server/responses/run-turn-execution.ts +13 -3
  327. package/src/server/responses/sidecar-execution.ts +113 -22
  328. package/src/server/responses/sidecar-send-budget.ts +58 -0
  329. package/src/server/responses/ws-upstream.ts +19 -6
  330. package/src/server/responses-undeclared-tool-guard.ts +5 -1
  331. package/src/server/startup-health-cache.ts +2 -1
  332. package/src/server/workflow-refusal.ts +33 -0
  333. package/src/service/cli.ts +20 -4
  334. package/src/service/desktop-command-guard.ts +32 -0
  335. package/src/service/desktop-startup.ts +66 -40
  336. package/src/service/desktop-supervision.d.mts +26 -0
  337. package/src/service/desktop-supervision.mjs +140 -0
  338. package/src/service/managing-cli.ts +1 -0
  339. package/src/service/orchestration.ts +46 -20
  340. package/src/service/ownership-mutation-lease.d.mts +1 -1
  341. package/src/service/ownership-mutation-lease.mjs +5 -7
  342. package/src/service/repair.ts +25 -9
  343. package/src/service/state.ts +4 -3
  344. package/src/service/systemd.ts +17 -8
  345. package/src/service/windows-ops.ts +61 -22
  346. package/src/sidecar/anthropic-binding.ts +79 -0
  347. package/src/sidecar/auth.ts +76 -2
  348. package/src/storage/policy-job.ts +5 -1
  349. package/src/storage/policy-worker.ts +3 -1
  350. package/src/storage/policy.ts +9 -0
  351. package/src/types/anthropic-account-pool.ts +33 -0
  352. package/src/types/config.ts +33 -28
  353. package/src/types/provider.ts +5 -0
  354. package/src/types/request.ts +5 -0
  355. package/src/types/tools.ts +7 -0
  356. package/src/types/wire.ts +1 -0
  357. package/src/types.ts +4 -0
  358. package/src/update/index.ts +57 -9
  359. package/src/update/restart-ownership.ts +10 -3
  360. package/src/update/runtime-ownership.d.mts +6 -0
  361. package/src/update/runtime-ownership.mjs +23 -7
  362. package/src/usage/cost.ts +3 -2
  363. package/src/usage/expected-prices.ts +66 -14
  364. package/src/vision/anthropic-describe.ts +16 -3
  365. package/src/vision/index.ts +3 -0
  366. package/src/vision/plan.ts +19 -6
  367. package/src/web-search/alpha-search.ts +7 -1
  368. package/src/web-search/anthropic-executor.ts +16 -4
  369. package/src/web-search/index.ts +7 -3
  370. package/src/web-search/loop.ts +32 -7
  371. package/src/web-search/passthrough-bridge.ts +4 -1
  372. package/src/web-search/sidecar-providers.ts +4 -3
  373. package/gui/dist/assets/App-MbDVfUeC.js +0 -54
  374. package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
  375. package/gui/dist/assets/index-UJt6npXk.js +0 -94
  376. package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
@@ -48,6 +48,7 @@ import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchField
48
48
  import { observeOutbound } from "../../usage/cache-diagnostic";
49
49
  import { normalizeForwardedClientHeaderName } from "../../lib/provider-client-headers";
50
50
  import { normalizeMuseToolChoice } from "./muse-tool-choice";
51
+ import { renderCompactionSearchHistoryItems } from "./compaction-search-history";
51
52
 
52
53
  /**
53
54
  * Identifies DeepSeek's strict Responses replay contract: tool-bearing continuations need
@@ -133,17 +134,21 @@ function stripInputImagesDeep(value: unknown): unknown {
133
134
  * be applied here. Images go too: a summary needs no pixels, and a text-only
134
135
  * gateway would reject them.
135
136
  */
136
- function buildRoutedCompactionBody(body: unknown): unknown {
137
+ function buildRoutedCompactionBody(body: unknown, responsesLite: boolean): unknown {
137
138
  if (!isPlainObject(body)) return body;
138
139
  // `text` goes with the tool fields: the summary must be prose, not schema-constrained JSON.
139
140
  const { tools: _tools, tool_choice: _toolChoice, parallel_tool_calls: _parallel, text: _text, ...rest } = body;
140
141
  const input = Array.isArray(body.input) ? body.input : [];
141
- const kept = input.filter(item => !isPlainObject(item)
142
+ // Bridge restoration has already recovered any available real call/result pairs. Remaining
143
+ // hosted cells require tools, so they become bounded assistant reference notes.
144
+ const kept = renderCompactionSearchHistoryItems(input.filter(item => !isPlainObject(item)
142
145
  // `additional_tools` is how Codex Desktop's responses-lite shape carries tools;
143
146
  // leaving it in would break the no-tools invariant even with `tools` removed.
144
- || (item.type !== "compaction_trigger" && item.type !== "additional_tools"));
147
+ || (item.type !== "compaction_trigger" && item.type !== "additional_tools")));
145
148
  return {
146
149
  ...rest,
150
+ // Lite validates this flag even when the summarizer has no callable tools.
151
+ ...(responsesLite ? { parallel_tool_calls: false } : {}),
147
152
  input: [
148
153
  ...(stripInputImagesDeep(kept) as unknown[]),
149
154
  { type: "message", role: "user", content: [{ type: "input_text", text: COMPACT_PROMPT }] },
@@ -466,7 +471,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
466
471
  // sanitizers below can still run after it.
467
472
  outBody = normalizeResponsesCodeMode(outBody, parsed, provider);
468
473
  if (parsed._compactionRequest === true && (!isCanonicalOpenAiForwardProvider(provider) || parsed._portableCompaction === true)) {
469
- outBody = buildRoutedCompactionBody(outBody);
474
+ outBody = buildRoutedCompactionBody(outBody, new Headers(headers).get(CODEX_RESPONSES_LITE_HEADER) === "true");
470
475
  }
471
476
  // Run after routed compaction so nested input_image parts are replaced before a malformed
472
477
  // tool output is flattened to text and can no longer be inspected structurally.
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../empty-tool-output-annotation";
3
3
  import { isPlainObject } from "./internal";
4
4
  import { peekBridgeSearchReplay } from "../../responses/bridge-search-replay-cache";
5
+ import { externalTaskInputResponsesContent } from "../../responses/task-input";
5
6
 
6
7
  const MAX_RESPONSES_CALL_ID_LENGTH = 64;
7
8
 
@@ -169,6 +170,16 @@ export function repairUnidentifiedToolOutputItems(body: unknown): unknown {
169
170
  || (typeof item.call_id === "string" && item.call_id.length > 0)) {
170
171
  return item;
171
172
  }
173
+ // #6764: an external task envelope is user input, not an orphaned result. Recognize it
174
+ // before the generic repair so the summarizer and the destination see a plain user turn.
175
+ // Any nonempty string call_id stays a tool result, as on the parsed path: the request schema
176
+ // accepts a whitespace call_id as a function_call_output and strips the envelope fields
177
+ // (src/responses/schema.ts), so both paths must agree that it is not task input.
178
+ const taskInput = externalTaskInputResponsesContent(item);
179
+ if (taskInput) {
180
+ changed = true;
181
+ return { type: "message", role: "user", content: taskInput };
182
+ }
172
183
  if (!isRepairableToolOutput(item.output)) return item;
173
184
  changed = true;
174
185
  return {
@@ -380,6 +391,15 @@ export function repairOrphanedInputItems(
380
391
  const isCustomOutput = item.type === "custom_tool_call_output";
381
392
  if (isFnOutput || isCustomOutput) {
382
393
  flushPendingSyntheticOutputs();
394
+ // Same rule as repairUnidentifiedToolOutputItems: a nonempty string call_id is a tool result
395
+ // on the parsed path (the schema strips the envelope fields), so it is never task input here.
396
+ const hasStringCallId = typeof item.call_id === "string" && item.call_id.length > 0;
397
+ const taskInput = isFnOutput && !hasStringCallId ? externalTaskInputResponsesContent(item) : undefined;
398
+ if (taskInput) {
399
+ changed = true;
400
+ repaired.push({ type: "message", role: "user", content: taskInput });
401
+ continue;
402
+ }
383
403
  const callId = typeof item.call_id === "string" ? item.call_id : "";
384
404
  const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
385
405
  const usableOutput = isRepairableToolOutput(item.output);
@@ -36,11 +36,14 @@ export function createAdapterPhysicalSend(ctx: AdapterFetchContext = {}, fallbac
36
36
  const physicalExecutor = (async (input, init) => {
37
37
  if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
38
38
  if (init?.signal?.aborted) throw abortError(init.signal);
39
- if (dispatched || (permit && !permit.use())) throw new SendBudgetExhaustedError(options.url);
40
- dispatched = true;
41
- ordinal += 1;
42
- ctx.onPhysicalSend?.({ ordinal, ...(options.recovery ? { recovery: options.recovery } : {}) });
43
- return (executor.unpacedFetch ?? executor)(input, init);
39
+ const run = async (): Promise<Response> => {
40
+ if (dispatched || (permit && !permit.use())) throw new SendBudgetExhaustedError(options.url);
41
+ dispatched = true;
42
+ ordinal += 1;
43
+ ctx.onPhysicalSend?.({ ordinal, ...(options.recovery ? { recovery: options.recovery } : {}) });
44
+ return (executor.unpacedFetch ?? executor)(input, init);
45
+ };
46
+ return permit?.execute ? permit.execute(run) : run();
44
47
  }) as typeof globalThis.fetch;
45
48
  let slot: ProviderRequestSlot | undefined;
46
49
  try {
@@ -1,20 +1,25 @@
1
1
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
2
+ import { fileURLToPath } from "node:url";
2
3
  import type { AdapterRequest, ProviderAdapter } from "../base";
3
4
  import { mapReasoningEffort } from "../../reasoning-effort";
4
- import { buildSystemPrompt } from "../coding-agent/protocol";
5
- import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps } from "../coding-agent/turn";
5
+ import { buildSystemPrompt, MAX_TOOL_BLOCK_STARTS } from "../coding-agent/protocol";
6
+ import { buildCodingAgentToolCatalog, CodingAgentToolCatalogError } from "../coding-agent/tool-catalog";
7
+ import { baseScopedEnv, runCodingAgentTurn, type CodingAgentDeps, type CodingAgentToolBridgeInput } from "../coding-agent/turn";
6
8
  import { QODER_PROFILES, type QoderProfile } from "./profiles";
7
9
  import { QoderScaffoldFilter, QODER_SCAFFOLD_ERROR_CODE, qoderScaffoldErrorMessage } from "./scaffold-guard";
8
10
 
9
11
  export type QoderAdapterDeps = CodingAgentDeps;
10
12
 
13
+ const QODER_MCP_SERVER_NAME = "opencodex";
14
+ const QODER_MCP_SERVER_PATH = fileURLToPath(new URL("../coding-agent/mcp-server.ts", import.meta.url));
15
+
11
16
  export function buildQoderChildEnv(profile: QoderProfile, apiKey: string, systemPrompt?: string): Record<string, string> {
12
17
  const promptEnv = profile.region === "cn" ? "QODERCN_APPEND_SYSTEM_PROMPT" : "QODER_APPEND_SYSTEM_PROMPT";
13
18
  return { ...baseScopedEnv(), NO_COLOR: "1", [profile.tokenEnv]: apiKey,
14
19
  ...(systemPrompt ? { [promptEnv]: systemPrompt } : {}) };
15
20
  }
16
21
 
17
- /** Single-shot, tools-disabled Qoder CLI invocation; Codex remains the tool owner. */
22
+ /** Disable Qoder's built-in tools; only a request-scoped MCP catalog can be exposed. */
18
23
  export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderConfig): string[] {
19
24
  const args = [
20
25
  "-p",
@@ -35,8 +40,8 @@ export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderCo
35
40
  /**
36
41
  * Wrap the turn's outbound channel with the scaffolding guard (#4190).
37
42
  *
38
- * The vendor CLI can put its own agent layer into the text channel despite being launched
39
- * with tools and MCP disabled, and the shared stream-json parser forwards a text delta
43
+ * The vendor CLI can put its own agent layer into the text channel despite built-in tools
44
+ * being disabled, and the shared stream-json parser forwards a text delta
40
45
  * without inspecting it. This is the last point that is still qoder-specific, so the guard
41
46
  * sits here rather than in the parser every coding-agent CLI shares.
42
47
  *
@@ -123,7 +128,36 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
123
128
  });
124
129
  return;
125
130
  }
126
- // Qoder documents QODER_APPEND_SYSTEM_PROMPT for scoped child configuration.
131
+ let catalog: ReturnType<typeof buildCodingAgentToolCatalog>;
132
+ try {
133
+ catalog = buildCodingAgentToolCatalog(parsed, QODER_MCP_SERVER_NAME);
134
+ } catch (error) {
135
+ if (!(error instanceof CodingAgentToolCatalogError)) throw error;
136
+ emit({
137
+ type: "error",
138
+ message: "Invalid Qoder tool catalog.",
139
+ status: 400,
140
+ errorType: "invalid_request_error",
141
+ code: "tool_catalog_invalid",
142
+ retryable: false,
143
+ });
144
+ return;
145
+ }
146
+ const toolBridge: CodingAgentToolBridgeInput | undefined = catalog.tools.length > 0 || catalog.requireToolCall
147
+ ? {
148
+ serverName: QODER_MCP_SERVER_NAME,
149
+ serverModulePath: QODER_MCP_SERVER_PATH,
150
+ tools: catalog.tools,
151
+ emittedNameMap: catalog.emittedNameMap,
152
+ maxTurnToolCalls: MAX_TOOL_BLOCK_STARTS,
153
+ projectHistoryToolNames: true,
154
+ completeAssistantToolUse: true,
155
+ toolTurnCompletionSignal: "assistant_tool_use_stop",
156
+ allowedToolsFlag: "--allowed-tools",
157
+ standaloneEntrypoint: "__qoder-mcp",
158
+ requireToolCall: catalog.requireToolCall,
159
+ }
160
+ : undefined;
127
161
  // Keep the folded prompt out of argv and never inherit an ambient vendor prompt.
128
162
  const system = buildSystemPrompt(parsed);
129
163
  await runCodingAgentTurn({
@@ -135,6 +169,7 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt
135
169
  buildArgs: (_profile, req, prov) => buildQoderArgs(req, prov),
136
170
  buildEnv: (profile, apiKey) => buildQoderChildEnv(profile as QoderProfile, apiKey, system),
137
171
  deps,
172
+ ...(toolBridge ? { toolBridge } : {}),
138
173
  });
139
174
  },
140
175
  };
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Vendor-scaffolding guard for the qoder route (#4190).
3
3
  *
4
- * The qoder route is contractually a text and reasoning surface: the CLI is spawned with
5
- * `--tools "" --strict-mcp-config --setting-sources ""`, and Codex keeps tool ownership.
6
- * The vendor CLI does not always honour that. It has been observed emitting its own agent
4
+ * Even with built-in tools disabled and strict MCP isolation, Codex keeps tool execution
5
+ * ownership. The vendor CLI does not always honour that. It has been observed emitting its own agent
7
6
  * layer into the assistant text channel — an MCP lazy-loading `<system-reminder>` block
8
7
  * listing the local machine's configured MCP servers, and framework tool-call markup with a
9
8
  * mismatched closer. Both reached the client verbatim, because the shared stream-json parser
@@ -4,6 +4,7 @@ import { createAnthropicAdapter } from "./anthropic";
4
4
  import { createGoogleAdapter } from "./google";
5
5
  import { createOpenAIChatAdapter } from "./openai-chat";
6
6
  import { createResponsesPassthroughAdapter } from "./openai-responses";
7
+ import { createAdapterPhysicalSend } from "./physical-send";
7
8
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
8
9
  import { createTranslatorBudget, releaseTranslatedEvent, retainTranslatedEvent, type TranslatorBudget } from "../lib/translator-budget";
9
10
  import { releaseCompactionCiphertextLease } from "../responses/compaction";
@@ -13,6 +14,7 @@ import {
13
14
  resolveZedModels,
14
15
  scrubZedCredentials,
15
16
  zedLlmFetch,
17
+ ZED_CLOUD_BASE_URL,
16
18
  ZED_HEADERS,
17
19
  type ZedCredentials,
18
20
  } from "../providers/zed";
@@ -340,8 +342,21 @@ export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
340
342
  async fetchResponse(request, ctx) {
341
343
  if (!credentials) throw new Error("Zed request credentials were not initialized");
342
344
  const fetchFn = ctx?.executor ?? globalThis.fetch;
345
+ const send = createAdapterPhysicalSend(ctx);
346
+ let completionAttempts = 0;
343
347
  return zedLlmFetch(credentials, "/completions", {
344
- fetchFn,
348
+ fetchFn: (async (input, init) => {
349
+ const url = input instanceof Request ? input.url : new URL(String(input)).href;
350
+ // Token exchange/account lookup are support traffic; both completion attempts
351
+ // share admission and physical ordinals, including the provider's 401 replay.
352
+ if (url === `${ZED_CLOUD_BASE_URL}/client/users/me`
353
+ || url === `${ZED_CLOUD_BASE_URL}/client/llm_tokens`) return fetchFn(input, init);
354
+ const recovery = completionAttempts++ > 0;
355
+ return send({ url, sendClass: recovery ? "auth-recovery" : "initial",
356
+ ...(recovery ? { recovery: "oauth-401" } : {}),
357
+ dispatch: executor => executor(input, init),
358
+ });
359
+ }) as typeof globalThis.fetch,
345
360
  signal: ctx?.abortSignal,
346
361
  baseUrl: provider.baseUrl,
347
362
  fetchInit: {
@@ -15,7 +15,8 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileS
15
15
  import { join } from "node:path";
16
16
  import type { OcxConfig } from "../types";
17
17
  import { renameAtomicFile } from "../lib/windows-atomic-replace";
18
- import { entryParts, SAFE_AGENT_MODEL_ID, withSubagentContextMarker } from "./subagent-model";
18
+ import { assertNotRealClaudeConfigUnderTest } from "../lib/test-home-guard";
19
+ import { accountsAt200k, entryParts, SAFE_AGENT_MODEL_ID, withSubagentContextMarker } from "./subagent-model";
19
20
  import { stripOneMillionMarker } from "./context-windows";
20
21
  import { claudeConfigDir } from "./gateway-cache";
21
22
  import { DEFAULT_SUBAGENT_MODELS } from "../config";
@@ -97,7 +98,7 @@ export function buildClaudeAgentDefs(
97
98
  // Generated defs mark [1m] on the authoritative window only — never the
98
99
  // main-session auto-context predicate (a 372K route marked [1m] would be
99
100
  // accounted at 1M with no compaction safety net in the subagent).
100
- const model = withSubagentContextMarker(alias, windows);
101
+ const model = withSubagentContextMarker(alias, windows, accountsAt200k(config));
101
102
  const bare = alias.toLowerCase();
102
103
  if (coveredModels.has(bare)) return;
103
104
  coveredModels.add(bare);
@@ -130,7 +131,7 @@ export function buildClaudeAgentDefs(
130
131
  // the next launch sync — documented limit. No resolvable default -> no self def.
131
132
  const selfModel = pickerDefaultModel(configDir) ?? (config.claudeCode?.model?.trim() || null);
132
133
  if (selfModel) {
133
- const marked = withSubagentContextMarker(selfModel, windows);
134
+ const marked = withSubagentContextMarker(selfModel, windows, accountsAt200k(config));
134
135
  defs.push({
135
136
  file: `${OWNED_PREFIX}self.md`,
136
137
  name: `${OWNED_PREFIX}self`,
@@ -203,6 +204,16 @@ function isOwnedFile(path: string): boolean {
203
204
  * atomic (tmp + rename). Best-effort — returns null on any failure.
204
205
  */
205
206
  export function syncClaudeAgentDefs(defs: readonly ClaudeAgentDef[], configDir = claudeConfigDir()): string[] | null {
207
+ // Outside the best-effort catch: under an armed test process a write or prune of the
208
+ // real Claude agents directory must throw, never degrade to "returned null" (#6775).
209
+ // Every path written is judged by where it resolves, including a pre-placed link named like
210
+ // a definition or its temporary file.
211
+ const agentsDir = join(configDir, "agents");
212
+ assertNotRealClaudeConfigUnderTest(
213
+ configDir,
214
+ agentsDir,
215
+ ...defs.flatMap(def => [join(agentsDir, def.file), `${join(agentsDir, def.file)}.tmp-${process.pid}`]),
216
+ );
206
217
  try {
207
218
  const dir = join(configDir, "agents");
208
219
  if (defs.length === 0) {
@@ -15,8 +15,9 @@ import { desktop3pAlias } from "./desktop-3p";
15
15
  import { nativeOpenAiContextWindow, type CatalogModel, type NativeContextLimitsInput } from "../codex/catalog";
16
16
  import { ANTHROPIC_MODEL_CONTEXT_WINDOWS } from "../providers/registry/model-seeds";
17
17
  import type { OcxClaudeCodeConfig } from "../types";
18
+ import { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR, ONE_MILLION } from "./long-context";
18
19
 
19
- const ONE_MILLION = 1_000_000;
20
+ export { AUTO_COMPACT_WINDOW_DEFAULT, AUTO_CONTEXT_FLOOR } from "./long-context";
20
21
 
21
22
  /**
22
23
  * The native id each Claude Code tier alias resolves to (2.1.282: `--model opus` sends
@@ -27,21 +28,6 @@ const ONE_MILLION = 1_000_000;
27
28
  */
28
29
  const CLAUDE_CODE_NATIVE_TIERS = { opus: "claude-opus-5-5", sonnet: "claude-sonnet-5", fable: "claude-fable-5-1" } as const;
29
30
 
30
- /**
31
- * Auto-context defaults (devlog 260712 020, user-approved).
32
- *
33
- * The compact window is the token count at which Claude Code starts compacting, and it is
34
- * also the floor `shouldMarkOneMillion` uses — a model may only carry the marker if it can
35
- * host this window. 350,000 was chosen when the widest native row advertised 372,000.
36
- *
37
- * It now matches the auto-compaction limit the Codex catalog ships for the same models
38
- * (`nativeAutoCompactLimit`: 829,800 against the 922,000 native window). Leaving the two
39
- * apart meant one model compacting at 350k under Claude Code and at 829,800 under Codex.
40
- * The value stays clear of the measured 922,000 ceiling by ~92k, so compaction still has
41
- * room to run before the upstream refuses.
42
- */
43
- export const AUTO_COMPACT_WINDOW_DEFAULT = 829_800;
44
- export const AUTO_CONTEXT_FLOOR = 200_000;
45
31
  /** Binary-verified accepted range for CLAUDE_CODE_AUTO_COMPACT_WINDOW (2.1.207: pSo=1e5, yDs=1e6). */
46
32
  export const AUTO_COMPACT_WINDOW_MIN = 100_000;
47
33
  export const AUTO_COMPACT_WINDOW_MAX = ONE_MILLION;
@@ -59,14 +45,27 @@ export interface AutoContextMode {
59
45
  enabled: boolean;
60
46
  /** Effective CLAUDE_CODE_AUTO_COMPACT_WINDOW value (tokens). */
61
47
  compactWindow: number;
48
+ /** claudeCode.contextAccounting "200k": nothing is marked [1m] automatically, whatever its window. */
49
+ accounting200k?: true;
62
50
  }
63
51
 
64
52
  export const AUTO_CONTEXT_OFF: AutoContextMode = { enabled: false, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT };
65
53
 
54
+ /**
55
+ * Marking mode for surfaces whose runner may not inherit the compaction env (Desktop pickers,
56
+ * discovery, generated subagents). shouldMarkOneMillion under it equals isLongContextWindow
57
+ * (long-context.ts); kept as a mode so the existing marker helpers serve every surface.
58
+ */
59
+ export const UNPAIRED_AUTO_CONTEXT: AutoContextMode = { enabled: true, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT };
60
+
61
+ /** The "200k" opt-in: no widening, no compact-window injection, no automatic marker. */
62
+ export const ACCOUNTING_200K: AutoContextMode = { enabled: false, compactWindow: AUTO_COMPACT_WINDOW_DEFAULT, accounting200k: true };
63
+
66
64
  interface AutoContextConfigSlice {
67
65
  autoContext?: boolean;
68
66
  autoCompactWindow?: number;
69
67
  maxContextTokens?: number;
68
+ contextAccounting?: string;
70
69
  }
71
70
 
72
71
  function inAutoCompactRange(value: number): boolean {
@@ -90,6 +89,8 @@ function inAutoCompactRange(value: number): boolean {
90
89
  * to AUTO_COMPACT_WINDOW_DEFAULT (the management API rejects them; this guards hand-edits).
91
90
  */
92
91
  export function resolveAutoContext(claudeCode: AutoContextConfigSlice | undefined, envOverride?: string): AutoContextMode {
92
+ // The 200k opt-in wins over every other lever, a user-exported compact window included.
93
+ if (claudeCode?.contextAccounting === "200k") return ACCOUNTING_200K;
93
94
  if (claudeCode?.autoContext === false) return AUTO_CONTEXT_OFF;
94
95
  const maxCtx = claudeCode?.maxContextTokens;
95
96
  if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) return AUTO_CONTEXT_OFF;
@@ -132,6 +133,7 @@ export function claudeToolSearchEnv(value: boolean | string | undefined): string
132
133
  */
133
134
  export function shouldMarkOneMillion(window: number | undefined, auto: AutoContextMode): boolean {
134
135
  if (typeof window !== "number" || window <= 0) return false;
136
+ if (auto.accounting200k) return false;
135
137
  if (window >= ONE_MILLION) return true;
136
138
  return auto.enabled && window > AUTO_CONTEXT_FLOOR && window >= auto.compactWindow;
137
139
  }
@@ -248,7 +250,7 @@ export interface ClaudeTierModels {
248
250
  * value injected into BOTH haiku variables.
249
251
  */
250
252
  export function effectiveModelEnv(
251
- claudeCode: { model?: string; smallFastModel?: string; tierModels?: ClaudeTierModels; autoContext?: boolean; autoCompactWindow?: number; maxContextTokens?: number } | undefined,
253
+ claudeCode: { model?: string; smallFastModel?: string; tierModels?: ClaudeTierModels; autoContext?: boolean; autoCompactWindow?: number; maxContextTokens?: number; contextAccounting?: string } | undefined,
252
254
  windows: Record<string, number>,
253
255
  autoOverride?: AutoContextMode,
254
256
  ): Record<string, string> {
@@ -10,9 +10,10 @@ import {
10
10
  } from "./desktop-3p-library";
11
11
  export { resolveDesktop3pConfigLibraryPath, type Desktop3pConfigLibraryOptions } from "./desktop-3p-library";
12
12
  import { createHash, randomUUID } from "node:crypto";
13
- import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
13
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, lstatSync, unlinkSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { atomicWriteFile, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
16
+ import { atomicWriteFileNoFollow } from "../config/atomic-write";
16
17
  import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
17
18
  import type { OcxClaudeDesktopProfile } from "../types";
18
19
  import {
@@ -24,6 +25,7 @@ import {
24
25
  import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../codex/catalog";
25
26
  import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
26
27
  import { assertDesktop3pModelsValid } from "./desktop-3p-guard";
28
+ import { claudeSurfaceSupportsOneMillion, ONE_MILLION } from "./long-context";
27
29
 
28
30
  export interface Desktop3pModelEntry {
29
31
  name: string;
@@ -55,12 +57,12 @@ export interface Desktop3pRoutedModel {
55
57
  }
56
58
 
57
59
  /**
58
- * 1M-context eligibility, shared with the Desktop DTO so the dashboard's 1M chip can
59
- * never disagree with what the writer emits. The DTO imports this from here — keeping
60
- * the constant in this module avoids a cycle, since shared.ts already reads
61
- * claude/desktop-profile.
60
+ * The genuine-1M threshold real Anthropic rows still need. Every other row follows the
61
+ * long-window rule; the writer, the profile renderer and the dashboard DTO all decide through
62
+ * `claudeSurfaceSupportsOneMillion` (long-context.ts), so the 1M chip cannot disagree with the
63
+ * written config.
62
64
  */
63
- export const DESKTOP_SUPPORTS_1M_THRESHOLD = 1_000_000;
65
+ export const DESKTOP_SUPPORTS_1M_THRESHOLD = ONE_MILLION;
64
66
 
65
67
  /** CLI arg parsing for `ocx claude desktop` mode flags (mutually exclusive). */
66
68
  export function parseDesktop3pModeArgs(flags: string[]): { mode: Desktop3pConfigMode } | { error: string } {
@@ -251,7 +253,7 @@ function collectDesktop3pModels(
251
253
  for (const { provider, id, contextWindow } of candidates) {
252
254
  const route = `${provider}/${id}`;
253
255
  const alias = desktop3pAlias(provider, id);
254
- const supports1m = typeof contextWindow === "number" && contextWindow >= DESKTOP_SUPPORTS_1M_THRESHOLD
256
+ const supports1m = claudeSurfaceSupportsOneMillion(provider, id, contextWindow)
255
257
  ? { supports1m: true as const }
256
258
  : {};
257
259
  if (alias === id) {
@@ -317,16 +319,30 @@ export function buildDesktop3pRegistry(
317
319
  }
318
320
 
319
321
  /** Generate Claude Desktop 3P model entries from the proxy's available models. */
322
+ export interface Desktop3pModelOptions {
323
+ /**
324
+ * false under claudeCode.contextAccounting "200k": a long-window model stays selectable at 1M
325
+ * (`supports1m`) but no longer defaults to it (`prefer1m` dropped).
326
+ */
327
+ preferOneMillion?: boolean;
328
+ }
329
+
320
330
  export function generateDesktop3pModels(
321
331
  nativeSlugs: string[],
322
332
  routedModels: Array<Desktop3pRoutedModel>,
323
333
  profile?: OcxClaudeDesktopProfile,
324
334
  nativeContextCap?: NativeContextLimitsInput,
335
+ options: Desktop3pModelOptions = {},
325
336
  ): Desktop3pModelEntry[] {
326
337
  const { models, registry, realAnthropicIds } = collectDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
327
338
  desktop3pRegistry = registry;
328
339
  desktop3pRealAnthropicIds = realAnthropicIds;
329
- return models;
340
+ return options.preferOneMillion === false ? models.map(({ prefer1m: _prefer1m, ...model }) => model) : models;
341
+ }
342
+
343
+ /** The Desktop model options a config asks for (one place, so writer and export agree). */
344
+ export function desktop3pModelOptions(claudeCode: { contextAccounting?: string } | undefined): Desktop3pModelOptions {
345
+ return { preferOneMillion: claudeCode?.contextAccounting !== "200k" };
330
346
  }
331
347
 
332
348
  /** Resolve an alias using the most recently generated Desktop model registry. */
@@ -382,6 +398,7 @@ export function generateDesktop3pConfig(
382
398
  mode: Desktop3pConfigMode = "static",
383
399
  profile?: OcxClaudeDesktopProfile,
384
400
  nativeContextCap?: NativeContextLimitsInput,
401
+ options: Desktop3pModelOptions = {},
385
402
  ): object {
386
403
  const base = {
387
404
  inferenceProvider: "gateway",
@@ -398,7 +415,7 @@ export function generateDesktop3pConfig(
398
415
  ...base,
399
416
  modelDiscoveryEnabled: mode === "hybrid",
400
417
  inferenceModels: (() => {
401
- const models = generateDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap);
418
+ const models = generateDesktop3pModels(nativeSlugs, routedModels, profile, nativeContextCap, options);
402
419
  // Fail loud at the write boundary rather than ship a config Desktop rejects:
403
420
  // the output counterpart of the request-path guards.
404
421
  assertDesktop3pModelsValid(models);
@@ -662,6 +679,7 @@ export function writeDesktop3pConfig(
662
679
  profile?: OcxClaudeDesktopProfile,
663
680
  nativeContextCap?: NativeContextLimitsInput,
664
681
  lifecycleLockDeps?: ClientLifecycleLockDeps,
682
+ refreshOnly?: { appliedFingerprint: string; admit: () => boolean },
665
683
  ): { written: boolean; path: string; reason?: string; fingerprint?: string } {
666
684
  try {
667
685
  return withClientLifecycleSync(() => withConfigMutationLockSync(() => {
@@ -674,9 +692,29 @@ export function writeDesktop3pConfig(
674
692
  if (!claudeDesktopIntegrationEnabled(latest.config)) {
675
693
  return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desired_state_changed" };
676
694
  }
695
+ if (refreshOnly && !desktopRefreshPathsSafe(resolveDesktop3pConfigLibraryPath())) {
696
+ return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_refresh_only_skipped" };
697
+ }
677
698
  if (connection.kind === "connected" || inspectRemoteDesktopCleanup().kind !== "absent") {
678
699
  return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_remote_store_active" };
679
700
  }
701
+ let requiredId: string | undefined;
702
+ if (refreshOnly) {
703
+ const refused = { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_refresh_only_skipped" };
704
+ // Ownership and bytes must still match under L→C; discovery outside these locks is not authority.
705
+ if (!refreshOnly.admit() || latest.config.claudeCode?.desktopProfile?.appliedFingerprint !== refreshOnly.appliedFingerprint) return refused;
706
+ const inspected = inspectDesktop3pConfigLibrary({ appliedFingerprint: refreshOnly.appliedFingerprint });
707
+ if (inspected.kind !== "gateway_ours" || !inspected.appliedId || !inspected.selectedProfilePath) return refused;
708
+ if (!desktopRefreshPathsSafe(inspected.libraryPath, inspected.selectedProfilePath)) return refused;
709
+ const metadata = parseMetadata(join(inspected.libraryPath, "_meta.json"));
710
+ if (!isOwnedDesktopGatewayEntry(metadata.entries.find(entry => entry.id === inspected.appliedId))) return refused;
711
+ try {
712
+ const selected = JSON.parse(readFileSync(inspected.selectedProfilePath, "utf8")) as Record<string, unknown>;
713
+ mode = selected.modelDiscoveryEnabled !== true ? "static"
714
+ : Array.isArray(selected.inferenceModels) ? "hybrid" : "discovery";
715
+ } catch { return refused; }
716
+ requiredId = inspected.appliedId;
717
+ }
680
718
  // Claude Desktop runs on this machine, so it dials the unauthenticated loopback listener
681
719
  // when one is enabled — on such a hub that is the only credential-free local socket
682
720
  // (#4236) — and otherwise the bind address, which answers but demands data-plane
@@ -700,8 +738,9 @@ export function writeDesktop3pConfig(
700
738
  );
701
739
  }
702
740
  return writeDesktop3pConfigWithGenerator(() => (
703
- generateDesktop3pConfig(destination.origin, nativeSlugs, routedModels, gatewayKey, mode, profile, nativeContextCap)
704
- ));
741
+ generateDesktop3pConfig(destination.origin, nativeSlugs, routedModels, gatewayKey, mode, profile, nativeContextCap,
742
+ desktop3pModelOptions(latest.config.claudeCode))
743
+ ), requiredId && refreshOnly ? { requiredId, expectedFingerprint: refreshOnly.appliedFingerprint, admit: refreshOnly.admit } : undefined);
705
744
  }), lifecycleLockDeps);
706
745
  } catch { return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_lifecycle_busy_or_unsafe" }; }
707
746
  }
@@ -734,17 +773,38 @@ export function writeRemoteDesktop3pConfig(options: {
734
773
  } catch { return { written: false, path: "", reason: "desktop_lifecycle_busy_or_unsafe" }; }
735
774
  }
736
775
 
776
+ /** Refresh-only writes never follow links, including a dangling backup link. */
777
+ function desktopRefreshPathsSafe(libraryPath: string, configPath?: string): boolean {
778
+ try {
779
+ if (!lstatSync(libraryPath).isDirectory() || !lstatSync(join(libraryPath, "_meta.json")).isFile()) return false;
780
+ if (configPath) {
781
+ if (!lstatSync(configPath).isFile()) return false;
782
+ try { if (lstatSync(`${configPath}.bak`).isSymbolicLink()) return false; }
783
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") return false; }
784
+ }
785
+ return true;
786
+ } catch { return false; }
787
+ }
788
+
737
789
  function writeDesktop3pConfigWithGenerator(
738
790
  generate: () => object,
791
+ refreshOnly?: { requiredId: string; expectedFingerprint: string; admit: () => boolean },
739
792
  ): { written: boolean; path: string; reason?: string; fingerprint?: string } {
740
793
  const libraryPath = resolveDesktop3pConfigLibraryPath();
741
794
  const metadataPath = join(libraryPath, "_meta.json");
742
795
  let configPath = libraryPath;
743
796
 
744
797
  try {
745
- mkdirSync(libraryPath, { recursive: true, mode: 0o700 });
798
+ if (refreshOnly && !desktopRefreshPathsSafe(libraryPath)) {
799
+ return { written: false, path: libraryPath, reason: "desktop_refresh_only_skipped" };
800
+ }
801
+ if (!refreshOnly) mkdirSync(libraryPath, { recursive: true, mode: 0o700 });
746
802
  const metadata = parseMetadata(metadataPath);
747
803
  const selected = metadata.entries.find(entry => entry?.id === metadata.appliedId && isOwnedDesktopGatewayEntry(entry));
804
+ // A refresh may reuse only the applied entry; fallback allocation would silently enroll again.
805
+ if (refreshOnly && selected?.id !== refreshOnly.requiredId) {
806
+ return { written: false, path: libraryPath, reason: "desktop_refresh_only_skipped" };
807
+ }
748
808
  const existing = selected ?? metadata.entries.find(entry => isOwnedDesktopGatewayEntry(entry) && typeof entry.id === "string");
749
809
  const id = existing?.id ?? randomUUID();
750
810
  configPath = profilePath(libraryPath, id);
@@ -757,6 +817,22 @@ function writeDesktop3pConfigWithGenerator(
757
817
  const preserved = readDesktopProfileForeignKeys(configPath);
758
818
  const configJson = JSON.stringify({ ...preserved, ...generated }, null, 2) + "\n";
759
819
  const fingerprint = createHash("sha256").update(configJson).digest("hex").slice(0, 16);
820
+ if (refreshOnly) {
821
+ atomicWriteFileNoFollow(configPath, configJson, undefined, {
822
+ validateBeforeRename: () => {
823
+ try {
824
+ if (!refreshOnly.admit() || !desktopRefreshPathsSafe(libraryPath, configPath)) throw new Error();
825
+ const freshMetadata = parseMetadata(metadataPath);
826
+ if (freshMetadata.appliedId !== refreshOnly.requiredId
827
+ || !isOwnedDesktopGatewayEntry(freshMetadata.entries.find(entry => entry.id === refreshOnly.requiredId))
828
+ || createHash("sha256").update(readFileSync(configPath)).digest("hex").slice(0, 16) !== refreshOnly.expectedFingerprint) {
829
+ throw new Error();
830
+ }
831
+ } catch { throw new Error("desktop_refresh_only_skipped"); }
832
+ },
833
+ });
834
+ return { written: true, path: configPath, fingerprint };
835
+ }
760
836
  const { backupPath } = atomicReplaceDesktopConfig(configPath, configJson);
761
837
  try {
762
838
  atomicWriteFile(metadataPath, JSON.stringify({ ...metadata, appliedId: id, entries }, null, 2) + "\n");
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { routeSupportsOneMillion } from "./long-context";
2
3
  import type {
3
4
  OcxClaudeDesktopAssignment,
4
5
  OcxClaudeDesktopFamily,
@@ -359,7 +360,7 @@ export function renderDesktopProfile(
359
360
  name: isRealAnthropicRoute(route) ? assignment.alias : desktopProfileWireAlias(assignment.alias),
360
361
  family: assignment.family,
361
362
  isFamilyDefault: effectiveDefaults[assignment.family] === route,
362
- supports1m: typeof model.contextWindow === "number" && model.contextWindow >= 1_000_000,
363
+ supports1m: routeSupportsOneMillion(route, model.contextWindow),
363
364
  };
364
365
  });
365
366
  }
@@ -17,6 +17,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
17
17
  import { homedir } from "node:os";
18
18
  import { join } from "node:path";
19
19
  import { localAdmissionToken, localInferenceDestination } from "../lib/local-destinations";
20
+ import { assertNotRealClaudeConfigUnderTest } from "../lib/test-home-guard";
20
21
  import type { OcxConfig } from "../types";
21
22
 
22
23
  export interface GatewayModelRow {
@@ -46,11 +47,27 @@ export interface GatewayModelTarget {
46
47
  /** Claude Code config dir (CLAUDE_CONFIG_DIR override honored, like the CLI). */
47
48
  export function claudeConfigDir(): string {
48
49
  const custom = process.env.CLAUDE_CONFIG_DIR;
49
- return custom && custom.length > 0 ? custom : join(homedir(), ".claude");
50
+ return custom && custom.length > 0 ? custom : join(currentUserHome(), ".claude");
51
+ }
52
+
53
+ /**
54
+ * The home Claude Code itself resolves. Claude Code runs on Node, whose `os.homedir()`
55
+ * consults HOME (POSIX) or USERPROFILE (Windows) at call time when it is set; Bun's
56
+ * `os.homedir()` keeps the value it read at startup. In production the two agree. Under
57
+ * the test preload, which rewrites HOME after Bun has started, only this one follows the
58
+ * sandbox — the cached value is the developer's real home (#6775). Exported for tests.
59
+ */
60
+ export function currentUserHome(env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform): string {
61
+ const fromEnv = platform === "win32" ? env.USERPROFILE : env.HOME;
62
+ return fromEnv !== undefined && fromEnv !== "" ? fromEnv : homedir();
50
63
  }
51
64
 
52
65
  /** Write the cache file; returns its path or null (best-effort, never throws). */
53
66
  export function writeGatewayModelCache(baseUrl: string, models: readonly GatewayModelRow[], configDir = claudeConfigDir()): string | null {
67
+ // Outside the best-effort catch: an armed test process must not write here, and must
68
+ // fail loudly rather than degrade to "returned null".
69
+ // The file itself too: writeFileSync follows a link at gateway-models.json.
70
+ assertNotRealClaudeConfigUnderTest(configDir, join(configDir, "cache"), join(configDir, "cache", "gateway-models.json"));
54
71
  try {
55
72
  // Mirror the CLI's usable-id filter so our file matches what it would cache.
56
73
  const usable = models.filter(m => /(claude|anthropic)/i.test(m.id));