@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,93 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { isLoopbackHostname, shouldInjectApiAuthHeader } from "../../codex/loopback-target";
4
+ import { formatSelectorConjunction } from "../../integrations/merge";
5
+ import { ClientPathError } from "../config-export";
6
+ import type { ExportContext, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
7
+ import { LOOPBACK_API_KEY_PLACEHOLDER, OPENCODE_PROVIDER_ID } from "./constants";
8
+ import { authoritativeContextWindow, normalizeExportModels } from "./model-metadata";
9
+
10
+ /**
11
+ * Qoder's configuration directory: `QODER_CONFIG_DIR` when set, otherwise `~/.qoder`
12
+ * (https://docs.qoder.com/cli/settings-reference). Export and detection both resolve it here.
13
+ * A relative override is refused because opencodex and Qoder would anchor it to different cwds.
14
+ */
15
+ export function qoderHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
16
+ const override = env.QODER_CONFIG_DIR?.trim();
17
+ if (!override) return join(home, ".qoder");
18
+ if (override === "~") return home;
19
+ if (override.startsWith("~/") || override.startsWith("~\\")) return join(home, override.slice(2));
20
+ if (!isAbsolute(override)) {
21
+ throw new ClientPathError(
22
+ `QODER_CONFIG_DIR must be an absolute path or start with ~; "${override}" depends on the working directory, `
23
+ + "so opencodex and Qoder would disagree about which file it names.",
24
+ );
25
+ }
26
+ return override;
27
+ }
28
+
29
+ export function qoderConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
30
+ return join(qoderHomeDir(env, home), "settings.json");
31
+ }
32
+
33
+ /** Qoder IDE and qodercli share this provider and custom-model catalog. */
34
+ export function buildQoderClientConfig(ctx: ExportContext) {
35
+ if (shouldInjectApiAuthHeader(ctx.config) || !isLoopbackHostname(new URL(ctx.baseUrl).hostname)) {
36
+ throw new Error("Qoder's placeholder API key requires a loopback-only listener.");
37
+ }
38
+ return qoderDocument(ctx);
39
+ }
40
+
41
+ function qoderDocument(ctx: ExportContext) {
42
+ const models = normalizeExportModels(ctx.models);
43
+ return {
44
+ providers: {
45
+ [OPENCODE_PROVIDER_ID]: {
46
+ type: "openai-compatible",
47
+ protocol: "openai",
48
+ displayName: "opencodex",
49
+ baseUrl: ctx.baseUrl,
50
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
51
+ ...(models[0] ? { defaultModel: models[0].namespaced } : {}),
52
+ models: models.map(model => ({ model: model.namespaced })),
53
+ },
54
+ },
55
+ modelConfigs: {
56
+ customModels: models.map(model => ({
57
+ provider: OPENCODE_PROVIDER_ID,
58
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
59
+ model: model.namespaced,
60
+ baseURL: ctx.baseUrl,
61
+ displayName: model.namespaced,
62
+ maxInputTokens: authoritativeContextWindow(model.contextWindow) ?? 128000,
63
+ })),
64
+ },
65
+ };
66
+ }
67
+
68
+ export type QoderGeneratedConfig = ReturnType<typeof buildQoderClientConfig>;
69
+
70
+ export function summarizeQoder(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
71
+ const models = (document as QoderGeneratedConfig | undefined)?.modelConfigs?.customModels ?? [];
72
+ return { modelCount: models.length, modelsWithoutLimits: 0 };
73
+ }
74
+
75
+ export function buildQoderContribution(ctx: ExportContext): ManagedContribution {
76
+ // The writer gates admission before applying. Classification and disable must
77
+ // still work after the operator changes the proxy to a non-loopback bind.
78
+ const doc = qoderDocument(ctx);
79
+ return {
80
+ clientId: "qoder",
81
+ fragments: [
82
+ { path: ["providers", OPENCODE_PROVIDER_ID], value: doc.providers[OPENCODE_PROVIDER_ID] },
83
+ ...doc.modelConfigs.customModels.map(model => {
84
+ const selector = formatSelectorConjunction([
85
+ { field: "provider", value: OPENCODE_PROVIDER_ID },
86
+ { field: "model", value: model.model },
87
+ ]);
88
+ if (selector === null) throw new Error("Qoder model ID cannot be represented by a managed array selector.");
89
+ return { path: ["modelConfigs", "customModels", selector], value: model };
90
+ }),
91
+ ],
92
+ };
93
+ }
@@ -20,10 +20,14 @@
20
20
  * targeting it is the caller's explicit act.
21
21
  */
22
22
  import { homedir } from "node:os";
23
+ import { gentleShellPaths } from "./gentle-shell-path";
24
+ import { absoluteClientPath, ClientPathError, piAgentDir } from "./client-path";
25
+ export { ClientPathError, piAgentDir } from "./client-path";
23
26
  import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
24
27
  import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
25
28
  import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
26
29
  import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
30
+ import { fileIO } from "../integrations/config-io";
27
31
  import { canonicalizeReasoningEfforts } from "../reasoning-effort";
28
32
  import { expandFastExportModels } from "./config-export/fast-models";
29
33
  import { probeHostname } from "../server/proxy-liveness";
@@ -81,6 +85,8 @@ import {
81
85
 
82
86
  export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock };
83
87
  export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot };
88
+ import { buildQoderClientConfig, summarizeQoder, buildQoderContribution, qoderConfigPath } from "./config-export/qoder";
89
+ export { buildQoderClientConfig, qoderConfigPath, qoderHomeDir, type QoderGeneratedConfig } from "./config-export/qoder";
84
90
 
85
91
 
86
92
 
@@ -210,21 +216,6 @@ function ompProfileName(env: OpencodeLaunchEnv): string | undefined {
210
216
  return profile;
211
217
  }
212
218
 
213
- /**
214
- * Pi resolves its agent directory from `PI_CODING_AGENT_DIR`, falling back to
215
- * `~/.pi/agent`. `ompAgentDir` below already reads that variable — OMP is a Pi
216
- * derivative — so Pi's own resolver honoring it is what makes the two agree
217
- * rather than a new claim about Pi's contract.
218
- *
219
- * A relative override is refused for the same reason MCode's and ZCode's are: a
220
- * background proxy and a foreground client can have different working
221
- * directories, and would otherwise disagree about which file is named.
222
- */
223
- export function piAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
224
- const override = env.PI_CODING_AGENT_DIR?.trim();
225
- if (override) return absoluteClientPath(override, home, "PI_CODING_AGENT_DIR");
226
- return join(home, ".pi", "agent");
227
- }
228
219
 
229
220
  /** Pi's canonical custom-provider catalog. */
230
221
  export function piConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
@@ -286,35 +277,6 @@ export function hermesConfigPath(env: OpencodeLaunchEnv = process.env, home: str
286
277
  return join(hermesHomeDir(env, home), "config.yaml");
287
278
  }
288
279
 
289
- /**
290
- * Expand `~` against the effective home; REFUSE anything still relative.
291
- *
292
- * A first attempt at this called `resolve()` and claimed the path would "mean
293
- * the same thing next time". It does not: `resolve()` only anchors the current
294
- * invocation, so applying from one directory and disabling from another still
295
- * resolved two different files — the second reported "not applied" and left
296
- * the managed block behind with nothing claiming it.
297
- *
298
- * There is no honest way to recover the other process's cwd, so a relative
299
- * selector is refused at the boundary instead of being silently anchored to
300
- * whichever directory we happened to start in. A `~` path IS stable, because
301
- * it anchors to the effective home rather than the cwd.
302
- */
303
- export class ClientPathError extends Error {}
304
-
305
- function absoluteClientPath(raw: string, home: string, variable: string): string {
306
- const trimmed = raw.trim();
307
- if (trimmed === "~") return home;
308
- if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return join(home, trimmed.slice(2));
309
- if (!isAbsolute(trimmed)) {
310
- throw new ClientPathError(
311
- `${variable} must be an absolute path or start with ~; "${trimmed}" depends on the working directory, `
312
- + "so opencodex and the client would disagree about which file it names.",
313
- );
314
- }
315
- return trimmed;
316
- }
317
-
318
280
  /**
319
281
  * OpenClaw's EFFECTIVE home: `OPENCLAW_HOME` outranks the OS home.
320
282
  *
@@ -475,10 +437,8 @@ export type ZcodePathIO = {
475
437
  readText: (path: string) => { kind: "text"; text: string } | { kind: "missing" } | { kind: "failed"; code?: string };
476
438
  };
477
439
  const zcodePathIO: ZcodePathIO = {
478
- readText: path => {
479
- try { return { kind: "text", text: readFileSync(path, "utf8") }; }
480
- catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? { kind: "missing" } : { kind: "failed" }; }
481
- },
440
+ /** Shares the integration reader's four-MiB limit and regular-file-only bootstrap contract. */
441
+ readText: path => fileIO().readText(path),
482
442
  };
483
443
  export class ZcodeSettingsPathError extends ClientPathError {}
484
444
 
@@ -1429,6 +1389,18 @@ export function clineSettingsDir(env: OpencodeLaunchEnv = process.env, home: str
1429
1389
  }
1430
1390
 
1431
1391
  export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1392
+ qoder: {
1393
+ id: "qoder",
1394
+ filename: "qoder-settings.json",
1395
+ destination: env => qoderConfigPath(env),
1396
+ apiKeyEnv: "",
1397
+ exportHint: "Qoder reads a non-secret placeholder from settings.json; loopback needs no key.",
1398
+ build: buildQoderClientConfig,
1399
+ format: "json",
1400
+ summarize: summarizeQoder,
1401
+ buildContribution: buildQoderContribution,
1402
+ loopbackOnly: true,
1403
+ },
1432
1404
  opencode: {
1433
1405
  id: "opencode",
1434
1406
  filename: "opencode.json",
@@ -1698,6 +1670,21 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1698
1670
  buildContribution: buildDroidContribution,
1699
1671
  loopbackOnly: true,
1700
1672
  },
1673
+ "gentle-shell": {
1674
+ id: "gentle-shell",
1675
+ filename: "gentle-shell-models.json",
1676
+ destination: env => gentleShellPaths(env).configPath,
1677
+ apiKeyEnv: "",
1678
+ exportHint: "Gentle Shell uses its persistent selected home; restart the shell to load the catalog. Link/shared homes use the Pi integration.",
1679
+ build: ctx => buildPiClientConfig(ctx, PI_EXPORT_OPTIONS),
1680
+ format: "json",
1681
+ summarize: summarizePi,
1682
+ buildContribution: ctx => {
1683
+ const doc = buildPiClientConfig(ctx, PI_EXPORT_OPTIONS);
1684
+ return singleFragment("gentle-shell", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
1685
+ },
1686
+ loopbackOnly: true,
1687
+ },
1701
1688
  };
1702
1689
 
1703
1690
  export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
@@ -0,0 +1,87 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { readBoundedRegularFileSync } from "../lib/bounded-file-read";
5
+ import { ClientPathError, piAgentDir } from "./client-path";
6
+
7
+ interface BootstrapIO {
8
+ readText(path: string): { kind: "text"; text: string } | { kind: "missing" } | { kind: "failed" };
9
+ }
10
+
11
+ function absolute(raw: string, label: string): string {
12
+ // Unlike Pi's resolver, the Shell launcher does not expand a literal '~'.
13
+ if (!isAbsolute(raw)) throw new ClientPathError(`${label} must be an absolute path; the proxy cannot infer the launcher's working directory.`);
14
+ return resolve(raw);
15
+ }
16
+
17
+ function readBootstrap(path: string, io?: BootstrapIO): string | undefined {
18
+ try {
19
+ if (!io) return readBoundedRegularFileSync(path, 64 * 1024).toString("utf8");
20
+ const read = io.readText(path);
21
+ if (read.kind === "missing") return undefined;
22
+ if (read.kind === "text" && Buffer.byteLength(read.text, "utf8") <= 64 * 1024) return read.text;
23
+ } catch (error) {
24
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
25
+ }
26
+ throw new ClientPathError(`Cannot read Gentle Shell launcher config at ${path}; no catalog was selected.`);
27
+ }
28
+
29
+ /** Resolve existing ancestors too, so a not-yet-created catalog cannot hide a directory alias. */
30
+ function canonical(path: string): string {
31
+ const missing: string[] = [];
32
+ let current = resolve(path);
33
+ for (;;) {
34
+ try {
35
+ const value = join(realpathSync.native(current), ...missing);
36
+ return process.platform === "win32" ? value.toLowerCase() : value;
37
+ } catch (error) {
38
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT" || dirname(current) === current) {
39
+ throw new ClientPathError(`Cannot verify Gentle Shell destination ${path}.`);
40
+ }
41
+ missing.unshift(basename(current));
42
+ current = dirname(current);
43
+ }
44
+ }
45
+ }
46
+
47
+ function sameFile(left: string, right: string): boolean {
48
+ if (canonical(left) === canonical(right)) return true;
49
+ // Hard-linked catalogs can have different canonical names but the same identity.
50
+ try {
51
+ const a = statSync(left, { bigint: true });
52
+ const b = statSync(right, { bigint: true });
53
+ return a.ino !== 0n && a.dev === b.dev && a.ino === b.ino;
54
+ } catch (error) {
55
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
56
+ throw new ClientPathError("Cannot verify Gentle Shell and Pi catalog identities.");
57
+ }
58
+ }
59
+
60
+ /** Persistent selectors only: a proxy cannot observe one-off `gentle-shell --home` flags. */
61
+ export function gentleShellPaths(
62
+ env: NodeJS.ProcessEnv = process.env,
63
+ home: string = homedir(),
64
+ io?: BootstrapIO,
65
+ ): { configPath: string; detectDir: string } {
66
+ const config = env.GENTLE_SHELL_CONFIG
67
+ ? absolute(env.GENTLE_SHELL_CONFIG, "GENTLE_SHELL_CONFIG")
68
+ : join(home, ".gentle-shell", "config.json");
69
+ const text = readBootstrap(config, io);
70
+ let selector: unknown;
71
+ try {
72
+ const parsed: unknown = text === undefined ? undefined : JSON.parse(text);
73
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) selector = (parsed as Record<string, unknown>).home;
74
+ } catch { /* The launcher also falls back to isolated for malformed JSON. */ }
75
+ const shared = "Gentle Shell uses a Pi destination; use the Pi integration for link/shared homes.";
76
+ if (selector === "link") throw new ClientPathError(shared);
77
+ const dir = typeof selector === "string" && selector.length > 0 && selector !== "isolated"
78
+ ? absolute(selector, "Gentle Shell saved home")
79
+ : env.GENTLE_SHELL_HOME
80
+ ? absolute(env.GENTLE_SHELL_HOME, "GENTLE_SHELL_HOME")
81
+ : join(home, ".gentle-shell", "agent");
82
+ const configPath = join(dir, "models.json");
83
+ const piHomes = [join(home, ".pi", "agent"), piAgentDir(env, home)];
84
+ if (env.GENTLE_SHELL_USER_PI_HOME) piHomes.push(absolute(env.GENTLE_SHELL_USER_PI_HOME, "GENTLE_SHELL_USER_PI_HOME"));
85
+ if (piHomes.some(pi => sameFile(configPath, join(pi, "models.json")))) throw new ClientPathError(shared);
86
+ return { configPath, detectDir: dir };
87
+ }
@@ -1,6 +1,7 @@
1
1
  import type { OcxConfig } from "../types";
2
2
  import { deleteConfigObjectChildKey } from "../config/rebase-provenance";
3
3
  import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
4
+ import { codexAccountUsesRemainingQuota } from "./account-use-remaining";
4
5
 
5
6
  export const DEFAULT_CODEX_AUTO_SWITCH_THRESHOLD = 80;
6
7
  export const MIN_CODEX_AUTO_SWITCH_THRESHOLD = 0;
@@ -34,6 +35,7 @@ export function getEffectiveCodexAutoSwitchThreshold(
34
35
  config: OcxConfig,
35
36
  accountId: string,
36
37
  ): number {
38
+ if (codexAccountUsesRemainingQuota(config, accountId)) return 0;
37
39
  const override = getCodexAccountAutoSwitchThresholdOverride(config, accountId);
38
40
  if (override !== null) return override;
39
41
  return config.autoSwitchThreshold ?? DEFAULT_CODEX_AUTO_SWITCH_THRESHOLD;
@@ -0,0 +1,29 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { codexLoginIdentity as identity, codexTokenIdentity, type CodexLoginIdentity } from "./signed-in-identity";
3
+ import { MAIN_CODEX_ACCOUNT_ID, isSelectableCodexPoolAccount } from "./account-id";
4
+ import type { CodexTokenReadResult } from "./auth-collision";
5
+ import { getCodexAccountCredential } from "./account-store";
6
+
7
+ /** Match existing entries from an already-owned main observation, without exposing identity fields. */
8
+ export function linkedCodexAccountIds(config: OcxConfig, selectedId: string, main: CodexTokenReadResult | { status: "api-key-only" }): string[] | undefined {
9
+ // Unknown main identity must not produce a successful but incomplete pause.
10
+ if (main.status === "unreadable" || main.status === "invalid") return undefined;
11
+ const identities = new Map<string, CodexLoginIdentity | undefined>();
12
+ if (main.status === "ok") {
13
+ const selected = codexTokenIdentity(main.tokens);
14
+ if (selected === null) return undefined;
15
+ identities.set(MAIN_CODEX_ACCOUNT_ID, selected);
16
+ }
17
+ for (const account of config.codexAccounts ?? []) {
18
+ if (!isSelectableCodexPoolAccount(account)) continue;
19
+ const credential = getCodexAccountCredential(account.id);
20
+ identities.set(account.id, identity(credential?.chatgptAccountId, account.email));
21
+ }
22
+ const selected = identities.get(selectedId);
23
+ // Missing identity evidence never links unrelated logins, including members of one workspace.
24
+ if (!selected) return [selectedId];
25
+ return [selectedId, ...[...identities].flatMap(([id, candidate]) => (
26
+ id !== selectedId && candidate?.accountId === selected.accountId && candidate.email === selected.email
27
+ ? [id] : []
28
+ ))];
29
+ }
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
2
2
  import type { CodexAccount, OcxConfig } from "../types";
3
3
  import type { CodexAuthContext } from "./auth-context";
4
4
  import type { ProviderApiKeySelection } from "../types/provider";
5
- import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
5
+ import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
6
6
 
7
7
  export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
8
8
 
@@ -1,3 +1,4 @@
1
+ import { observeSignedInCodexIdentity } from "./signed-in-identity";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import {
3
4
  deleteConfigTopLevelKey,
@@ -8,17 +9,20 @@ import {
8
9
  import { captureConfigTopLevelRollback } from "../config/rebase-provenance";
9
10
  import { removeCodexAccountCredential } from "./account-store";
10
11
  import { clearAccountNeedsReauth } from "./account-runtime-state";
11
- import { getMainChatgptAccountId, readCodexTokensResult } from "./auth-collision";
12
+ import { readCodexTokensResult } from "./auth-collision";
12
13
  import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
13
14
  import { clearAccountQuota } from "./quota";
14
- import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./routing";
15
+ import { forgetManualPreference } from "./routing/active-account";
16
+ import { deleteAllHealthForAccount } from "./routing/health-store";
17
+ import { clearThreadAccountMapForAccount } from "./routing/thread-affinity";
15
18
 
16
19
  import { clearCodexPoolRefreshFailure } from "./pool-refresh-backoff";
17
20
  import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
18
21
  import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaCredential, observeMainQuotaIdentity } from "./main-account-cache";
19
- import { extractAccountIdClaims } from "../oauth/chatgpt";
22
+ import { extractAccountId, extractAccountIdClaims } from "../oauth/chatgpt";
20
23
  import { forgetCodexAccountPause } from "./account-pause";
21
24
  import { forgetCodexAccountCreditUse } from "./account-credit-use";
25
+ import { setCodexAccountUseRemainingQuota } from "./account-use-remaining";
22
26
  import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
23
27
  import { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
24
28
  import { forgetCodexAccountAutoSwitchThreshold } from "./account-auto-switch";
@@ -45,7 +49,11 @@ export function purgeCodexAccountRuntimeState(accountId: string): void {
45
49
  clearAccountNeedsReauth(accountId);
46
50
  clearAccountQuota(accountId);
47
51
  clearThreadAccountMapForAccount(accountId);
48
- clearCodexUpstreamHealthForAccount(accountId);
52
+ // Deliberately not via `./routing`: the account-delete path only needs the health rows and
53
+ // the manual preference gone, and importing the router here closed a cycle through
54
+ // routing/health-store and routing/active-account.
55
+ deleteAllHealthForAccount(accountId);
56
+ forgetManualPreference(accountId);
49
57
  clearCodexPoolRefreshFailure(accountId);
50
58
  if (accountId === MAIN_CODEX_ACCOUNT_ID) {
51
59
  clearMainAccountInfoCache();
@@ -66,7 +74,11 @@ function purgeMainCodexAccountRuntimeState(): void {
66
74
  * cooldown, quota, reauth, and thread affinity do not leak across accounts.
67
75
  */
68
76
  export function reconcileMainCodexAccountRuntimeState(): boolean {
69
- const currentAccountId = getMainChatgptAccountId();
77
+ const read = readCodexTokensResult();
78
+ const currentAccountId = read.status === "ok"
79
+ ? extractAccountId(read.tokens.id_token, read.tokens.access_token) ?? (read.tokens.account_id || null)
80
+ : null;
81
+ if (read.status === "ok") observeSignedInCodexIdentity(read.tokens);
70
82
  // A missing/malformed auth.json is an unknown identity, not a confirmed account switch. Keep the
71
83
  // prior observation and its safety state until a real account id can be read again.
72
84
  if (currentAccountId === null) return false;
@@ -110,6 +122,7 @@ export function initializeMainAccountPolicyBinding(authPath: string): boolean {
110
122
  const previousAccountId = observedMainChatgptAccountId;
111
123
  observedMainChatgptAccountId = accountId;
112
124
  if (previousAccountId !== undefined && previousAccountId !== accountId) purgeMainCodexAccountRuntimeState();
125
+ observeSignedInCodexIdentity(tokens);
113
126
  observeMainQuotaIdentity(accountId);
114
127
  return observeMainQuotaCredential(tokens.access_token, accountId) !== undefined;
115
128
  }
@@ -122,6 +135,7 @@ export function applyConfirmedMainCodexAccountTransition(
122
135
  fromAccountId: string,
123
136
  toAccountId: string,
124
137
  ): boolean {
138
+ observeSignedInCodexIdentity();
125
139
  if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) {
126
140
  if (toAccountId) {
127
141
  observedMainChatgptAccountId = toAccountId;
@@ -137,6 +151,7 @@ export function applyConfirmedMainCodexAccountTransition(
137
151
 
138
152
  export function resetMainCodexAccountIdentityTrackingForTests(): void {
139
153
  observedMainChatgptAccountId = undefined;
154
+ observeSignedInCodexIdentity();
140
155
  clearMainAccountCredentialPresence();
141
156
  }
142
157
 
@@ -183,6 +198,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
183
198
  forgetCodexAccountCreditUse(runtimeConfig, accountId);
184
199
  forgetCodexAccountPriority(runtimeConfig, accountId);
185
200
  forgetCodexAccountAutoSwitchThreshold(runtimeConfig, accountId);
201
+ setCodexAccountUseRemainingQuota(runtimeConfig, accountId, false);
186
202
  if (runtimeConfig.codexQuotaAutoRefresh?.[accountId]) {
187
203
  const retained = { ...runtimeConfig.codexQuotaAutoRefresh };
188
204
  delete retained[accountId];
@@ -0,0 +1,28 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { MAIN_CODEX_ACCOUNT_ID, isSelectableCodexPoolAccount } from "./account-id";
3
+ import { getCodexAccountCredential } from "./account-store";
4
+ import { codexAccountPriorityLookup } from "./account-priority";
5
+ import { MIN_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY } from "./pool-rotation";
6
+ import { codexLoginIdentity, getSignedInCodexIdentity, sameCodexLogin } from "./signed-in-identity";
7
+
8
+ /** Resolve current rows afresh: a pool replacement must not inherit the prior login's role. */
9
+ export function signedInCodexAccountIds(config: OcxConfig): ReadonlySet<string> {
10
+ const ids = new Set([MAIN_CODEX_ACCOUNT_ID]);
11
+ const main = getSignedInCodexIdentity();
12
+ if (!main) return ids;
13
+ for (const account of config.codexAccounts ?? []) {
14
+ if (!isSelectableCodexPoolAccount(account)) continue;
15
+ const credential = getCodexAccountCredential(account.id);
16
+ if (sameCodexLogin(main, codexLoginIdentity(credential?.chatgptAccountId, account.email))) ids.add(account.id);
17
+ }
18
+ return ids;
19
+ }
20
+
21
+ /** Two disjoint bands, preserving custom order inside each; never writes the stored priorities. */
22
+ export function codexSelectionPriorityLookup(config: OcxConfig): (id: string) => number {
23
+ const priority = codexAccountPriorityLookup(config);
24
+ if (config.codexPreserveSignedInAccount !== true) return priority;
25
+ const reserved = signedInCodexAccountIds(config);
26
+ const band = MAX_ACCOUNT_PRIORITY - MIN_ACCOUNT_PRIORITY + 1;
27
+ return id => priority(id) - (reserved.has(id) ? band : 0);
28
+ }
@@ -0,0 +1,23 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
3
+ import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
4
+
5
+ type Policy = Pick<OcxConfig, "codexUseRemainingQuotaAccountIds">;
6
+
7
+ /** Included-quota preference only; never grants permission to spend credits. */
8
+ export function codexAccountUsesRemainingQuota(config: Policy | undefined, accountId: string): boolean {
9
+ return config?.codexUseRemainingQuotaAccountIds?.includes(accountId) ?? false;
10
+ }
11
+
12
+ export function validRemainingQuotaAccountIds(value: unknown): value is string[] {
13
+ return Array.isArray(value) && value.every(id => id === MAIN_CODEX_ACCOUNT_ID || isValidCodexAccountId(id));
14
+ }
15
+
16
+ /** Preserve the underlying thresholds so turning this off restores their current values. */
17
+ export function setCodexAccountUseRemainingQuota(config: OcxConfig, accountId: string, enabled: boolean): void {
18
+ const ids = new Set(config.codexUseRemainingQuotaAccountIds ?? []);
19
+ if (enabled) ids.add(accountId);
20
+ else ids.delete(accountId);
21
+ if (ids.size) config.codexUseRemainingQuotaAccountIds = [...ids];
22
+ else deleteConfigTopLevelKey(config, "codexUseRemainingQuotaAccountIds");
23
+ }
@@ -14,7 +14,7 @@ import {
14
14
  resolveTrustedWindowsPowerShellExe,
15
15
  resolveTrustedWindowsTaskkillExe,
16
16
  } from "../lib/windows-elevation";
17
- import { readCodexCatalogPath } from "./catalog/parsing";
17
+ import { readCodexCatalogPath } from "./paths";
18
18
 
19
19
  export const STALE_CODEX_APP_SERVER_HINT =
20
20
  "If Codex still shows an older model list, run `ocx sync --restart-codex`: it restarts the long-lived app-server "
@@ -332,6 +332,32 @@ export function isCodexAppServerCommandLine(commandLine: string, executable?: st
332
332
  return false;
333
333
  }
334
334
 
335
+ /**
336
+ * True when the command line is ANY live Codex session, not just an app-server.
337
+ *
338
+ * The update-blocking contract needs the broader reading: an interactive TUI, a
339
+ * one-shot `codex exec`, or an app-server all have the package's binaries in use,
340
+ * so every subcommand counts. The executable identity stays exactly as strict as
341
+ * {@link isCodexAppServerCommandLine} - only the argv discipline is dropped. A
342
+ * coincidental codex-shaped argument can never open this matcher.
343
+ */
344
+ export function isCodexSessionCommandLine(commandLine: string, executable?: string): boolean {
345
+ const trimmed = commandLine.trim();
346
+ let tokens = tokenizeCommandLine(trimmed);
347
+ if (executable) {
348
+ if (isCodeModeHostToken(executable) || isCodexExecutableToken(executable)) return true;
349
+ }
350
+ if (tokens.length === 0) return false;
351
+ if (isCodeModeHostProcess(tokens)) return true;
352
+ // Same interpreter unwrap the app-server matcher uses: a codex-shaped token must
353
+ // immediately follow the interpreter so an interpreter flag value is never read
354
+ // as the entrypoint.
355
+ if (isInterpreterToken(tokens[0]!) && tokens.length > 1 && isCodexExecutableToken(tokens[1]!)) {
356
+ tokens = tokens.slice(1);
357
+ }
358
+ return isCodexExecutableToken(tokens[0]!);
359
+ }
360
+
335
361
  function parseUnixProcStatusUid(status: string): number | undefined {
336
362
  const match = /^Uid:\s+(\d+)/m.exec(status);
337
363
  if (!match) return undefined;
@@ -339,7 +365,7 @@ function parseUnixProcStatusUid(status: string): number | undefined {
339
365
  return Number.isSafeInteger(uid) ? uid : undefined;
340
366
  }
341
367
 
342
- function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
368
+ function listUnixProcSnapshots(uid: number | undefined, requireComplete = true): ProcessSnapshot[] {
343
369
  // procfs missing on a Linux-shaped platform is an enumeration failure, not
344
370
  // "no processes" — the staleness collector must not read it as not_running.
345
371
  if (!existsSync("/proc")) throw new Error("procfs_unavailable");
@@ -359,8 +385,10 @@ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
359
385
  const commandLine = argv.join(" ").trim();
360
386
  if (!commandLine) continue;
361
387
  out.push({ pid, commandLine, executable: argv[0], uid: processUid });
362
- } catch {
363
- /* process exited mid-scan */
388
+ } catch (error) {
389
+ // Complete observations may ignore only disappearance. Restart target
390
+ // enumeration can skip an unreadable PID while retaining verified ones.
391
+ if (requireComplete && (error as NodeJS.ErrnoException)?.code !== "ENOENT") throw error;
364
392
  }
365
393
  }
366
394
  return out;
@@ -518,10 +546,10 @@ async function listWindowsSnapshotsAsync(): Promise<ProcessSnapshot[]> {
518
546
  return parseWindowsSnapshotOutput(output);
519
547
  }
520
548
 
521
- function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined): ProcessSnapshot[] {
549
+ function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined, requireComplete = true): ProcessSnapshot[] {
522
550
  if (platform === "win32") return listWindowsSnapshots();
523
551
  if (platform === "darwin") return listDarwinSnapshots(getuid());
524
- return listUnixProcSnapshots(getuid());
552
+ return listUnixProcSnapshots(getuid(), requireComplete);
525
553
  }
526
554
 
527
555
  export interface ListProcessSnapshotsOptions {
@@ -565,7 +593,7 @@ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): C
565
593
  // Restart/kill contract (#476): enumeration failure means no targets —
566
594
  // never signal a process we could not verify.
567
595
  try {
568
- snapshots = defaultListSnapshots(platform, getuid);
596
+ snapshots = defaultListSnapshots(platform, getuid, false);
569
597
  } catch {
570
598
  snapshots = [];
571
599
  }
@@ -581,6 +609,68 @@ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): C
581
609
  return matched;
582
610
  }
583
611
 
612
+ /**
613
+ * Enumeration outcome for callers that must DEFER on an unreadable process table.
614
+ *
615
+ * {@link listCodexAppServerProcesses} maps enumeration failure to an empty list because its
616
+ * kill contract must never signal a process it could not verify. The Codex CLI update
617
+ * workflow needs the opposite reading: "no matches" and "could not look" lead to different
618
+ * decisions, and only the first one may allow an install to proceed.
619
+ */
620
+ export type CodexAppServerProcessScan =
621
+ | Readonly<{ kind: "observed"; processes: readonly CodexAppServerProcess[] }>
622
+ | Readonly<{ kind: "unavailable" }>;
623
+
624
+ function scanProcessSnapshots(
625
+ io: CodexAppServerProcessIo,
626
+ matches: (commandLine: string, executable?: string) => boolean,
627
+ ): CodexAppServerProcessScan {
628
+ const platform = io.platform ?? process.platform;
629
+ const getuid = io.getuid ?? (() => {
630
+ try {
631
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
632
+ } catch {
633
+ return undefined;
634
+ }
635
+ });
636
+ let snapshots: ProcessSnapshot[];
637
+ try {
638
+ snapshots = io.listSnapshots ? io.listSnapshots() : defaultListSnapshots(platform, getuid);
639
+ } catch {
640
+ return Object.freeze({ kind: "unavailable" as const });
641
+ }
642
+ const seen = new Set<number>();
643
+ const matched: CodexAppServerProcess[] = [];
644
+ for (const snapshot of snapshots) {
645
+ if (seen.has(snapshot.pid)) continue;
646
+ if (!matches(snapshot.commandLine, snapshot.executable)) continue;
647
+ seen.add(snapshot.pid);
648
+ matched.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
649
+ }
650
+ return Object.freeze({ kind: "observed" as const, processes: Object.freeze(matched) });
651
+ }
652
+
653
+ /** Same matcher and snapshot sources as {@link listCodexAppServerProcesses}, failing closed. */
654
+ export function scanCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): CodexAppServerProcessScan {
655
+ return scanProcessSnapshots(io, isCodexAppServerCommandLine);
656
+ }
657
+
658
+ /**
659
+ * {@link isCodexSessionCommandLine} over the same snapshot sources, failing closed.
660
+ *
661
+ * Any live Codex session counts - TUI, exec or app-server all hold the installed
662
+ * package open while npm would be replacing it, and a missed TUI was the reported
663
+ * source of a half-written install on Linux. The kill path keeps the narrower
664
+ * {@link isCodexAppServerCommandLine} because a signal must never leave its
665
+ * verified contract; this scan decides whether it is SAFE to install, so it may
666
+ * only err toward deferring.
667
+ */
668
+ export function scanCodexSessionProcesses(io: CodexAppServerProcessIo = {}): CodexAppServerProcessScan {
669
+ // Shared installs may be held open by another readable user's session. This
670
+ // read-only scan is broader than the unchanged same-user restart/kill path.
671
+ return scanProcessSnapshots({ ...io, getuid: io.getuid ?? (() => undefined) }, isCodexSessionCommandLine);
672
+ }
673
+
584
674
  export function formatStaleCodexAppServerWarning(
585
675
  processes: readonly { pid: number }[],
586
676
  ): string {