@bitkyc08/opencodex 2.84.0 → 2.87.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 (518) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  4. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  5. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DCd-A1az.js} +1 -1
  6. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  7. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  8. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -2
  11. package/src/adapters/anthropic/beta-allowlist.ts +91 -11
  12. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  13. package/src/adapters/anthropic/client-identity.ts +1 -1
  14. package/src/adapters/anthropic/passthrough.ts +16 -3
  15. package/src/adapters/anthropic-progress.ts +84 -0
  16. package/src/adapters/anthropic.ts +108 -21
  17. package/src/adapters/azure.ts +19 -1
  18. package/src/adapters/base.ts +2 -0
  19. package/src/adapters/claude-cli/adapter.ts +111 -23
  20. package/src/adapters/claude-cli/profiles.ts +2 -1
  21. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  22. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  23. package/src/adapters/client-fingerprint.ts +24 -0
  24. package/src/adapters/coding-agent/protocol.ts +1 -1
  25. package/src/adapters/coding-agent/turn.ts +40 -11
  26. package/src/adapters/cursor/arg-codec.ts +17 -5
  27. package/src/adapters/cursor/catalog.ts +8 -0
  28. package/src/adapters/cursor/effort-map.ts +3 -0
  29. package/src/adapters/cursor/freeform-args.ts +75 -0
  30. package/src/adapters/cursor/protobuf-events.ts +36 -23
  31. package/src/adapters/dsh-account.ts +97 -0
  32. package/src/adapters/google-antigravity-wire.ts +22 -1
  33. package/src/adapters/google.ts +37 -10
  34. package/src/adapters/openai-chat/messages.ts +4 -3
  35. package/src/adapters/openai-chat/passthrough.ts +8 -2
  36. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  37. package/src/adapters/openai-chat/response-events.ts +59 -1
  38. package/src/adapters/openai-chat.ts +8 -5
  39. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  40. package/src/adapters/openai-responses/passthrough.ts +57 -125
  41. package/src/adapters/openai-responses/request-strips.ts +25 -3
  42. package/src/adapters/openai-responses/transport.ts +81 -0
  43. package/src/adapters/qoder/adapter.ts +44 -3
  44. package/src/adapters/registry.ts +5 -0
  45. package/src/adapters/zed.ts +26 -1
  46. package/src/advisor/activity.ts +55 -0
  47. package/src/advisor/budget.ts +62 -0
  48. package/src/advisor/consult.ts +223 -0
  49. package/src/advisor/context.ts +238 -0
  50. package/src/advisor/disclosure.ts +20 -0
  51. package/src/advisor/evidence-tool.ts +21 -0
  52. package/src/advisor/evidence.ts +117 -0
  53. package/src/advisor/runtime.ts +353 -0
  54. package/src/advisor/settings.ts +144 -0
  55. package/src/advisor/state.ts +369 -0
  56. package/src/advisor/synthetic-tool.ts +33 -0
  57. package/src/bridge/errors.ts +24 -1
  58. package/src/bridge/internal.ts +3 -1
  59. package/src/bridge/response-json.ts +18 -3
  60. package/src/bridge/sse.ts +58 -8
  61. package/src/bridge/terminal-refusal.ts +59 -0
  62. package/src/chat/inbound.ts +30 -6
  63. package/src/chat/outbound.ts +5 -0
  64. package/src/claude/desktop-3p-library.ts +72 -7
  65. package/src/claude/desktop-3p.ts +75 -19
  66. package/src/claude/desktop-first-party.ts +1 -1
  67. package/src/claude/desktop-picker-platform.ts +4 -0
  68. package/src/claude/desktop-picker-profile.ts +11 -4
  69. package/src/claude/desktop-picker.ts +15 -8
  70. package/src/claude/desktop-profile.ts +10 -1
  71. package/src/claude/desktop-remote-store-io.ts +5 -3
  72. package/src/claude/desktop-system-proxy.ts +50 -4
  73. package/src/claude/inbound-content-options.ts +11 -0
  74. package/src/claude/inbound.ts +154 -50
  75. package/src/claude/intercept/local-ca.ts +8 -1
  76. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  77. package/src/claude/intercept/picker-runtime.ts +6 -5
  78. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  79. package/src/claude/intercept/picker-trust.ts +19 -0
  80. package/src/claude/intercept/port-policy.ts +30 -0
  81. package/src/claude/intercept/runtime.ts +97 -94
  82. package/src/claude/model-default-settings.ts +154 -0
  83. package/src/claude/model-info.ts +7 -7
  84. package/src/claude/outbound.ts +51 -19
  85. package/src/cli/account-api.ts +1 -0
  86. package/src/cli/account-policy-dto.ts +6 -0
  87. package/src/cli/account-policy.ts +34 -5
  88. package/src/cli/account-preservation.ts +44 -0
  89. package/src/cli/account.ts +8 -2
  90. package/src/cli/advisor.ts +165 -0
  91. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  92. package/src/cli/capabilities-accounts.ts +20 -1
  93. package/src/cli/capabilities-agents-routing.ts +23 -2
  94. package/src/cli/capabilities-base.ts +39 -11
  95. package/src/cli/capabilities-observe-system.ts +63 -3
  96. package/src/cli/capabilities-provider-models.ts +11 -0
  97. package/src/cli/chatgpt-command.ts +35 -14
  98. package/src/cli/claude.ts +46 -6
  99. package/src/cli/codex-cli-update.ts +151 -41
  100. package/src/cli/codex-provider.ts +44 -0
  101. package/src/cli/combo-input.ts +10 -1
  102. package/src/cli/combo-result.ts +1 -1
  103. package/src/cli/combo.ts +2 -1
  104. package/src/cli/cross-home-owner.ts +17 -14
  105. package/src/cli/dispatch.ts +30 -5
  106. package/src/cli/doctor.ts +15 -1
  107. package/src/cli/ensure-desktop-cold-start.ts +382 -0
  108. package/src/cli/export-command.ts +1 -1
  109. package/src/cli/help.ts +4 -2
  110. package/src/cli/index.ts +14 -5
  111. package/src/cli/integration-plan-dto.ts +3 -0
  112. package/src/cli/log-history.ts +172 -0
  113. package/src/cli/models-runtime-subcommands.ts +1 -0
  114. package/src/cli/models-runtime.ts +72 -8
  115. package/src/cli/observe-stream.ts +2 -0
  116. package/src/cli/observe.ts +16 -4
  117. package/src/cli/provider.ts +27 -5
  118. package/src/cli/registry.ts +41 -9
  119. package/src/cli/restart-scope.ts +3 -1
  120. package/src/cli/runtime-api.ts +5 -3
  121. package/src/cli/system-command.ts +40 -8
  122. package/src/cli/uninstall-client-state.ts +32 -0
  123. package/src/cli/usage-compare.ts +309 -0
  124. package/src/cli/usage-model-search.ts +4 -4
  125. package/src/cli/usage-report.ts +2 -2
  126. package/src/cli/v2.ts +1 -1
  127. package/src/client/machine-listener.ts +1 -3
  128. package/src/client/runtime.ts +1 -0
  129. package/src/clients/client-path.ts +51 -0
  130. package/src/clients/config-export/contracts.ts +3 -1
  131. package/src/clients/config-export/model-metadata.ts +3 -2
  132. package/src/clients/config-export/qoder.ts +93 -0
  133. package/src/clients/config-export.ts +35 -48
  134. package/src/clients/gentle-shell-path.ts +87 -0
  135. package/src/codex/account-auto-switch.ts +2 -0
  136. package/src/codex/account-identity.ts +29 -0
  137. package/src/codex/account-label.ts +1 -1
  138. package/src/codex/account-lifecycle.ts +21 -5
  139. package/src/codex/account-preservation.ts +28 -0
  140. package/src/codex/account-use-remaining.ts +23 -0
  141. package/src/codex/app-server-processes.ts +97 -7
  142. package/src/codex/app-server-restart-service.ts +55 -4
  143. package/src/codex/auth-api/account-list.ts +4 -0
  144. package/src/codex/auth-api/account-pause-group.ts +6 -50
  145. package/src/codex/auth-api/account-preservation.ts +57 -0
  146. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  147. package/src/codex/auth-api/routes.ts +8 -0
  148. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  149. package/src/codex/auth-api.ts +0 -1
  150. package/src/codex/auth-context.ts +81 -7
  151. package/src/codex/autostart-health.ts +4 -0
  152. package/src/codex/catalog/auto-review.ts +20 -3
  153. package/src/codex/catalog/build-entries.ts +19 -2
  154. package/src/codex/catalog/combo-member.ts +14 -5
  155. package/src/codex/catalog/configured-tiers.ts +50 -0
  156. package/src/codex/catalog/effort.ts +4 -1
  157. package/src/codex/catalog/gather-capture.ts +2 -0
  158. package/src/codex/catalog/metadata.ts +11 -9
  159. package/src/codex/catalog/model-hints.ts +42 -19
  160. package/src/codex/catalog/model-visibility.ts +3 -1
  161. package/src/codex/catalog/native-ultrafast.ts +106 -0
  162. package/src/codex/catalog/parsing.ts +35 -86
  163. package/src/codex/catalog/provider-models.ts +40 -34
  164. package/src/codex/catalog/retained-sync.ts +2 -0
  165. package/src/codex/catalog/routed-gather.ts +33 -16
  166. package/src/codex/catalog/vendor-context.ts +82 -0
  167. package/src/codex/cli-install-provenance.ts +105 -17
  168. package/src/codex/cli-update-plan.ts +545 -0
  169. package/src/codex/config-write-lock.ts +63 -4
  170. package/src/codex/convergence.ts +2 -0
  171. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  172. package/src/codex/desktop-app/types.ts +2 -0
  173. package/src/codex/desktop-app/windows.ts +29 -16
  174. package/src/codex/desktop-app-restart.ts +25 -2
  175. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  176. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  177. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  178. package/src/codex/desktop-compatibility/installed-build.ts +73 -0
  179. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  180. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  181. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  182. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  183. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  184. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  185. package/src/codex/desktop-compatibility/runtime.ts +182 -0
  186. package/src/codex/desktop-compatibility/service.ts +30 -0
  187. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  188. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  189. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  190. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  191. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  192. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  193. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  194. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  195. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  196. package/src/codex/desktop-compatibility/windows-key-protection.ts +165 -0
  197. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  198. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  199. package/src/codex/features.ts +85 -45
  200. package/src/codex/history-provider.ts +1 -1
  201. package/src/codex/inject/config-write-section.ts +4 -1
  202. package/src/codex/inject/history-failure.ts +27 -0
  203. package/src/codex/inject/remove.ts +18 -6
  204. package/src/codex/inject/restore.ts +24 -2
  205. package/src/codex/inject/routing-classify.ts +4 -3
  206. package/src/codex/inject.ts +18 -27
  207. package/src/codex/journal.ts +16 -1
  208. package/src/codex/main-account-hard-lock.ts +30 -1
  209. package/src/codex/main-account.ts +99 -15
  210. package/src/codex/native-main-refresh-mode.ts +217 -0
  211. package/src/codex/native-profile-startup.ts +1 -1
  212. package/src/codex/ocx-compaction-history.ts +12 -10
  213. package/src/codex/paths.ts +86 -1
  214. package/src/codex/provider-routing.ts +174 -0
  215. package/src/codex/refresh.ts +2 -2
  216. package/src/codex/routing/health-store.ts +2 -2
  217. package/src/codex/routing/pool-recovery.ts +79 -0
  218. package/src/codex/routing/selection.ts +4 -3
  219. package/src/codex/routing/thread-affinity.ts +1 -1
  220. package/src/codex/shim-diagnostics.ts +6 -0
  221. package/src/codex/shim-overlay.ts +7 -2
  222. package/src/codex/signed-in-identity.ts +43 -0
  223. package/src/codex/subagent-model-fallback.ts +1 -1
  224. package/src/codex/sync.ts +6 -1
  225. package/src/codex/user-identity.ts +16 -5
  226. package/src/codex/windows-local-appdata.ts +84 -0
  227. package/src/combos/failover.ts +6 -1
  228. package/src/combos/index.ts +1 -0
  229. package/src/combos/resolve.ts +46 -10
  230. package/src/combos/types.ts +15 -0
  231. package/src/config/diagnostics.ts +35 -7
  232. package/src/config/live-reconcile.ts +51 -2
  233. package/src/config/load-degrade.ts +23 -1
  234. package/src/config/opencode-wire-upgrade.ts +54 -0
  235. package/src/config/opencode-zen-upgrade.ts +52 -0
  236. package/src/config/owner-registry.ts +22 -17
  237. package/src/config/paths.ts +10 -3
  238. package/src/config/provider-validation.ts +45 -2
  239. package/src/config/schema/config-schema.ts +39 -0
  240. package/src/config/schema/desktop-compatibility.ts +9 -0
  241. package/src/config/schema/leaf-validators.ts +39 -9
  242. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  243. package/src/config/schema/native-reasoning-retention.ts +26 -0
  244. package/src/config.ts +2 -0
  245. package/src/generated/compatibility-version.json +962 -350
  246. package/src/generated/model-metadata.ts +1 -1
  247. package/src/grok/sync.ts +1 -1
  248. package/src/images/loop.ts +46 -8
  249. package/src/integrations/catalog-refresh.ts +1 -1
  250. package/src/integrations/config-io.ts +20 -2
  251. package/src/integrations/journal.ts +15 -1
  252. package/src/integrations/mutation-plan.ts +2 -0
  253. package/src/integrations/registry.ts +15 -0
  254. package/src/integrations/store.ts +3 -0
  255. package/src/integrations/writer.ts +10 -1
  256. package/src/lib/advisor-activation.ts +16 -0
  257. package/src/lib/background-work.ts +65 -0
  258. package/src/lib/bounded-file-read.ts +58 -0
  259. package/src/lib/bun-path-runtime.d.mts +10 -1
  260. package/src/lib/bun-path-runtime.mjs +62 -3
  261. package/src/lib/codex-restart-contract.ts +15 -0
  262. package/src/lib/desktop-proxy-route.ts +30 -0
  263. package/src/lib/desktop-upstream-tunnel.ts +72 -0
  264. package/src/lib/empty-tool-catalog.ts +62 -0
  265. package/src/lib/errors.ts +26 -3
  266. package/src/lib/gcp-adc.ts +96 -9
  267. package/src/lib/gcp-credential-input.ts +12 -0
  268. package/src/lib/lab-live-route-production.ts +5 -0
  269. package/src/lib/local-internal-call-capability.ts +55 -0
  270. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  271. package/src/lib/package-tree-integrity.ts +2 -2
  272. package/src/lib/redact.ts +2 -2
  273. package/src/lib/socks5-fetch.ts +48 -71
  274. package/src/lib/socks5-handshake.ts +112 -0
  275. package/src/lib/translator-budget.ts +7 -1
  276. package/src/oauth/account-quota-rank.ts +69 -2
  277. package/src/oauth/anthropic-account-refusal.ts +20 -2
  278. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  279. package/src/oauth/anthropic-routing.ts +1 -21
  280. package/src/oauth/anthropic.ts +38 -7
  281. package/src/oauth/callback-server.ts +27 -1
  282. package/src/oauth/chatgpt-device.ts +1 -1
  283. package/src/oauth/chatgpt-shared.ts +83 -0
  284. package/src/oauth/chatgpt.ts +8 -72
  285. package/src/oauth/dsh.ts +203 -0
  286. package/src/oauth/generic-account-failover.ts +62 -12
  287. package/src/oauth/generic-session-affinity.ts +125 -0
  288. package/src/oauth/index.ts +188 -111
  289. package/src/oauth/kiro-device-login.ts +2 -1
  290. package/src/oauth/kiro.ts +7 -1
  291. package/src/oauth/local-token-detect.ts +21 -1
  292. package/src/oauth/login-cli-errors.ts +44 -0
  293. package/src/oauth/login-cli.ts +69 -7
  294. package/src/oauth/login-flow-state.ts +371 -23
  295. package/src/oauth/meta-muse-errors.ts +41 -0
  296. package/src/oauth/meta-muse-keychain.ts +52 -0
  297. package/src/oauth/meta-muse.ts +51 -84
  298. package/src/oauth/model-discovery-auth.ts +3 -0
  299. package/src/oauth/pool-settings-capability.ts +4 -2
  300. package/src/oauth/refresh-flight-retirement.ts +48 -0
  301. package/src/oauth/session-affinity-key.ts +23 -0
  302. package/src/oauth/store-anthropic-instance.ts +9 -2
  303. package/src/oauth/store.ts +12 -5
  304. package/src/protocols/codecs/chat-ir.ts +467 -0
  305. package/src/protocols/codecs/chat.ts +10 -1
  306. package/src/protocols/codecs/messages-ir.ts +500 -0
  307. package/src/protocols/encoders/adapter-events.ts +5 -4
  308. package/src/protocols/encoders/messages.ts +9 -6
  309. package/src/protocols/features.ts +10 -2
  310. package/src/protocols/path.ts +11 -1
  311. package/src/protocols/plan-snapshot.ts +39 -16
  312. package/src/protocols/plan.ts +9 -1
  313. package/src/protocols/provider-summary.ts +2 -1
  314. package/src/protocols/settings.ts +8 -0
  315. package/src/providers/antigravity-account-roster.ts +102 -0
  316. package/src/providers/antigravity-models.ts +85 -7
  317. package/src/providers/api-key-cooldowns.ts +49 -0
  318. package/src/providers/api-key-resolve.ts +11 -6
  319. package/src/providers/api-key-response-ownership.ts +57 -0
  320. package/src/providers/api-key-selection.ts +31 -6
  321. package/src/providers/api-keys.ts +214 -3
  322. package/src/providers/claude-cli-usage.ts +96 -0
  323. package/src/providers/dsh-account-destination.ts +31 -0
  324. package/src/providers/dsh-account-models.ts +41 -0
  325. package/src/providers/gcp-credential-cleanup.ts +59 -0
  326. package/src/providers/key-failover.ts +54 -50
  327. package/src/providers/key-store.ts +7 -2
  328. package/src/providers/model-rename-fields.ts +5 -0
  329. package/src/providers/model-rename-migration.ts +7 -5
  330. package/src/providers/openai-tiers-destination.ts +11 -2
  331. package/src/providers/openai-tiers.ts +5 -12
  332. package/src/providers/opencode-go-transport.ts +3 -1
  333. package/src/providers/quota/account-cache.ts +6 -5
  334. package/src/providers/quota/account-row.ts +34 -0
  335. package/src/providers/quota/antigravity.ts +28 -7
  336. package/src/providers/quota/dsh-account.ts +70 -0
  337. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  338. package/src/providers/quota-types.ts +2 -0
  339. package/src/providers/quota.ts +14 -20
  340. package/src/providers/registry/entries-core.ts +44 -14
  341. package/src/providers/registry/entries-extended.ts +19 -0
  342. package/src/providers/registry/types.ts +1 -1
  343. package/src/providers/request-pacing.ts +77 -19
  344. package/src/providers/routed-config.ts +339 -0
  345. package/src/providers/service-tier.ts +2 -1
  346. package/src/quota/window-mapping.ts +5 -2
  347. package/src/responses/anthropic-progress.ts +34 -0
  348. package/src/responses/assistant-replay.ts +35 -0
  349. package/src/responses/compaction-history-framing.ts +65 -0
  350. package/src/responses/compaction.ts +6 -1
  351. package/src/responses/incomplete-reason.ts +31 -0
  352. package/src/responses/parser.ts +78 -22
  353. package/src/responses/reasoning-envelope.ts +4 -0
  354. package/src/responses/reasoning-replay-cache.ts +24 -2
  355. package/src/responses/reasoning-retention.ts +173 -0
  356. package/src/responses/retained-compaction.ts +46 -0
  357. package/src/responses/schema.ts +23 -0
  358. package/src/responses/state.ts +32 -29
  359. package/src/router.ts +8 -321
  360. package/src/routing/health.ts +37 -22
  361. package/src/routing/history/build-file.ts +92 -0
  362. package/src/routing/history/indexer.ts +254 -221
  363. package/src/routing/history/ingest.ts +242 -0
  364. package/src/routing/history/rebuild-job.ts +111 -0
  365. package/src/routing/history/rebuild-protocol.ts +15 -0
  366. package/src/routing/history/rebuild-worker.ts +34 -0
  367. package/src/routing/profile.ts +1 -1
  368. package/src/server/admission-model-scope.ts +21 -1
  369. package/src/server/antigravity-direct-eligibility.ts +208 -0
  370. package/src/server/auth-cors.ts +15 -0
  371. package/src/server/chat-completions.ts +82 -10
  372. package/src/server/chat-native-reasoning.ts +123 -0
  373. package/src/server/chat-native-sse.ts +110 -7
  374. package/src/server/chat-native.ts +76 -17
  375. package/src/server/claude-messages.ts +164 -433
  376. package/src/server/effort-policy.ts +1 -1
  377. package/src/server/images.ts +15 -2
  378. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  379. package/src/server/index/model-alias-id.ts +36 -0
  380. package/src/server/index/optional-listeners.ts +2 -0
  381. package/src/server/index/serve-options.ts +11 -3
  382. package/src/server/index/startup-warnings.ts +11 -0
  383. package/src/server/index/websocket-handler.ts +9 -0
  384. package/src/server/index.ts +14 -11
  385. package/src/server/inference/advisor-diagnostics.ts +31 -0
  386. package/src/server/inference/client-encoder-delivery.ts +6 -5
  387. package/src/server/login-shell-path.ts +146 -0
  388. package/src/server/management/advisor-routes.ts +202 -0
  389. package/src/server/management/agent-settings-routes.ts +22 -4
  390. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  391. package/src/server/management/combo-routes.ts +176 -50
  392. package/src/server/management/companion-routes.ts +4 -0
  393. package/src/server/management/config-routes.ts +77 -6
  394. package/src/server/management/context.ts +4 -0
  395. package/src/server/management/cursor-integration-routes.ts +1 -1
  396. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  397. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  398. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  399. package/src/server/management/model-routes.ts +13 -8
  400. package/src/server/management/model-rows.ts +14 -0
  401. package/src/server/management/native-integration-routes.ts +2 -7
  402. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  403. package/src/server/management/oauth-account-routes.ts +87 -14
  404. package/src/server/management/protocol-settings-patch.ts +1 -0
  405. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  406. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  407. package/src/server/management/provider-overwrite-carry.ts +7 -0
  408. package/src/server/management/provider-routes.ts +25 -30
  409. package/src/server/management/route-registry.ts +21 -1
  410. package/src/server/management/sibling-guard.ts +1 -0
  411. package/src/server/management-api.ts +9 -1
  412. package/src/server/messages-antigravity-direct.ts +195 -0
  413. package/src/server/messages-native-tool-guard.ts +185 -0
  414. package/src/server/messages-native.ts +106 -22
  415. package/src/server/messages-shared.ts +431 -0
  416. package/src/server/relay.ts +1 -1
  417. package/src/server/request-log-terminal-status.ts +1 -1
  418. package/src/server/request-log.ts +13 -1
  419. package/src/server/responses/account-change-state.ts +8 -2
  420. package/src/server/responses/adapter-continuation.ts +48 -19
  421. package/src/server/responses/adapter-delivery.ts +43 -8
  422. package/src/server/responses/adapter-dispatch.ts +48 -4
  423. package/src/server/responses/advisor-plan-slot.ts +27 -0
  424. package/src/server/responses/advisor-retention.ts +117 -0
  425. package/src/server/responses/advisor-slot.ts +368 -0
  426. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  427. package/src/server/responses/codex-ws-wire.ts +15 -8
  428. package/src/server/responses/compact.ts +303 -125
  429. package/src/server/responses/compaction-recovery.ts +18 -10
  430. package/src/server/responses/compaction-retention.ts +61 -0
  431. package/src/server/responses/context-overflow.ts +11 -6
  432. package/src/server/responses/core-auth.ts +6 -0
  433. package/src/server/responses/core-codex-account.ts +11 -1
  434. package/src/server/responses/core-combo-failure.ts +12 -1
  435. package/src/server/responses/core-combo.ts +141 -36
  436. package/src/server/responses/core-errors.ts +3 -3
  437. package/src/server/responses/core-lifetime.ts +7 -2
  438. package/src/server/responses/core-normalize.ts +11 -0
  439. package/src/server/responses/core-opaque-recovery.ts +35 -0
  440. package/src/server/responses/core-options.ts +51 -0
  441. package/src/server/responses/core-pipeline.ts +141 -0
  442. package/src/server/responses/core-replay.ts +25 -6
  443. package/src/server/responses/core.ts +3 -116
  444. package/src/server/responses/fetch-helpers.ts +73 -19
  445. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  446. package/src/server/responses/native-response-control.ts +4 -2
  447. package/src/server/responses/passthrough-delivery.ts +19 -11
  448. package/src/server/responses/passthrough-dispatch.ts +29 -16
  449. package/src/server/responses/request-prepare.ts +144 -17
  450. package/src/server/responses/request-sidecar-auth.ts +2 -2
  451. package/src/server/responses/request-transport.ts +120 -27
  452. package/src/server/responses/response-effects.ts +1 -0
  453. package/src/server/responses/run-turn-execution.ts +166 -38
  454. package/src/server/responses/sidecar-execution.ts +98 -5
  455. package/src/server/responses/terminal-guard.ts +4 -17
  456. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  457. package/src/server/startup-action-control.ts +16 -2
  458. package/src/server/startup-health-cache.ts +1 -0
  459. package/src/service/auth-preflight.ts +71 -0
  460. package/src/service/cli.ts +4 -2
  461. package/src/service/definition.ts +97 -0
  462. package/src/service/desktop-startup.ts +14 -0
  463. package/src/service/guards.ts +1 -61
  464. package/src/service/health-sentinel-linux.ts +112 -0
  465. package/src/service/health-sentinel-main.ts +99 -61
  466. package/src/service/health-sentinel-probe.ts +24 -7
  467. package/src/service/health.ts +5 -89
  468. package/src/service/launchd.ts +27 -7
  469. package/src/service/logging.ts +135 -0
  470. package/src/service/repair.ts +2 -1
  471. package/src/service/state.ts +7 -8
  472. package/src/service/systemd.ts +1 -1
  473. package/src/service/windows-guarded-manager.ts +2 -1
  474. package/src/service/windows-ops.ts +1 -1
  475. package/src/service/windows-scheduler.ts +2 -7
  476. package/src/service/windows-taskxml.ts +2 -2
  477. package/src/service/windows-wscript.ts +14 -0
  478. package/src/service.ts +4 -2
  479. package/src/sidecar/auth.ts +2 -0
  480. package/src/tray/windows-tray.ps1 +43 -24
  481. package/src/types/config.ts +124 -6
  482. package/src/types/opencode-go.ts +61 -0
  483. package/src/types/opencode-zen.ts +29 -0
  484. package/src/types/provider.ts +39 -6
  485. package/src/types/request.ts +38 -2
  486. package/src/types/tools.ts +2 -0
  487. package/src/types/wire.ts +29 -37
  488. package/src/types.ts +1 -0
  489. package/src/update/async-check.ts +1 -1
  490. package/src/update/badge.ts +2 -2
  491. package/src/update/index.ts +88 -15
  492. package/src/update/job-supervised-restart.ts +107 -0
  493. package/src/update/job.ts +32 -32
  494. package/src/update/notify.ts +1 -1
  495. package/src/update/npm-invocation.mjs +12 -2
  496. package/src/update/refresh-scheduler.ts +2 -2
  497. package/src/update/supervised-recovery.d.mts +27 -0
  498. package/src/update/supervised-recovery.mjs +157 -0
  499. package/src/usage/cache-diagnostic.ts +2 -0
  500. package/src/usage/cost.ts +48 -12
  501. package/src/usage/expected-prices.ts +15 -0
  502. package/src/usage/ledger-retention.ts +2 -1
  503. package/src/usage/ledger-scanner.ts +6 -5
  504. package/src/usage/log.ts +17 -8
  505. package/src/usage/summary.ts +22 -9
  506. package/src/usage/user-cost-overlays.ts +14 -19
  507. package/src/usage/user-cost-validation.ts +38 -0
  508. package/src/vision/eligibility.ts +13 -0
  509. package/src/web-search/gemini-executor.ts +2 -2
  510. package/src/web-search/loop.ts +55 -5
  511. package/src/web-search/progress-stream.ts +4 -1
  512. package/src/web-search/run-turn-loop.ts +9 -1
  513. package/assets/sponsors/packycode.png +0 -0
  514. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  515. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  516. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  517. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  518. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Single-route opaque proxy tunnel. The desktop upstream dialer retains its
3
+ * own fixed-target TLS contract.
4
+ * Shares OpenCodex's MIT SOCKS handshake; the caller owns all post-establishment
5
+ * cancellation and socket cleanup. Never replay or implicitly retry directly.
6
+ */
7
+ import net, { type Socket } from 'node:net';
8
+ import tls from 'node:tls';
9
+ import { socks5Handshake, socks5Credentials, Socks5HandshakeError } from './socks5-handshake';
10
+
11
+ interface RouteEndpoint { host: string; port: number }
12
+ type RouteCredentials = { username?: undefined; password?: undefined } | { username: string; password: string };
13
+ type PlainProxy = { kind?: 'http'; proxyCa?: never };
14
+ type SecureProxy = { kind: 'https'; proxyCa?: string };
15
+ type SocksProxy = { kind: 'socks5' | 'socks5h'; proxyCa?: never };
16
+ export type OpaqueProxyRoute = RouteEndpoint & RouteCredentials & (PlainProxy | SecureProxy | SocksProxy);
17
+ export interface OpaqueTunnelOptions {
18
+ signal?: AbortSignal;
19
+ timeoutMs?: number;
20
+ maxHeaderBytes?: number;
21
+ }
22
+ interface AttemptOptions {
23
+ signal?: AbortSignal;
24
+ timeoutMs: number;
25
+ maxHeaderBytes: number;
26
+ }
27
+ export class TunnelError extends Error {
28
+ override readonly name = 'TunnelError';
29
+ readonly code: string;
30
+ readonly status?: number;
31
+ constructor(code: string, message: string, { status }: { status?: number } = {}) {
32
+ super(message); this.code = code; this.status = status;
33
+ }
34
+ }
35
+ const aborted = () => new TunnelError('ABORTED', 'Tunnel establishment cancelled');
36
+
37
+ function validateAuthority(authority: string): string {
38
+ if (typeof authority !== 'string' || !/^(?:\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):[0-9]+$/.test(authority)) {
39
+ throw new TunnelError('TARGET', 'An explicit host:port authority is required');
40
+ }
41
+ const port = Number(authority.slice(authority.lastIndexOf(':') + 1));
42
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new TunnelError('TARGET', 'Invalid target port');
43
+ return authority;
44
+ }
45
+
46
+ function validateOptions(p: OpaqueProxyRoute, timeoutMs: number, maxHeaderBytes: number): void {
47
+ if (!p || Array.isArray(p) || typeof p.host !== 'string' || !p.host || /[\s/@]/.test(p.host)
48
+ || !Number.isInteger(p.port) || p.port < 1 || p.port > 65535
49
+ || !['http', 'https', 'socks5', 'socks5h'].includes(p.kind ?? 'http')) throw new TunnelError('CONFIG', 'Invalid proxy route');
50
+ if ((p.username === undefined) !== (p.password === undefined)
51
+ || (p.username !== undefined && (typeof p.username !== 'string' || typeof p.password !== 'string'
52
+ || (!p.kind?.startsWith('socks') && p.username.includes(':')) || Buffer.byteLength(p.username + p.password) > 4096))) throw new TunnelError('CONFIG', 'Invalid proxy credentials');
53
+ if ((p.kind ?? 'http').startsWith('socks') && p.username !== undefined
54
+ && (Buffer.byteLength(p.username) > 255 || Buffer.byteLength(p.password) > 255)) throw new TunnelError('CONFIG', 'SOCKS credentials must each fit in 255 UTF-8 bytes');
55
+ if (p.proxyCa !== undefined && p.kind !== 'https') throw new TunnelError('CONFIG', 'Proxy TLS settings require HTTPS proxy');
56
+ if (p.proxyCa !== undefined && typeof p.proxyCa !== 'string') throw new TunnelError('CONFIG', 'Invalid proxy CA');
57
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000
58
+ || !Number.isSafeInteger(maxHeaderBytes) || maxHeaderBytes < 64 || maxHeaderBytes > 65536) {
59
+ throw new TunnelError('CONFIG', 'Invalid deadline or header limit');
60
+ }
61
+ }
62
+
63
+ function connectOne(authority: string, proxy: OpaqueProxyRoute, { signal, timeoutMs, maxHeaderBytes }: AttemptOptions): Promise<Socket> {
64
+ if (proxy.kind === 'socks5' || proxy.kind === 'socks5h') return connectSocks(authority, proxy, { signal, timeoutMs, maxHeaderBytes });
65
+ return new Promise<Socket>((resolve, reject) => {
66
+ if (signal?.aborted) return reject(aborted());
67
+ const secure = proxy.kind === 'https';
68
+ const identity = proxy.host;
69
+ const tlsOptions: tls.ConnectionOptions & { allowHalfOpen: boolean } = {
70
+ host: proxy.host, port: proxy.port, allowHalfOpen: true,
71
+ servername: net.isIP(identity) ? undefined : identity, ca: proxy.proxyCa,
72
+ checkServerIdentity: (_name, cert) => tls.checkServerIdentity(identity, cert),
73
+ rejectUnauthorized: true, ALPNProtocols: ['http/1.1'],
74
+ };
75
+ const socket = secure
76
+ ? tls.connect(tlsOptions)
77
+ : net.createConnection({ host: proxy.host, port: proxy.port, allowHalfOpen: true });
78
+ // Keep a defensive observer across the promise-to-owner handoff. Errors still
79
+ // settle the establishment promise or are handled by the relay after handoff.
80
+ socket.on('error', () => {});
81
+ let pending: Buffer = Buffer.alloc(0); let settled = false, tlsReady = !secure;
82
+ const connectedEvent = secure ? 'secureConnect' : 'connect';
83
+ const cleanup = () => {
84
+ clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
85
+ socket.removeListener(connectedEvent, onConnect); socket.removeListener('data', onData);
86
+ socket.removeListener('error', onError); socket.removeListener('end', onEnd); socket.removeListener('close', onClose);
87
+ };
88
+ const fail = (error: Error) => {
89
+ if (settled) return; settled = true; cleanup(); socket.destroy(); reject(error);
90
+ };
91
+ const onAbort = () => fail(aborted());
92
+ const establishmentFailure = (code: string, message: string) => fail(new TunnelError(code, message));
93
+ const onError = () => {
94
+ if (secure && !tlsReady) {
95
+ fail(new TunnelError('PROXY_TLS', 'HTTPS proxy establishment failed'));
96
+ } else establishmentFailure('TRANSPORT', 'HTTP proxy connection failed');
97
+ };
98
+ const onEnd = () => establishmentFailure('EARLY_CLOSE', 'HTTP proxy closed before establishing the tunnel');
99
+ const onClose = onEnd;
100
+ const onConnect = () => {
101
+ tlsReady = true;
102
+ const auth = proxy.username === undefined ? '' : `Proxy-Authorization: Basic ${Buffer.from(`${proxy.username}:${proxy.password}`).toString('base64')}\r\n`;
103
+ socket.write(`CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${auth}\r\n`);
104
+ };
105
+ const onData = (chunk: Buffer) => {
106
+ pending = Buffer.concat([pending, chunk]);
107
+ const lineEnd = pending.indexOf('\r\n');
108
+ if (lineEnd >= 0) {
109
+ const firstLine = pending.subarray(0, lineEnd).toString('latin1');
110
+ const firstStatus = /^HTTP\/1\.[01] ([0-9]{3})(?: [\t\x20-\x7e\x80-\xff]*)?$/.exec(firstLine);
111
+ if (!firstStatus) return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy status line'));
112
+ const status = Number(firstStatus[1]);
113
+ if (status < 200 || status >= 300) return fail(new TunnelError('REFUSED', 'HTTP proxy refused CONNECT', { status }));
114
+ }
115
+ const end = pending.indexOf('\r\n\r\n');
116
+ if (end < 0) {
117
+ if (pending.length > maxHeaderBytes) fail(new TunnelError('PROTOCOL', 'HTTP proxy response headers exceed limit'));
118
+ return;
119
+ }
120
+ if (end + 4 > maxHeaderBytes) return fail(new TunnelError('PROTOCOL', 'HTTP proxy response headers exceed limit'));
121
+ const lines = pending.subarray(0, end).toString('latin1').split('\r\n');
122
+ const statusLine = lines[0];
123
+ const match = /^HTTP\/1\.[01] ([0-9]{3})(?: [^\r\n]*)?$/.exec(statusLine);
124
+ if (!match) return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy status line'));
125
+ const status = Number(match[1]);
126
+ if (lines.slice(1).some(line => !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+:[\t\x20-\x7e\x80-\xff]*$/.test(line))) {
127
+ return fail(new TunnelError('PROTOCOL', 'Invalid HTTP proxy response header'));
128
+ }
129
+ if (status < 200 || status >= 300) return fail(new TunnelError('REFUSED', 'HTTP proxy refused CONNECT', { status }));
130
+ settled = true; socket.pause(); cleanup();
131
+ const head = pending.subarray(end + 4);
132
+ if (head.length) socket.unshift(head);
133
+ resolve(socket);
134
+ };
135
+ const timer = setTimeout(() => establishmentFailure('TIMEOUT', 'HTTP proxy establishment deadline exceeded'), timeoutMs);
136
+ socket.once(connectedEvent, onConnect); socket.on('data', onData);
137
+ socket.once('error', onError); socket.once('end', onEnd); socket.once('close', onClose);
138
+ signal?.addEventListener('abort', onAbort, { once: true });
139
+ if (signal?.aborted) onAbort();
140
+ });
141
+ }
142
+
143
+ // The bounded socket reader owns setup cancellation and preserves queued bytes.
144
+ // Framing uses the existing
145
+ // MIT socks5-handshake module; neither shared framing nor its DNS policy changes.
146
+ async function connectSocks(authority: string, proxy: OpaqueProxyRoute, { signal, timeoutMs }: AttemptOptions): Promise<Socket> {
147
+ if (signal?.aborted) throw aborted();
148
+ const socket = net.createConnection({ host: proxy.host, port: proxy.port, allowHalfOpen: true });
149
+ socket.on('error', () => {});
150
+ let buffer: Buffer = Buffer.alloc(0), failure: Error | undefined;
151
+ let waiting: { length: number; resolve: () => void; reject: (error: Error) => void } | undefined;
152
+ const fail = (error: Error) => { if (failure) return; failure = error; const current = waiting; waiting = undefined; current?.reject(error); socket.destroy(); };
153
+ const transportError = (code: string, message: string) => fail(new TunnelError(code, message));
154
+ const onData = (chunk: Buffer) => {
155
+ // Consume at most the bytes needed by a bounded handshake read, then pause.
156
+ // A valid CONNECT reply can share a packet with arbitrary early target data;
157
+ // that remainder is not a malformed/oversized handshake and must survive.
158
+ socket.pause();
159
+ buffer = Buffer.concat([buffer, chunk]);
160
+ if (waiting && buffer.length >= waiting.length) { const current = waiting; waiting = undefined; current.resolve(); }
161
+ else if (waiting) socket.resume();
162
+ };
163
+ const onError = () => transportError('TRANSPORT', 'SOCKS proxy connection failed');
164
+ const onClose = () => transportError('EARLY_CLOSE', 'SOCKS proxy closed before tunnel establishment');
165
+ const onAbort = () => fail(aborted());
166
+ const timer = setTimeout(() => transportError('TIMEOUT', 'SOCKS establishment deadline exceeded'), timeoutMs);
167
+ socket.on('data', onData); socket.once('error', onError); socket.once('end', onClose); socket.once('close', onClose);
168
+ socket.pause();
169
+ signal?.addEventListener('abort', onAbort, { once: true }); if (signal?.aborted) onAbort();
170
+ const reader = {
171
+ write(bytes: Uint8Array): void { if (failure) throw failure; socket.write(bytes); },
172
+ async readExact(length: number): Promise<Buffer> {
173
+ if (failure) throw failure;
174
+ if (!Number.isSafeInteger(length) || length < 0 || length > 1024) throw new TunnelError('PROTOCOL', 'Invalid handshake read size');
175
+ if (buffer.length < length) await new Promise<void>((resolve, reject) => {
176
+ if (waiting) { reject(new TunnelError('PROTOCOL', 'Concurrent handshake read')); return; }
177
+ waiting = { length, resolve, reject };
178
+ socket.resume();
179
+ });
180
+ if (failure) throw failure;
181
+ const result = buffer.subarray(0, length); buffer = buffer.subarray(length); return result;
182
+ },
183
+ };
184
+ try {
185
+ const credentialsUrl = new URL('socks5://fixture.invalid');
186
+ if (proxy.username !== undefined) { credentialsUrl.username = encodeURIComponent(proxy.username); credentialsUrl.password = encodeURIComponent(proxy.password); }
187
+ const colon = authority.lastIndexOf(':');
188
+ const target = { host: authority.slice(0, colon).replace(/^\[|\]$/g, ''), port: Number(authority.slice(colon + 1)) };
189
+ await socks5Handshake(reader, target, socks5Credentials(credentialsUrl), signal);
190
+ if (failure) throw failure;
191
+ if (signal?.aborted || socket.destroyed) throw aborted();
192
+ socket.pause(); if (buffer.length) socket.unshift(buffer); buffer = Buffer.alloc(0);
193
+ return socket;
194
+ } catch (error) {
195
+ socket.destroy();
196
+ if (error instanceof Socks5HandshakeError) throw new TunnelError('SOCKS_REFUSED', 'SOCKS authentication, policy, or protocol refusal');
197
+ throw error;
198
+ } finally {
199
+ clearTimeout(timer); signal?.removeEventListener('abort', onAbort);
200
+ socket.removeListener('data', onData); socket.removeListener('error', onError);
201
+ socket.removeListener('end', onClose); socket.removeListener('close', onClose);
202
+ }
203
+ }
204
+
205
+ /** Establish only. There is no application data, implicit DIRECT, or replay here.
206
+ * HTTP/HTTPS CONNECT and SOCKS5/SOCKS5h yield an opaque raw target tunnel.
207
+ */
208
+ export async function dialTunnel(authority: string, proxy: OpaqueProxyRoute, options: OpaqueTunnelOptions = {}): Promise<Socket> {
209
+ const { signal, timeoutMs = 10000, maxHeaderBytes = 65536 } = options;
210
+ validateAuthority(authority);
211
+ validateOptions(proxy, timeoutMs, maxHeaderBytes);
212
+ if (signal?.aborted) throw aborted();
213
+ // Snapshot the selected route. No alternate proxy or direct fallback exists.
214
+ const socket = await connectOne(authority, { ...proxy }, { signal, timeoutMs, maxHeaderBytes });
215
+ if (signal?.aborted || socket.destroyed) { socket.destroy(); throw aborted(); }
216
+ return socket;
217
+ }
@@ -68,7 +68,7 @@ export interface PackageTreeIntegrityOptions {
68
68
  }
69
69
 
70
70
  export type ObservePackageTree = () => PackageTreeObservation | null;
71
- export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source";
71
+ export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source" | "standalone";
72
72
 
73
73
  const packageManifestUrl = new URL("../../package.json", import.meta.url);
74
74
 
@@ -323,7 +323,7 @@ export function createRuntimePackageTreeIntegrityGuard(
323
323
  now: () => number = Date.now,
324
324
  options: PackageTreeIntegrityOptions = {},
325
325
  ): PackageTreeIntegrityGuard {
326
- if (installer === "source" || isStandaloneBinary()) {
326
+ if (installer === "source" || installer === "standalone" || isStandaloneBinary()) {
327
327
  return { status: () => ({ ok: true }), dispose: () => {} };
328
328
  }
329
329
  // The restart replacement spawns process.execPath, the bundled Bun in an npm install. Read it
package/src/lib/redact.ts CHANGED
@@ -7,7 +7,7 @@ export const REDACTED_SECRET = "[REDACTED]";
7
7
  * credentials over an unsafe channel (e.g. plaintext non-loopback HTTP) rather than
8
8
  * re-deriving a narrower local list.
9
9
  */
10
- export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
10
+ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|x-dsh-auth-token|copilot[-_]?session[-_]?token|session[-_]?token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn|exa[-_]?api[-_]?key)$/i;
11
11
 
12
12
  /**
13
13
  * Colon-labelled credential headers echoed back inside an error body
@@ -40,7 +40,7 @@ export const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cook
40
40
  // Every letter position also accepts \u0001, the placeholder the fold emits for
41
41
  // an unresolved HTML named reference: `author&ii;zation` is the label with one
42
42
  // character we cannot name, and that is still the label.
43
- const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
43
+ const CREDENTIAL_HEADER_LABEL_RAW = "copilot[_-]?session[_-]?token|session[_-]?token|sessionToken|x-api-key|x-goog-api-key|x-amz-security-token|x-dsh-auth-token|api[_-]?key|apiKey|exa[_-]?api[_-]?key|exaApiKey|access[_-]?token|accessToken|refresh[_-]?token|refreshToken|id[_-]?token|client[_-]?secret|clientSecret|authorization|proxy-authorization|cookie|set-cookie|password|secret|token";
44
44
 
45
45
  const CREDENTIAL_HEADER_LABEL = CREDENTIAL_HEADER_LABEL_RAW
46
46
  .replace(/(?<![\[\\])([A-Za-z])(?![\]\-])/g, "[$1\u0001]");
@@ -1,6 +1,7 @@
1
1
  import net, { type Socket } from "node:net";
2
2
  import tls, { type TLSSocket } from "node:tls";
3
3
  import { classifyContentCoding, isNullBodyStatus } from "./http-response-semantics";
4
+ import { socks5Credentials, socks5Handshake, Socks5HandshakeError } from "./socks5-handshake";
4
5
 
5
6
  const DEFAULT_SOCKS5_PORT = 1080;
6
7
  const SOCKS5_CONNECT_TIMEOUT_MS = 30_000;
@@ -10,11 +11,6 @@ const MAX_BODY_SLICE_BYTES = 64 * 1024;
10
11
  const MAX_DECODED_BODY_BYTES = 32 * 1024 * 1024;
11
12
  const MAX_STREAM_DECODE_EXPANSION_RATIO = 128;
12
13
  const SOCKS5_VERSION = 0x05;
13
- const SOCKS5_NO_AUTH = 0x00;
14
- const SOCKS5_USER_PASS = 0x02;
15
- const SOCKS5_CONNECT = 0x01;
16
- const SOCKS5_DOMAIN = 0x03;
17
- const SOCKS5_SUCCESS = 0x00;
18
14
  const CRLF = Buffer.from("\r\n");
19
15
  const HEADER_END = Buffer.from("\r\n\r\n");
20
16
 
@@ -22,24 +18,6 @@ export class Socks5FetchError extends Error {
22
18
  override readonly name = "Socks5FetchError";
23
19
  }
24
20
 
25
- function proxyCredentials(proxy: URL): { username?: Uint8Array; password?: Uint8Array } {
26
- if (!proxy.username && !proxy.password) return {};
27
- let username: string;
28
- let password: string;
29
- try {
30
- username = decodeURIComponent(proxy.username);
31
- password = decodeURIComponent(proxy.password);
32
- } catch {
33
- throw new Socks5FetchError("SOCKS5 proxy credentials contain invalid percent encoding");
34
- }
35
- const usernameBytes = new TextEncoder().encode(username);
36
- const passwordBytes = new TextEncoder().encode(password);
37
- if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
38
- throw new Socks5FetchError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
39
- }
40
- return { username: usernameBytes, password: passwordBytes };
41
- }
42
-
43
21
  function validateProxy(proxy: string): URL {
44
22
  let parsed: URL;
45
23
  try {
@@ -55,7 +33,10 @@ function validateProxy(proxy: string): URL {
55
33
  throw new Socks5FetchError("SOCKS5 proxy port is invalid");
56
34
  }
57
35
  if (parsed.search || parsed.hash) throw new Socks5FetchError("SOCKS5 proxy URL must not contain a query or fragment");
58
- proxyCredentials(parsed);
36
+ try { socks5Credentials(parsed); } catch (error) {
37
+ if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
38
+ throw error;
39
+ }
59
40
  return parsed;
60
41
  }
61
42
 
@@ -130,6 +111,14 @@ class SocketReader {
130
111
  socket.resume();
131
112
  }
132
113
 
114
+ write(bytes: Uint8Array): void {
115
+ this.socket.write(bytes);
116
+ }
117
+
118
+ readExact(bytes: number, signal?: AbortSignal): Promise<Buffer> {
119
+ return this.read(bytes, signal);
120
+ }
121
+
133
122
  private readonly onData = (chunk: Buffer | string): void => {
134
123
  const value = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
135
124
  if (this.anyWaiters.length > 0) {
@@ -263,7 +252,7 @@ class SocketReader {
263
252
 
264
253
  async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal): Promise<Socket> {
265
254
  const parsedProxy = validateProxy(proxy);
266
- const credentials = proxyCredentials(parsedProxy);
255
+ const credentials = socks5Credentials(parsedProxy);
267
256
  const proxyHost = parsedProxy.hostname.replace(/^\[|\]$/g, "");
268
257
  const socket = await connectSocket(proxyHost, Number(parsedProxy.port) || DEFAULT_SOCKS5_PORT, signal);
269
258
  socket.setTimeout(SOCKS5_CONNECT_TIMEOUT_MS, () => {
@@ -271,49 +260,12 @@ async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal):
271
260
  });
272
261
  const reader = new SocketReader(socket);
273
262
  try {
274
- const methods = credentials.username ? Buffer.from([SOCKS5_NO_AUTH, SOCKS5_USER_PASS]) : Buffer.from([SOCKS5_NO_AUTH]);
275
- socket.write(Buffer.from([SOCKS5_VERSION, methods.byteLength, ...methods]));
276
- const greeting = await reader.read(2, signal);
277
- if (greeting[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid greeting");
278
- if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
279
- socket.write(Buffer.from([
280
- 0x01,
281
- credentials.username.byteLength,
282
- ...credentials.username,
283
- credentials.password.byteLength,
284
- ...credentials.password,
285
- ]));
286
- const auth = await reader.read(2, signal);
287
- if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5FetchError("SOCKS5 proxy authentication failed");
288
- } else if (greeting[1] !== SOCKS5_NO_AUTH) {
289
- throw new Socks5FetchError("SOCKS5 proxy does not accept an offered authentication method");
290
- }
291
-
292
- const hostname = new TextEncoder().encode(target.hostname);
293
- if (hostname.byteLength > 255) throw new Socks5FetchError("SOCKS5 target hostname is too long");
294
- const port = targetPort(target);
295
- socket.write(Buffer.from([
296
- SOCKS5_VERSION,
297
- SOCKS5_CONNECT,
298
- 0x00,
299
- SOCKS5_DOMAIN,
300
- hostname.byteLength,
301
- ...hostname,
302
- port >> 8,
303
- port & 0xff,
304
- ]));
305
- const reply = await reader.read(4, signal);
306
- if (reply[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid connect response");
307
- if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5FetchError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
308
- if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
309
- throw new Socks5FetchError("SOCKS5 proxy returned an invalid address type or reserved byte");
310
- }
311
- const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.read(1, signal))[0]! : 16;
312
- await reader.read(addressLength + 2, signal);
263
+ await socks5Handshake(reader, { host: target.hostname, port: targetPort(target) }, credentials, signal);
313
264
  socket.setTimeout(0);
314
265
  return socket;
315
266
  } catch (error) {
316
267
  socket.destroy();
268
+ if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
317
269
  throw error;
318
270
  } finally {
319
271
  reader.dispose();
@@ -341,25 +293,50 @@ function requestHeaders(request: Request, target: URL): { text: string; chunked:
341
293
 
342
294
  async function secureSocket(socket: Socket, target: URL, signal: AbortSignal): Promise<TLSSocket> {
343
295
  return new Promise((resolve, reject) => {
296
+ if (signal.aborted) {
297
+ socket.destroy();
298
+ reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
299
+ return;
300
+ }
344
301
  const tlsSocket = tls.connect({
345
302
  socket,
346
303
  servername: target.hostname,
347
304
  rejectUnauthorized: true,
348
305
  });
349
- const onAbort = () => {
306
+ let settled = false;
307
+ // Absolute deadline: a peer sending occasional TLS bytes must not extend the handshake.
308
+ const timer = setTimeout(() => {
309
+ fail(new Socks5FetchError("SOCKS5 TLS handshake timed out"));
310
+ }, SOCKS5_CONNECT_TIMEOUT_MS);
311
+ const cleanup = () => {
312
+ clearTimeout(timer);
313
+ signal.removeEventListener("abort", onAbort);
314
+ tlsSocket.removeListener("secureConnect", onSecureConnect);
315
+ tlsSocket.removeListener("error", onError);
316
+ tlsSocket.removeListener("close", onClose);
317
+ };
318
+ const fail = (error: Error) => {
319
+ if (settled) return;
320
+ settled = true;
321
+ cleanup();
322
+ // No error argument: pending event observers must not receive a second error.
350
323
  tlsSocket.destroy();
351
- reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
324
+ reject(error);
352
325
  };
326
+ const onAbort = () => fail(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
327
+ const onError = (error: Error) => fail(error);
328
+ const onClose = () => fail(new Socks5FetchError("SOCKS5 upstream closed during TLS handshake"));
353
329
  const onSecureConnect = () => {
354
- signal.removeEventListener("abort", onAbort);
330
+ if (settled) return;
331
+ settled = true;
332
+ cleanup();
355
333
  resolve(tlsSocket);
356
334
  };
357
335
  tlsSocket.once("secureConnect", onSecureConnect);
358
- tlsSocket.once("error", error => {
359
- signal.removeEventListener("abort", onAbort);
360
- reject(error);
361
- });
336
+ tlsSocket.once("error", onError);
337
+ tlsSocket.once("close", onClose);
362
338
  signal.addEventListener("abort", onAbort, { once: true });
339
+ if (signal.aborted) onAbort();
363
340
  });
364
341
  }
365
342
 
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The SOCKS5 handshake both raw outbound transports share: method negotiation
3
+ * (RFC 1928), the optional RFC 1929 username/password subnegotiation, and the
4
+ * CONNECT exchange.
5
+ *
6
+ * `src/lib/socks5-fetch.ts` (the HTTP tunnel) and
7
+ * `src/lib/desktop-upstream-tunnel.ts` (the native desktop relay's raw
8
+ * dial) each own their socket, their timeouts, and what happens after CONNECT;
9
+ * this module only frames bytes through the reader each hands it, so a
10
+ * credentialed `socks5://` proxy URL authenticates identically whether the caller
11
+ * is a fetch or a raw WebSocket upgrade. When the URL carries credentials the
12
+ * greeting offers NO-AUTH alongside USER-PASS, which keeps an unauthenticated
13
+ * proxy working for a caller that configured more than it needed.
14
+ */
15
+
16
+ export class Socks5HandshakeError extends Error {
17
+ override readonly name = "Socks5HandshakeError";
18
+ }
19
+
20
+ const SOCKS5_VERSION = 0x05;
21
+ const SOCKS5_NO_AUTH = 0x00;
22
+ const SOCKS5_USER_PASS = 0x02;
23
+ const SOCKS5_CONNECT = 0x01;
24
+ const SOCKS5_DOMAIN = 0x03;
25
+ const SOCKS5_SUCCESS = 0x00;
26
+
27
+ /** RFC 1929 credentials decoded from the proxy URL, each bounded to one length byte. */
28
+ export interface Socks5Credentials {
29
+ username?: Uint8Array;
30
+ password?: Uint8Array;
31
+ }
32
+
33
+ export function socks5Credentials(proxy: URL): Socks5Credentials {
34
+ if (!proxy.username && !proxy.password) return {};
35
+ let username: string;
36
+ let password: string;
37
+ try {
38
+ username = decodeURIComponent(proxy.username);
39
+ password = decodeURIComponent(proxy.password);
40
+ } catch {
41
+ throw new Socks5HandshakeError("SOCKS5 proxy credentials contain invalid percent encoding");
42
+ }
43
+ const usernameBytes = new TextEncoder().encode(username);
44
+ const passwordBytes = new TextEncoder().encode(password);
45
+ if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
46
+ throw new Socks5HandshakeError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
47
+ }
48
+ return { username: usernameBytes, password: passwordBytes };
49
+ }
50
+
51
+ /**
52
+ * The slice of a transport's socket reader the handshake needs: exact-length reads
53
+ * that reject on close, on error, or on the transport's own timeout, plus writes.
54
+ * Bytes read past a step stay queued in the transport's reader; the handshake never
55
+ * touches the socket itself.
56
+ */
57
+ export interface Socks5HandshakeReader {
58
+ write(bytes: Uint8Array): void;
59
+ readExact(bytes: number, signal?: AbortSignal): Promise<Uint8Array>;
60
+ }
61
+
62
+ export interface Socks5Target {
63
+ host: string;
64
+ port: number;
65
+ }
66
+
67
+ /** Negotiate methods, authenticate when the proxy picks USER-PASS, and CONNECT to `target`. */
68
+ export async function socks5Handshake(
69
+ reader: Socks5HandshakeReader,
70
+ target: Socks5Target,
71
+ credentials: Socks5Credentials,
72
+ signal?: AbortSignal,
73
+ ): Promise<void> {
74
+ const hostBytes = Buffer.from(target.host, "utf8");
75
+ if (hostBytes.byteLength > 255) throw new Socks5HandshakeError("SOCKS5 target hostname is too long");
76
+ const methods = credentials.username ? [SOCKS5_NO_AUTH, SOCKS5_USER_PASS] : [SOCKS5_NO_AUTH];
77
+ reader.write(Buffer.from([SOCKS5_VERSION, methods.length, ...methods]));
78
+ const greeting = await reader.readExact(2, signal);
79
+ if (greeting[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid greeting");
80
+ if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
81
+ reader.write(Buffer.from([
82
+ 0x01,
83
+ credentials.username.byteLength,
84
+ ...credentials.username,
85
+ credentials.password.byteLength,
86
+ ...credentials.password,
87
+ ]));
88
+ const auth = await reader.readExact(2, signal);
89
+ if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5HandshakeError("SOCKS5 proxy authentication failed");
90
+ } else if (greeting[1] !== SOCKS5_NO_AUTH) {
91
+ throw new Socks5HandshakeError("SOCKS5 proxy does not accept an offered authentication method");
92
+ }
93
+
94
+ reader.write(Buffer.from([
95
+ SOCKS5_VERSION,
96
+ SOCKS5_CONNECT,
97
+ 0x00,
98
+ SOCKS5_DOMAIN,
99
+ hostBytes.byteLength,
100
+ ...hostBytes,
101
+ target.port >> 8,
102
+ target.port & 0xff,
103
+ ]));
104
+ const reply = await reader.readExact(4, signal);
105
+ if (reply[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid connect response");
106
+ if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5HandshakeError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
107
+ if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
108
+ throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid address type or reserved byte");
109
+ }
110
+ const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.readExact(1, signal))[0]! : 16;
111
+ await reader.readExact(addressLength + 2, signal);
112
+ }
@@ -1,3 +1,5 @@
1
+ import { isNonReplayableResponse, markResponseNonReplayable } from "./upstream-retry";
2
+
1
3
  export const TRANSLATOR_MAX_CALL_ARGUMENT_BYTES = 2 * 1024 * 1024;
2
4
  export const TRANSLATOR_MAX_TURN_BYTES = 32 * 1024 * 1024;
3
5
  export const TRANSLATOR_MAX_SSE_EVENT_BYTES = 32 * 1024 * 1024;
@@ -410,5 +412,9 @@ export function finalizeTranslatorBudgetResponse(response: Response, budget: Tra
410
412
  });
411
413
  if (signal?.aborted) finalize();
412
414
  else signal?.addEventListener("abort", finalize, { once: true });
413
- return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
415
+ const finalizedResponse = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
416
+ // The rebuild above drops identity markers. A refusal that must not be replayed
417
+ // stays non-replayable through accounting finalization.
418
+ if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
419
+ return finalizedResponse;
414
420
  }
@@ -96,7 +96,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
96
96
  const family = classifyModelFamilyForQuota(provider, requestedModelId);
97
97
  if (family) {
98
98
  const percents = (quota.customWindows ?? [])
99
- .filter(window => windowMatchesFamily(window.label, family))
99
+ .filter(window => window.modelId === undefined && windowMatchesFamily(window.label, family))
100
100
  .map(window => window.percent)
101
101
  .filter((value): value is number => typeof value === "number");
102
102
  if (percents.length === 0) return null;
@@ -106,7 +106,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri
106
106
  quota.fiveHourPercent,
107
107
  quota.weeklyPercent,
108
108
  quota.monthlyPercent,
109
- ...(quota.customWindows ?? []).map(window => window.percent),
109
+ ...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.percent),
110
110
  ].filter((value): value is number => typeof value === "number");
111
111
  if (percents.length === 0) return null;
112
112
  return 100 - Math.max(...percents);
@@ -140,6 +140,73 @@ export function isAccountQuotaExhausted(
140
140
  return headroom !== null && headroom <= 0;
141
141
  }
142
142
 
143
+ /**
144
+ * Earliest future reset timestamp (in ms) for the relevant quota window, or null if unknown.
145
+ * Prefers weekly reset when multiple windows exist so expiring allowance is consumed first.
146
+ */
147
+ export function accountResetTimestamp(
148
+ provider: string,
149
+ accountId: string,
150
+ requestedModelId?: string | null,
151
+ now = Date.now(),
152
+ ): number | null {
153
+ const quota = getCachedProviderAccountQuota(provider, accountId);
154
+ if (!quota) return null;
155
+ const family = classifyModelFamilyForQuota(provider, requestedModelId);
156
+ const windows = (quota.customWindows ?? []).filter(w => !family || windowMatchesFamily(w.label, family));
157
+ const weekly = windows.find(w => /week/i.test(w.label));
158
+ let rawReset = weekly?.resetAt ?? quota.weeklyResetAt;
159
+ if (rawReset === undefined && provider !== "google-antigravity" && provider !== "anthropic") {
160
+ // Providers without a weekly cadence: prefer an explicitly 5h-labeled window before
161
+ // falling back to first-window order.
162
+ const fiveHour = windows.find(w => /5h|five/i.test(w.label)) ?? windows[0];
163
+ rawReset = fiveHour?.resetAt ?? quota.fiveHourResetAt;
164
+ } else if (rawReset === undefined) {
165
+ rawReset = windows[0]?.resetAt ?? quota.fiveHourResetAt;
166
+ }
167
+ if (typeof rawReset !== "number" || !Number.isFinite(rawReset)) return null;
168
+ const ms = rawReset < 1e11 ? rawReset * 1000 : rawReset;
169
+ return ms > now ? ms : null;
170
+ }
171
+
172
+ /**
173
+ * Rank candidates by soonest reset timestamp first among healthy accounts.
174
+ * Accounts whose quota will reset earliest get priority so allowances do not expire unused.
175
+ *
176
+ * When a passive provider has incomplete quota evidence (some ring members with no
177
+ * cached record), reset timestamps are unreliable: an unknown account converts to
178
+ * Infinity and always loses to a known near-exhausted one. Fall back to headroom
179
+ * order instead of inventing a reset ordering.
180
+ */
181
+ export function rankAccountsByResetFirst(
182
+ provider: string,
183
+ ring: readonly string[],
184
+ requestedModelId?: string | null,
185
+ now = Date.now(),
186
+ accounts?: ReadonlyMap<string, ProviderAccount>,
187
+ ): string[] {
188
+ if (ring.length < 2) return [...ring];
189
+ const ranked = rankAccountsByHeadroom(provider, ring, requestedModelId, accounts);
190
+ const healthy = ranked.filter(id => !isAccountQuotaExhausted(provider, id, requestedModelId, accounts?.get(id)));
191
+ if (healthy.length < 2) return ranked;
192
+ if (
193
+ hasPassiveAccountQuota(provider)
194
+ && !ring.every(id => headroomOf(provider, id, requestedModelId, accounts?.get(id)) !== null)
195
+ ) return ranked;
196
+
197
+ const withReset = healthy.map((id, index) => ({
198
+ id,
199
+ resetAt: accountResetTimestamp(provider, id, requestedModelId, now) ?? Number.POSITIVE_INFINITY,
200
+ headroom: headroomOf(provider, id, requestedModelId, accounts?.get(id)) ?? 0,
201
+ index,
202
+ }));
203
+
204
+ withReset.sort((a, b) => (a.resetAt - b.resetAt) || (b.headroom - a.headroom) || (a.index - b.index));
205
+ const sortedHealthy = withReset.map(r => r.id);
206
+ const exhausted = ranked.filter(id => isAccountQuotaExhausted(provider, id, requestedModelId, accounts?.get(id)));
207
+ return [...sortedHealthy, ...exhausted];
208
+ }
209
+
143
210
  /**
144
211
  * Order candidates best-first.
145
212
  *