@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
@@ -3,10 +3,10 @@
3
3
  * occasional TaskCreate nudges) into system text that becomes Responses
4
4
  * `instructions`. That churn breaks Muse/Go prefix cache on the instructions
5
5
  * prefix even when tools stay stable. Strip dynamics from instructions;
6
- * surface the latest notice on `input` instead.
6
+ * drop token footers and surface the latest TaskCreate notice on `input` instead.
7
7
  *
8
- * Relocation is identified by harness shape: only a trailing, unfenced,
9
- * canonical notice at the end of instructions is moved. An unmatched fence
8
+ * Peeling is identified by harness shape: only a trailing, unfenced,
9
+ * canonical notice at the end of instructions is removed. An unmatched fence
10
10
  * opener covers through EOF. No match → the original string is returned
11
11
  * byte-for-byte. The matcher is content identity only; `translateAnthropicRequest`
12
12
  * requires `claudeCode.stabilizePromptCache: true` before this helper runs. Claude Code
@@ -86,7 +86,6 @@ export function stabilizeClaudeInstructionsForPromptCache(
86
86
  const ranges = fencedRanges(instructions);
87
87
  let fenceIndex = ranges.length - 1;
88
88
  let end = instructions.length;
89
- let latestTotal: string | null = null;
90
89
  let latestNudge: string | null = null;
91
90
  let peeled = false;
92
91
 
@@ -110,7 +109,6 @@ export function stabilizeClaudeInstructionsForPromptCache(
110
109
 
111
110
  while (fenceIndex >= 0 && ranges[fenceIndex]!.start > contentStart) fenceIndex--;
112
111
  if (fenceIndex >= 0 && contentStart < ranges[fenceIndex]!.end) break;
113
- if (total && latestTotal === null) latestTotal = notice;
114
112
  if (nudge && latestNudge === null) latestNudge = notice;
115
113
  peeled = true;
116
114
 
@@ -123,8 +121,5 @@ export function stabilizeClaudeInstructionsForPromptCache(
123
121
  }
124
122
 
125
123
  if (!peeled) return { instructions, dynamicNotice: null };
126
- const noticeParts: string[] = [];
127
- if (latestTotal) noticeParts.push(latestTotal);
128
- if (latestNudge) noticeParts.push(latestNudge);
129
- return { instructions: instructions.slice(0, end), dynamicNotice: noticeParts.join("\n\n") };
124
+ return { instructions: instructions.slice(0, end), dynamicNotice: latestNudge };
130
125
  }
@@ -281,7 +281,7 @@ function userMessageToItems(content: unknown, input: Rec[], elide: SkillElisionC
281
281
  pushUserMessage(input, pending);
282
282
  }
283
283
 
284
- function assistantMessageToItems(content: unknown, input: Rec[], budget: TranslatorBudget): void {
284
+ function assistantMessageToItems(content: unknown, input: Rec[], budget: TranslatorBudget, requestedModel: string, nativeReasoningReplay: Map<string, string>): void {
285
285
  if (typeof content === "string") {
286
286
  if (content.length > 0) input.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: content }] });
287
287
  return;
@@ -314,6 +314,24 @@ function assistantMessageToItems(content: unknown, input: Rec[], budget: Transla
314
314
  const owned = decodeReasoningEnvelope(signature, budget);
315
315
  if (!owned) throw new AnthropicRequestError("malformed ocxr1 reasoning signature");
316
316
  if (Object.hasOwn(owned, "sig")) throw new AnthropicRequestError("OpenCodex reasoning continuity cannot be replayed as an Anthropic signature");
317
+ if (owned.nat) {
318
+ // The provider's own blob goes back only to the model that minted it; any other
319
+ // model gets the visible text alone, as before this envelope field existed.
320
+ const nat = owned.nat.model === requestedModel ? owned.nat : undefined;
321
+ if (nat) {
322
+ const previous = nativeReasoningReplay.get(nat.enc);
323
+ nativeReasoningReplay.set(nat.enc, previous === undefined || previous === nat.tag ? nat.tag : "");
324
+ }
325
+ if (nat) budget.chargeRetained(2 * nat.enc.length, { kind: "reasoning" });
326
+ if (thinking.length === 0 && !nat) break;
327
+ input.push({
328
+ type: "reasoning",
329
+ id: nat?.id ?? `rs_${crypto.randomUUID().replace(/-/g, "")}`,
330
+ summary: thinking.length > 0 ? [{ type: "summary_text", text: thinking }] : [],
331
+ ...(nat ? { encrypted_content: nat.enc } : {}),
332
+ });
333
+ break;
334
+ }
317
335
  }
318
336
  const encrypted = signature.length === 0 ? undefined : signature.startsWith(OCX_REASONING_PREFIX) ? signature : encodeReasoningEnvelope({ sig: signature }, budget);
319
337
  if (encrypted) budget.chargeRetained(2 * encrypted.length, { kind: "reasoning" });
@@ -355,6 +373,7 @@ export type ClaudeCacheKeySource = "metadata" | "system" | null;
355
373
  export interface ClaudeInboundTranslation {
356
374
  body: Rec;
357
375
  cacheKeySource: ClaudeCacheKeySource;
376
+ nativeReasoningReplay: ReadonlyMap<string, string>;
358
377
  }
359
378
 
360
379
  /**
@@ -400,6 +419,7 @@ function translateAnthropicRequest(
400
419
  }
401
420
 
402
421
  const input: Rec[] = [];
422
+ const nativeReasoningReplay = new Map<string, string>();
403
423
  const systemParts: string[] = [];
404
424
  const topLevelSystem = systemToInstructions(raw.system);
405
425
  if (topLevelSystem !== undefined) systemParts.push(topLevelSystem);
@@ -411,7 +431,7 @@ function translateAnthropicRequest(
411
431
  for (const msg of raw.messages) {
412
432
  if (!isRec(msg)) throw new AnthropicRequestError("each message must be an object");
413
433
  if (msg.role === "user") userMessageToItems(msg.content, input, elide);
414
- else if (msg.role === "assistant") assistantMessageToItems(msg.content, input, budget);
434
+ else if (msg.role === "assistant") assistantMessageToItems(msg.content, input, budget, raw.model, nativeReasoningReplay);
415
435
  else if (msg.role === "system") {
416
436
  const text = systemMessageText(msg.content);
417
437
  // Keep it where the client put it. `developer` is first-class in the Responses
@@ -445,6 +465,7 @@ function translateAnthropicRequest(
445
465
  // a matching suffix or metadata.user_id.
446
466
  const stabilized = stabilizeClaudeInstructionsForPromptCache(joinedSystem);
447
467
  if (stabilized.instructions) body.instructions = stabilized.instructions;
468
+ // Token footers are dropped; append nothing if no other notice remains.
448
469
  if (stabilized.dynamicNotice) {
449
470
  input.push({
450
471
  type: "message",
@@ -530,7 +551,61 @@ function translateAnthropicRequest(
530
551
  reasoning.effort = effortForThinkingBudget(thinking.budget_tokens);
531
552
  }
532
553
  body.reasoning = reasoning;
554
+ // store:false returns a provider's encrypted reasoning only on request. The outbound
555
+ // envelope (`nat`) carries it to the client so the next turn can hand it back.
556
+ body.include = ["reasoning.encrypted_content"];
533
557
  }
534
558
 
535
- return { body, cacheKeySource };
559
+ return { body, cacheKeySource, nativeReasoningReplay };
560
+ }
561
+
562
+ /**
563
+ * Remove proxy-owned native blobs before sending a Messages body to native Anthropic.
564
+ *
565
+ * Every decode, and every re-encode on top of it, is reserved against the request's translator
566
+ * budget first (eight bytes per code unit, as `decodeReasoningEnvelope` does), and every new
567
+ * signature is charged as a retained copy, so a body of near-limit envelopes cannot allocate past the per-turn bound.
568
+ * Throws TranslatorBudgetExceededError when it would; callers answer 413.
569
+ */
570
+ export function nativeAnthropicProjection(body: Rec, budget: TranslatorBudget): Rec {
571
+ if (!Array.isArray(body.messages)) return body;
572
+ let changed = false;
573
+ const messages = body.messages.flatMap(message => {
574
+ if (!isRec(message) || message.role !== "assistant" || !Array.isArray(message.content)) return [message];
575
+ let localChanged = false;
576
+ const content = message.content.flatMap(block => {
577
+ if (!isRec(block) || block.type !== "thinking" || typeof block.signature !== "string"
578
+ || !block.signature.startsWith(OCX_REASONING_PREFIX)) return [block];
579
+ // The decoded object stays alive while it is re-encoded, so its reservation is held until
580
+ // the new signature exists; the encode copies get their own reservation on top of it.
581
+ const decodeReservation = budget.reserveTransient(8 * block.signature.length, { kind: "reasoning" });
582
+ try {
583
+ let decoded: unknown;
584
+ try { decoded = JSON.parse(Buffer.from(block.signature.slice(OCX_REASONING_PREFIX.length), "base64").toString("utf8")); }
585
+ catch { decoded = undefined; }
586
+ if (!isRec(decoded)) { localChanged = true; return []; }
587
+ if (!Object.hasOwn(decoded, "nat")) return [block];
588
+ localChanged = true;
589
+ const { nat: _nat, ...rest } = decoded;
590
+ if (!rest.sig && !rest.red && !rest.krc && !(typeof rest.txt === "string" && rest.txt.length > 0)) return [];
591
+ // The re-encoded envelope is never larger than the decoded one (a field was removed), so a
592
+ // decode-sized reservation bounds the stringify, buffer and base64 copies before they exist.
593
+ const encodeReservation = budget.reserveTransient(8 * block.signature.length, { kind: "reasoning" });
594
+ let signature: string;
595
+ try {
596
+ signature = OCX_REASONING_PREFIX + Buffer.from(JSON.stringify(rest), "utf8").toString("base64");
597
+ } finally {
598
+ encodeReservation.release();
599
+ }
600
+ budget.chargeRetained(2 * signature.length, { kind: "reasoning" });
601
+ return [{ ...block, signature }];
602
+ } finally {
603
+ decodeReservation.release();
604
+ }
605
+ });
606
+ if (!localChanged) return [message];
607
+ changed = true;
608
+ return content.length ? [{ ...message, content }] : [];
609
+ });
610
+ return changed ? { ...body, messages } : body;
536
611
  }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { readdirSync, unlinkSync } from "node:fs";
16
16
  import { join } from "node:path";
17
+ import { assertNotRealClaudeConfigUnderTest } from "../../lib/test-home-guard";
17
18
  import type { ClaudeFirstPartyDesired } from "../first-party-settings";
18
19
  import { classifyInterceptClient } from "./client-class";
19
20
  import { BOOTSTRAP_MAX_DECODED_BYTES, injectPickerModels, type PickerModelEntry } from "./picker-bootstrap";
@@ -150,6 +151,8 @@ export async function rewriteCliCatalogResponse(
150
151
  */
151
152
  export function invalidateClaudeCodeServedCatalog(claudeDir: string): number {
152
153
  const dir = join(claudeDir, "cache", "model-catalog");
154
+ // Outside the catch below: an armed test process must not prune the real Claude cache (#6775).
155
+ assertNotRealClaudeConfigUnderTest(claudeDir, dir);
153
156
  let names: string[];
154
157
  try { names = readdirSync(dir); } catch { return 0; } // no-excuse-ok: catch -- no cache directory means nothing to drop.
155
158
  let removed = 0;
@@ -35,6 +35,7 @@ export const DESKTOP_PICKER_ID_SUGGESTIONS: readonly string[] = [
35
35
  "claude-sonnet-5-5",
36
36
  "claude-sonnet-5",
37
37
  "claude-fable-5-1",
38
+ "claude-haiku-5-5", // 261008 preemptive suggestion; Desktop owns the live picker.
38
39
  "claude-haiku-4-5",
39
40
  "claude-opus-5",
40
41
  "claude-fable-5",
@@ -1,5 +1,5 @@
1
1
  import { X509Certificate } from "node:crypto";
2
- import { constants, closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync, rmSync, writeFileSync, type Stats } from "node:fs";
2
+ import { constants, closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync, rmSync, writeFileSync, type BigIntStats } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import {
5
5
  decodePickerCaCredential, openPickerCaCredential, pickerCaConfigId, readPickerCaCredential,
@@ -21,18 +21,19 @@ export function canonicalPickerConfigDir(configDir: string): string {
21
21
 
22
22
  /** Public records still cannot follow symlinks or accept oversized/malformed recovery data. */
23
23
  function readState(path: string, configId: string, initialization: boolean): Initialization | AuthorityMetadata | null {
24
- let expected: Stats;
25
- try { expected = lstatSync(path); }
24
+ // BigInt stats: a Windows file ID exceeds 2^53, so Number ino loses low bits and two files can compare equal.
25
+ let expected: BigIntStats;
26
+ try { expected = lstatSync(path, { bigint: true }); }
26
27
  catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw new Error("picker_ca_metadata_unsafe"); }
27
28
  if (expected.isSymbolicLink()) throw new Error("picker_ca_metadata_unsafe");
28
29
  let fd: number;
29
30
  try { fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); }
30
31
  catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw new Error("picker_ca_metadata_unsafe"); }
31
32
  try {
32
- const stat = fstatSync(fd);
33
+ const stat = fstatSync(fd, { bigint: true });
33
34
  if (stat.dev !== expected.dev || stat.ino !== expected.ino) throw new Error("picker_ca_metadata_unsafe");
34
- if (!stat.isFile() || stat.nlink !== 1 || stat.size > MAX_PUBLIC_STATE_BYTES
35
- || (process.platform !== "win32" && stat.uid !== process.getuid!())) throw new Error();
35
+ if (!stat.isFile() || stat.nlink !== 1n || stat.size > BigInt(MAX_PUBLIC_STATE_BYTES)
36
+ || (process.platform !== "win32" && stat.uid !== BigInt(process.getuid!()))) throw new Error();
36
37
  const bytes = Buffer.alloc(MAX_PUBLIC_STATE_BYTES + 1);
37
38
  let used = 0;
38
39
  while (used < bytes.length) {
@@ -11,7 +11,7 @@
11
11
  * HTTP/1.1 server, unchanged. Upstream is one HTTP/1.1 request per client request on both paths.
12
12
  * Only the bounded bootstrap response is held; other bodies and upgraded sockets relay as streams.
13
13
  */
14
- import { createSecureServer } from "node:http2";
14
+ import { constants as h2Constants, createSecureServer } from "node:http2";
15
15
  import type { Http2ServerRequest, Http2ServerResponse, ServerHttp2Session } from "node:http2";
16
16
  import { createServer, request as httpsRequest } from "node:https";
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
@@ -184,9 +184,35 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
184
184
  // reason phrase differs, and sendHead branches on it.
185
185
  const res = relayRes as ServerResponse;
186
186
  const h2 = req.httpVersionMajor === 2;
187
+ // Bun's h2 compat response emits finish only after both stream halves close.
188
+ const responseWritable = h2 ? (req as Http2ServerRequest).stream : res;
189
+ // Framing, not method, determines whether a peer can keep sending a body. A bodyless h2
190
+ // request carries END_STREAM on its headers; HTTP/1.1 needs length or chunked framing.
191
+ const uploading = h2 ? !(req as Http2ServerRequest).stream.endAfterHeaders
192
+ : req.headers["transfer-encoding"] !== undefined || Number(req.headers["content-length"] ?? 0) > 0;
193
+ const closeInput = (completeReply = false) => {
194
+ if (h2) {
195
+ const stream = (req as Http2ServerRequest).stream;
196
+ if (!stream.closed && !stream.destroyed) {
197
+ stream.close(completeReply ? h2Constants.NGHTTP2_NO_ERROR : h2Constants.NGHTTP2_CANCEL);
198
+ }
199
+ } else if (!req.destroyed) {
200
+ if (completeReply) {
201
+ // finish is not peer receipt: destroy can truncate bytes still queued in TLS/TCP.
202
+ // Discard further input and flush a graceful FIN without waiting for body completion.
203
+ req.resume();
204
+ req.socket.end();
205
+ } else req.destroy();
206
+ }
207
+ };
187
208
  // Refusals answer with an empty response and a fixed log line that carries no request data.
188
209
  const refuse = (status: 400 | 503) => {
189
- res.writeHead(status, { "Content-Length": "0" });
210
+ // Do not leave a refused, still-uploading stream behind after delivering its empty reply.
211
+ if (uploading && !req.complete) {
212
+ req.once("error", () => res.destroy());
213
+ responseWritable.once("finish", () => closeInput(true));
214
+ }
215
+ res.writeHead(status, { "Content-Length": "0", ...(!h2 && uploading && !req.complete ? { Connection: "close" } : {}) });
190
216
  res.end();
191
217
  options.log?.(`picker request refused ${status}`);
192
218
  };
@@ -209,12 +235,16 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
209
235
  const fail = () => {
210
236
  if (clientGone) return;
211
237
  if (res.headersSent) res.destroy();
212
- else { res.writeHead(502, { "Content-Length": "0" }); res.end(); log(502); }
238
+ else {
239
+ res.writeHead(502, { "Content-Length": "0", ...(!h2 && uploading && !req.complete ? { Connection: "close" } : {}) });
240
+ res.end(); log(502);
241
+ }
213
242
  };
214
243
  const omit = bootstrap ? new Set(["accept-encoding"]) : new Set<string>();
215
244
  const headers = upstreamRequestHeaders(req, omit);
216
245
  if (bootstrap) headers.push("Accept-Encoding", narrowBootstrapAcceptEncoding());
217
246
  let upReq: ReturnType<typeof httpsRequest>;
247
+ let upstreamResponded = false;
218
248
  try {
219
249
  // The HTTP/1.1 client validates the method, path and header values synchronously.
220
250
  upReq = httpsRequest({
@@ -222,13 +252,17 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
222
252
  ca: upstream.ca, rejectUnauthorized: true, agent: false,
223
253
  method, path: req.url, headers, maxHeaderSize: PICKER_MAX_HEADER_BYTES,
224
254
  }, upRes => {
255
+ upstreamResponded = true;
225
256
  const status = upRes.statusCode ?? 502;
226
257
  const originalHeaders = filteredHeaders(upRes.rawHeaders);
227
258
  const sendHead = (raw: string[]) => {
228
259
  if (res.headersSent) return;
229
260
  // HTTP/2 has no reason phrase; its compat writeHead takes the same flat raw header array.
230
261
  if (h2) (relayRes as Http2ServerResponse).writeHead(status, raw as unknown as Record<string, string>);
231
- else res.writeHead(status, upRes.statusMessage, raw);
262
+ else {
263
+ if (uploading && !req.complete) raw.push("Connection", "close");
264
+ res.writeHead(status, upRes.statusMessage, raw);
265
+ }
232
266
  log(status);
233
267
  };
234
268
  upRes.on("error", fail);
@@ -293,6 +327,39 @@ export async function startPickerListener(options: PickerListenerOptions): Promi
293
327
  // Bun's compat response skips its close event for a HEAD reset before end(); the stream's own
294
328
  // close always fires. Destroying an upstream request that already completed is a no-op.
295
329
  if (h2) (req as Http2ServerRequest).stream.once("close", onClientClose);
330
+ if (uploading) {
331
+ let uploadFinished = false;
332
+ const finishUpload = () => {
333
+ if (uploadFinished) return;
334
+ uploadFinished = true;
335
+ req.off("end", finishUpload);
336
+ req.off("close", finishUpload);
337
+ upReq.off("close", onUpstreamClose);
338
+ responseWritable.off("finish", onResponseFinish);
339
+ res.off("close", stopUnfinishedUpload);
340
+ if (h2) (req as Http2ServerRequest).stream.off("close", finishUpload);
341
+ };
342
+ // Upstream close can precede draining a successful reply to a slow downstream.
343
+ // Stop relaying input only once that reply's writable finishes or closes.
344
+ const stopUnfinishedUpload = (completeReply = false) => {
345
+ if (!uploadFinished) {
346
+ req.unpipe(upReq);
347
+ closeInput(completeReply);
348
+ finishUpload();
349
+ upReq.destroy();
350
+ }
351
+ };
352
+ const onResponseFinish = () => stopUnfinishedUpload(true);
353
+ const onUpstreamClose = () => {
354
+ if (!upstreamResponded && !res.writableEnded) stopUnfinishedUpload();
355
+ };
356
+ req.once("end", finishUpload);
357
+ req.once("close", finishUpload);
358
+ upReq.once("close", onUpstreamClose);
359
+ responseWritable.once("finish", onResponseFinish);
360
+ res.once("close", stopUnfinishedUpload);
361
+ if (h2) (req as Http2ServerRequest).stream.once("close", finishUpload);
362
+ }
296
363
  req.pipe(upReq);
297
364
  };
298
365
  server.on("request", relay);
@@ -7,7 +7,8 @@ import { dirname } from "node:path";
7
7
  import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../../codex/catalog";
8
8
  import type { OcxClaudeDesktopProfile } from "../../types";
9
9
  import { aliasForRoute, claudeCodeNativeAlias } from "../alias";
10
- import { AUTO_CONTEXT_OFF, stripOneMillionMarker, withOneMillionMarker } from "../context-windows";
10
+ import { AUTO_CONTEXT_OFF, stripOneMillionMarker, UNPAIRED_AUTO_CONTEXT, withOneMillionMarker, type AutoContextMode } from "../context-windows";
11
+ import { isAnthropicClaudeRoute } from "../long-context";
11
12
  import { activeDesktop3pAlias, displayModelId, resolveDesktop3pAlias, type Desktop3pRoutedModel } from "../desktop-3p";
12
13
  import { reconcileDesktopProfile, renderDesktopProfile, type DesktopProfileModel } from "../desktop-profile";
13
14
  import type { PickerModelEntry } from "./picker-bootstrap";
@@ -17,6 +18,8 @@ export interface PickerRouteInput {
17
18
  routedModels: Desktop3pRoutedModel[];
18
19
  profile?: OcxClaudeDesktopProfile;
19
20
  nativeContextCap?: NativeContextLimitsInput;
21
+ /** resolveAutoContext(config.claudeCode); only its contextAccounting "200k" opt-in matters here. */
22
+ auto?: AutoContextMode;
20
23
  }
21
24
 
22
25
  /** Candidate routes in the gateway profile's order and with its labels. */
@@ -46,9 +49,14 @@ function splitRoute(route: string): { provider: string; id: string } {
46
49
  return { provider: route.slice(0, slash), id: route.slice(slash + 1) };
47
50
  }
48
51
 
49
- /** Desktop runners do not inherit the proxy's compaction env: mark only real >=1M windows. */
50
- function pickerSelector(alias: string, contextWindow: number | undefined): string {
51
- return withOneMillionMarker(alias, contextWindow === undefined ? {} : { [alias]: contextWindow }, AUTO_CONTEXT_OFF)!;
52
+ /**
53
+ * Desktop runners do not inherit the proxy's compaction env, so a long window (>= the default
54
+ * compact window) is marked on the strength of the `prompt is too long` recovery instead
55
+ * (devlog/_plan/261009_claude_1m_default/020). Real Anthropic models need a genuine 1M.
56
+ */
57
+ function pickerSelector(alias: string, contextWindow: number | undefined, provider: string, id: string, auto?: AutoContextMode): string {
58
+ const mode = auto?.accounting200k ? auto : isAnthropicClaudeRoute(provider, id) ? AUTO_CONTEXT_OFF : UNPAIRED_AUTO_CONTEXT;
59
+ return withOneMillionMarker(alias, contextWindow === undefined ? {} : { [alias]: contextWindow }, mode)!;
52
60
  }
53
61
 
54
62
  export function buildPickerModels(input: PickerRouteInput): PickerModelEntry[] {
@@ -61,7 +69,7 @@ export function buildPickerModels(input: PickerRouteInput): PickerModelEntry[] {
61
69
  const alias = provider === "native" ? claudeCodeNativeAlias(id) : aliasForRoute(provider, id);
62
70
  if (!alias || seen.has(alias)) continue;
63
71
  seen.add(alias);
64
- out.push({ id: pickerSelector(alias, model.contextWindow), name: model.label,
72
+ out.push({ id: pickerSelector(alias, model.contextWindow, provider, id, input.auto), name: model.label,
65
73
  ...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }) });
66
74
  }
67
75
  return out;
@@ -81,7 +89,7 @@ export function buildCliPickerModels(input: PickerRouteInput): PickerModelEntry[
81
89
  const alias = activeDesktop3pAlias(provider, id);
82
90
  if (seen.has(alias) || resolveDesktop3pAlias(alias) !== model.route) continue;
83
91
  seen.add(alias);
84
- out.push({ id: pickerSelector(alias, model.contextWindow), name: model.label,
92
+ out.push({ id: pickerSelector(alias, model.contextWindow, provider, id, input.auto), name: model.label,
85
93
  description: `opencodex · ${model.route}`, route: model.route,
86
94
  ...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }) });
87
95
  }
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { atomicWriteFile } from "../../config/atomic-write";
4
+ import { assertNotRealClaudeConfigUnderTest } from "../../lib/test-home-guard";
4
5
  import { claudeConfigDir } from "../auth-detect";
5
6
  import { invalidateClaudeCodeServedCatalog } from "./cli-catalog";
6
7
 
@@ -112,6 +113,8 @@ export function inspectClaudeInterceptSettings(
112
113
  }
113
114
 
114
115
  function writeSettings(path: string, doc: SettingsDoc): void {
116
+ // An armed test process must not rewrite the real Claude settings.json (#6775).
117
+ assertNotRealClaudeConfigUnderTest(dirname(path), path);
115
118
  mkdirSync(dirname(path), { recursive: true });
116
119
  // The managed env embeds the proxy token, so the file must stay owner-only:
117
120
  // atomicWriteFile applies the real NTFS ACL on Windows where chmod is a no-op.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Long-context thresholds shared by every Claude surface
3
+ * (devlog/_plan/261009_claude_1m_default/020_long_context_eligibility.md).
4
+ *
5
+ * A leaf module: desktop-3p.ts and desktop-profile.ts need the predicate, and context-windows.ts
6
+ * already imports desktop-3p.ts, so the predicate cannot live there.
7
+ */
8
+ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
9
+
10
+ export const ONE_MILLION = 1_000_000;
11
+
12
+ /** A window at or below this is Claude Code's own default accounting; widening it gains nothing. */
13
+ export const AUTO_CONTEXT_FLOOR = 200_000;
14
+
15
+ /**
16
+ * Auto-context defaults (devlog 260712 020, user-approved).
17
+ *
18
+ * The compact window is the token count at which Claude Code starts compacting, and it is
19
+ * also the floor `shouldMarkOneMillion` uses — a model may only carry the marker if it can
20
+ * host this window. 350,000 was chosen when the widest native row advertised 372,000.
21
+ *
22
+ * It now matches the auto-compaction limit the Codex catalog ships for the same models
23
+ * (`nativeAutoCompactLimit`: 829,800 against the 922,000 native window). Leaving the two
24
+ * apart meant one model compacting at 350k under Claude Code and at 829,800 under Codex.
25
+ * The value stays clear of the measured 922,000 ceiling by ~92k, so compaction still has
26
+ * room to run before the upstream refuses.
27
+ */
28
+ export const AUTO_COMPACT_WINDOW_DEFAULT = 829_800;
29
+
30
+ /**
31
+ * Whether a window earns the 1M selection on a surface whose Claude Code runner may not inherit
32
+ * CLAUDE_CODE_AUTO_COMPACT_WINDOW (Desktop pickers, discovery, Desktop 3P, generated subagents).
33
+ * A window >= 1M always does. A smaller one does when it can host the default compact window, so
34
+ * a runner that has the variable compacts in time, and one that lacks it overflows into the
35
+ * `prompt is too long` envelope (anthropicErrorBody) and compacts reactively. The floor is fixed
36
+ * on purpose: a custom compact window must not re-admit a 372k route (#854).
37
+ */
38
+ export function isLongContextWindow(window: number | undefined): boolean {
39
+ if (typeof window !== "number" || window <= 0) return false;
40
+ return window >= ONE_MILLION || (window > AUTO_CONTEXT_FLOOR && window >= AUTO_COMPACT_WINDOW_DEFAULT);
41
+ }
42
+
43
+ /**
44
+ * A Claude model on an Anthropic-named route. Claude windows are 200k or a genuine 1M and a bare
45
+ * id rides the native passthrough, so the long-window rule never widens one: a Claude row listed
46
+ * under 1M is a capped one, and on a runner without the compaction env it keeps the 200k
47
+ * accounting it had before long windows were widened. Keyed on the model id as well, so a
48
+ * configured gateway that only shares the name `anthropic2` and serves other models keeps the
49
+ * long-window rule. A custom `anthropic2` gateway serving Claude below 1M is treated like a pool
50
+ * row on these surfaces, which is the pre-existing (unwidened) behavior.
51
+ */
52
+ export function isAnthropicClaudeRoute(provider: string, modelId: string): boolean {
53
+ return isAnthropicInstanceId(provider) && modelId.startsWith("claude-");
54
+ }
55
+
56
+ /** Desktop 3P / picker / discovery / dashboard 1M eligibility for one provider route. */
57
+ export function claudeSurfaceSupportsOneMillion(provider: string, modelId: string, window: number | undefined): boolean {
58
+ if (isAnthropicClaudeRoute(provider, modelId)) return typeof window === "number" && window >= ONE_MILLION;
59
+ return isLongContextWindow(window);
60
+ }
61
+
62
+ /** Same, for a `provider/id` route string. */
63
+ export function routeSupportsOneMillion(route: string, window: number | undefined): boolean {
64
+ const slash = route.indexOf("/");
65
+ return claudeSurfaceSupportsOneMillion(route.slice(0, slash), route.slice(slash + 1), window);
66
+ }
@@ -19,7 +19,8 @@ import { orderForModelPicker, catalogModelEfforts, nativeEffortClamp, nativeOpen
19
19
  import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
20
20
  import { cursorFastIdFor } from "../adapters/cursor/catalog";
21
21
  import { desktop3pAlias } from "./desktop-3p";
22
- import { AUTO_CONTEXT_OFF, type AutoContextMode } from "./context-windows";
22
+ import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, UNPAIRED_AUTO_CONTEXT, type AutoContextMode } from "./context-windows";
23
+ import { isAnthropicClaudeRoute } from "./long-context";
23
24
 
24
25
  const MODEL_INFO_CREATED_AT = "2026-01-01T00:00:00Z";
25
26
  const ANTHROPIC_EFFORT_RUNGS = new Set(["low", "medium", "high", "xhigh", "max"]);
@@ -144,23 +145,25 @@ export function buildAnthropicModelInfos(
144
145
  : aliasForRoute(m.provider, m.id);
145
146
  }),
146
147
  ]);
147
- // [1m] picker variant (devlog 260712 B1): Claude Code accounts exactly 1M for ids
148
- // carrying the marker (2.1.207 binary: /\[1m\]/i → 1e6, compaction preserved), so
149
- // ONLY models with an authoritative >=1M window get a second selectable row —
150
- // the auto-context widening that let a 372K route carry the marker (and be
151
- // over-filled) is the #854 defect and does not come back. Guards (audit R1#11):
152
- // same dedupe set, never double-suffix.
148
+ // [1m] picker variant (devlog 260712 B1; widened in devlog/_plan/261009_claude_1m_default/020):
149
+ // Claude Code accounts exactly 1M for ids carrying the marker (2.1.207 binary: /\[1m\]/i -> 1e6,
150
+ // compaction preserved). A discovery client may lack CLAUDE_CODE_AUTO_COMPACT_WINDOW, so the
151
+ // rule is the fixed unpaired one (long-context.ts), not the configurable launch compact window:
152
+ // a custom 350k window must not re-admit a 372k route (#854). The server's auto mode only turns
153
+ // widening off (autoContext: false / maxContextTokens), falling back to >= 1M. Base and Fast
154
+ // rows stay unmarked: this list is a choice list. Guards (audit R1#11): same dedupe set, never
155
+ // double-suffix.
156
+ const variantMode = auto.enabled ? UNPAIRED_AUTO_CONTEXT : AUTO_CONTEXT_OFF;
153
157
  const push1mVariant = (
154
158
  base: AnthropicModelInfo,
155
159
  contextWindow: number | undefined,
156
160
  maxInputTokens?: number,
157
161
  selectorId?: string,
162
+ mode: AutoContextMode = variantMode,
158
163
  ) => {
159
- // The [1m] marker makes Claude Code account 1e6 tokens for the row, so it
160
- // may only name models whose AUTHORITATIVE effective window is >= 1M —
161
- // never the auto-context widening, which would mark a 372K route and have
162
- // Claude Code over-fill it (the #854 defect).
163
- if (contextWindow === undefined || contextWindow < ONE_MILLION) return;
164
+ // A window >= 1M always earns the row; a long window earns it under the unpaired rule,
165
+ // because an overflow is answered as `prompt is too long`, which Claude Code compacts on.
166
+ if (!shouldMarkOneMillion(contextWindow, mode)) return;
164
167
  if (base.id.includes("[1m]")) return;
165
168
  const id = selectorId ?? `${base.id}[1m]`;
166
169
  if (seen.has(id)) return;
@@ -170,9 +173,9 @@ export function buildAnthropicModelInfos(
170
173
  // (measured — see devlog/_plan/260817_native_gpt56_1m_context/001_measurement_evidence.md).
171
174
  // Advertising the flat 1e6 there would invite mid-session context_length_exceeded, so the
172
175
  // variant reports whichever of the two is smaller.
173
- const advertised = typeof maxInputTokens === "number" && maxInputTokens > 0
174
- ? Math.min(ONE_MILLION, maxInputTokens)
175
- : ONE_MILLION;
176
+ // A long window under 1M must not be advertised as 1e6 input either.
177
+ const ceiling = typeof maxInputTokens === "number" && maxInputTokens > 0 ? maxInputTokens : contextWindow;
178
+ const advertised = typeof ceiling === "number" && ceiling > 0 ? Math.min(ONE_MILLION, ceiling) : ONE_MILLION;
176
179
  out.push({ ...base, id, display_name: `${base.display_name} · 1M`, max_input_tokens: advertised });
177
180
  };
178
181
  /**
@@ -251,7 +254,7 @@ export function buildAnthropicModelInfos(
251
254
  && listedModelId.startsWith("claude-fable-")
252
255
  ? `${claudeCodeNativeAlias(listedModelId)}[1m]`
253
256
  : undefined;
254
- push1mVariant(info, m.contextWindow, routedMaxInput, oneMillionSelector);
257
+ push1mVariant(info, m.contextWindow, routedMaxInput, oneMillionSelector, isAnthropicClaudeRoute(m.provider, m.id) ? AUTO_CONTEXT_OFF : variantMode);
255
258
  // The whole model is passed, not a (provider, id) pair: a combo row lives in its own
256
259
  // namespace with no config.providers entry, so the caller classifies it from the
257
260
  // aggregated supportsServiceTier the row already carries.