@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
package/src/cli/status.ts CHANGED
@@ -5,7 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
5
5
  import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config";
6
6
  import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state";
7
7
  import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
8
- import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
8
+ import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
9
9
  import type { OcxConfig } from "../types";
10
10
  import { diagnoseService, serviceLogPath } from "../service";
11
11
  import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
@@ -104,6 +104,16 @@ export type CliRemoteHubStatus = {
104
104
  truncated: boolean;
105
105
  /** The hub's own Claude Code toggle; a client cannot infer it from local config. */
106
106
  claudeCodeEnabled: boolean | null;
107
+ /**
108
+ * Present on a Remote Link Child (#6957): the peer is a Home reached over the link, not a hub,
109
+ * and every sentence names it that way. `codexSettings` lists the Home's Codex settings the
110
+ * last sync applied, or why the Child's local values are in effect.
111
+ */
112
+ link?: {
113
+ linkId: string;
114
+ homePhrase: string;
115
+ codexSettings: { source: "home"; fetchedAt: string; lines: string[] } | { source: "local"; reason: string };
116
+ };
107
117
  };
108
118
 
109
119
  export type CliStatusJson = {
@@ -152,6 +162,7 @@ export type CliStatusJson = {
152
162
  };
153
163
  connection: {
154
164
  state: "disconnected" | "connected" | "invalid" | "mismatched";
165
+ transport?: "hub" | "link";
155
166
  reason?: string;
156
167
  serverUrl?: string;
157
168
  managementUrl?: string;
@@ -321,8 +332,12 @@ export function selectStatusStartupHealth(
321
332
  if (evidence?.kind === "desktop" && evidence.runtimePid === livePid) {
322
333
  const local = fallback();
323
334
  const supervisor = local.desktop?.supervisor;
324
- // The local probe runs later; it must still describe the same unowned process pair.
325
- if (local.desktop?.owned === false && supervisor
335
+ // The local probe runs later; it must still describe the same process pair. macOS and Linux
336
+ // accept only an unowned pair. On Windows an older runtime never published a Desktop
337
+ // diagnostic, so a durable Desktop claim beside proven live parentage qualifies too.
338
+ const ownershipEligible = local.desktop?.owned === false
339
+ || (local.desktop?.owned === true && local.platform === "win32");
340
+ if (ownershipEligible && supervisor
326
341
  && supervisor.runtimePid === evidence.runtimePid && supervisor.supervisorPid === evidence.supervisorPid) {
327
342
  return { startup: local, startupSource: "local-supervision-override" };
328
343
  }
@@ -499,8 +514,8 @@ function boundHubStateToken(
499
514
  * degradation.
500
515
  */
501
516
  export async function collectRemoteHubStatus(
502
- connection: Pick<ClientConnectionStatus, "state" | "serverUrl" | "apiKeyId" | "connectedAt">,
503
- options: { fetchImpl?: typeof fetch; timeoutMs?: number; now?: number } = {},
517
+ connection: Pick<ClientConnectionStatus, "state" | "serverUrl" | "apiKeyId" | "connectedAt"> & { transport?: "hub" | "link" },
518
+ options: { fetchImpl?: typeof fetch; timeoutMs?: number; now?: number; findChild?: () => Promise<LiveProxy | null> } = {},
504
519
  ): Promise<CliRemoteHubStatus> {
505
520
  if (connection.state !== "connected" || !connection.serverUrl || !connection.apiKeyId || !connection.connectedAt) {
506
521
  return disconnectedRemoteHubStatus();
@@ -512,14 +527,50 @@ export async function collectRemoteHubStatus(
512
527
  connectedAt: connection.connectedAt,
513
528
  };
514
529
  const bound = boundHubStateToken(readClientConnectionState(), readServiceApiTokenState(), owner);
530
+ // Link credentials never go to the persisted tunnel URL from this CLI. Ask the local Child
531
+ // without a credential; its connection-bound relay authenticates the actual Home connection.
532
+ let fetchImpl = options.fetchImpl;
533
+ let allowNetwork = true;
534
+ let localRelayFetchImpl: typeof fetch | undefined;
535
+ if (connection.transport === "link") {
536
+ const live = await (options.findChild ?? findLiveProxy)();
537
+ const current = readClientConnectionState();
538
+ allowNetwork = live?.role === "client" && current.kind === "connected" && sameClientConnectionOwner(current.value, owner);
539
+ if (allowNetwork && live) {
540
+ const localOrigin = `http://${probeHostname(live.hostname)}:${live.port}`;
541
+ localRelayFetchImpl = ((_, init) => (options.fetchImpl ?? fetch)(localOrigin + "/v1/hub-state", {
542
+ method: "GET", redirect: "manual", signal: init?.signal, headers: { Accept: "application/json" },
543
+ })) as typeof fetch;
544
+ }
545
+ }
546
+ // #6957: the Home's name, the link id and the Codex settings the last sync applied.
547
+ const linkState = readClientConnectionState();
548
+ const linkConnection = connection.transport === "link" && linkState.kind === "connected" && linkState.value.transport === "link" && linkState.value.link
549
+ ? linkState.value : null;
550
+ const linkHome = linkConnection ? await import("../client/link-home") : null;
515
551
  const resolved = await resolveHubState({
516
552
  owner,
517
- token: bound.token,
553
+ token: connection.transport === "link" ? null : bound.token,
554
+ ...(localRelayFetchImpl ? { localRelayFetchImpl } : {}),
518
555
  ...(bound.withheldReason ? { withheldTokenReason: bound.withheldReason } : {}),
519
- ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
556
+ ...(fetchImpl ? { fetchImpl } : {}),
557
+ allowNetwork,
558
+ ...(connection.transport === "link" ? { peer: "Home" as const } : {}),
520
559
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
521
560
  ...(options.now === undefined ? {} : { now: options.now }),
522
561
  });
562
+ let link: CliRemoteHubStatus["link"];
563
+ if (linkConnection && linkHome) {
564
+ const { describeHomeCodexSettings } = await import("../link/codex-settings");
565
+ const record = linkHome.readHomeCodexSettingsRecord(linkConnection.link!.linkId);
566
+ link = {
567
+ linkId: linkConnection.link!.linkId,
568
+ homePhrase: linkHome.linkHomePhrase(linkConnection),
569
+ codexSettings: record
570
+ ? { source: "home", fetchedAt: record.fetchedAt, lines: describeHomeCodexSettings(record.document.settings) }
571
+ : { source: "local", reason: "no Home settings recorded yet; run: ocx sync" },
572
+ };
573
+ }
523
574
  return {
524
575
  connected: true,
525
576
  // The hub's own advertised origin when it publishes one; otherwise the URL this client
@@ -535,6 +586,7 @@ export async function collectRemoteHubStatus(
535
586
  subagentModels: resolved.state?.subagentModels ?? [],
536
587
  truncated: resolved.state?.truncated === true,
537
588
  claudeCodeEnabled: resolved.state ? resolved.state.claudeCode.enabled : null,
589
+ ...(link ? { link } : {}),
538
590
  };
539
591
  }
540
592
 
@@ -546,8 +598,24 @@ export async function collectRemoteHubStatus(
546
598
  * A buried `Remote hub: connected (<url>)` line — which is all this report had — does not stop
547
599
  * that, as #4236 demonstrated.
548
600
  */
549
- export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus): string | null {
601
+ export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus, transport?: "hub" | "link"): string | null {
550
602
  if (!remoteHub.connected) return null;
603
+ if (remoteHub.link) {
604
+ const peer = `${remoteHub.link.homePhrase} over Remote Link (${remoteHub.link.linkId})`;
605
+ if (remoteHub.stateSource === "unavailable") {
606
+ return `⚠️ ${peer}: state unavailable (${remoteHub.reason ?? "unknown reason"}) — provider and login lines below are LOCAL and do not describe the Home.`;
607
+ }
608
+ const cached = remoteHub.stateSource === "cache"
609
+ ? ` — cached ${remoteHub.ageSeconds ?? "?"}s ago (${remoteHub.reason ?? "live read failed"})`
610
+ : "";
611
+ return `🔗 State from ${peer}${cached}: provider credentials, logins and delegable models below are the Home's, not this machine's.`;
612
+ }
613
+ if (transport === "link") {
614
+ const origin = remoteHub.origin ?? "unknown";
615
+ return remoteHub.stateSource === "unavailable"
616
+ ? `🔗 Remote Link Home ${origin}: provider credentials and routing come from Home. State unavailable (${remoteHub.reason ?? "unknown reason"}).`
617
+ : `🔗 State from Remote Link Home ${origin}${remoteHub.stateSource === "cache" ? " (cached)" : ""}: provider credentials, logins and models below come from Home.`;
618
+ }
551
619
  const origin = remoteHub.origin ?? "the hub";
552
620
  if (remoteHub.stateSource === "unavailable") {
553
621
  return `⚠️ Hub ${origin}: state unavailable (${remoteHub.reason ?? "unknown reason"}) — provider and login lines below are LOCAL and do not describe the hub.`;
@@ -562,32 +630,47 @@ export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus): string | nul
562
630
  * The hub-sourced provider/login/model block, owned here so the sentences are testable without
563
631
  * spawning the CLI. Indentation is the caller's. Empty when nothing true is known.
564
632
  */
565
- export function remoteHubStatusLines(remoteHub: CliRemoteHubStatus): string[] {
566
- if (!remoteHub.connected || remoteHub.stateSource === "unavailable") return [];
567
- const origin = remoteHub.origin ?? "hub";
633
+ export function remoteHubStatusLines(remoteHub: CliRemoteHubStatus, transport?: "hub" | "link"): string[] {
634
+ if (!remoteHub.connected) return [];
635
+ const settings = remoteHub.link ? linkCodexSettingsLines(remoteHub.link) : [];
636
+ if (remoteHub.stateSource === "unavailable") return settings;
637
+ const isLink = Boolean(remoteHub.link) || transport === "link";
638
+ const peer = remoteHub.link?.homePhrase ?? `${isLink ? "Home" : "hub"} ${remoteHub.origin ?? (isLink ? "Home" : "hub")}`;
639
+ const subject = isLink ? "the Home" : "the hub";
568
640
  const lines = [
569
- `OAuth logins (hub ${origin}):`,
641
+ `OAuth logins (${peer}):`,
570
642
  ...(remoteHub.oauth.length === 0
571
- ? [" (the hub reported no OAuth providers)"]
643
+ ? [` (${subject} reported no OAuth providers)`]
572
644
  : remoteHub.oauth.map(entry => ` ${entry.provider.padEnd(10)} ${entry.loggedIn ? "✓ logged in" : "✗ not logged in"}`)),
573
- `Providers (hub ${origin}):`,
645
+ `Providers (${peer}):`,
574
646
  ...(remoteHub.providers.length === 0
575
- ? [" (the hub reported no providers)"]
647
+ ? [` (${subject} reported no providers)`]
576
648
  : remoteHub.providers.map(provider => {
577
649
  // `authMode` is what keeps "no API key" from reading as "not configured": an `oauth`
578
650
  // provider legitimately has no key and is still fully usable.
579
651
  const credential = provider.hasCredential ? "credential stored" : `no API key (authMode ${provider.authMode ?? "key"})`;
580
652
  return ` ${provider.name.padEnd(10)} ${provider.adapter} — ${credential}${provider.disabled ? ", disabled" : ""}`;
581
653
  })),
582
- `Delegable models (hub ${origin}): ${remoteHub.subagentModels.length === 0 ? "none" : remoteHub.subagentModels.join(", ")}`,
654
+ `Delegable models (${peer}): ${remoteHub.subagentModels.length === 0 ? "none" : remoteHub.subagentModels.join(", ")}`,
583
655
  ];
584
- if (remoteHub.hubVersion) lines.push(`Hub version: ${remoteHub.hubVersion}`);
656
+ if (remoteHub.hubVersion) lines.push(`${remoteHub.link ? "Home" : "Hub"} version: ${remoteHub.hubVersion}`);
585
657
  // The hub told us its own lists are a prefix. Saying nothing here would make this report claim
586
658
  // completeness it does not have — the same shape of confident-and-wrong answer as #4236.
587
659
  if (remoteHub.truncated) {
588
- lines.push(" (the hub truncated this state to fit its response caps; some rows are not listed)");
660
+ lines.push(` (${subject} truncated this state to fit its response caps; some rows are not listed)`);
589
661
  }
590
- return lines;
662
+ return [...lines, ...settings];
663
+ }
664
+
665
+ /** Which Codex settings the last sync took from the Home (#6957). */
666
+ export function linkCodexSettingsLines(link: NonNullable<CliRemoteHubStatus["link"]>): string[] {
667
+ if (link.codexSettings.source === "local") {
668
+ return [`Codex settings: this machine's local values (${link.codexSettings.reason})`];
669
+ }
670
+ return [
671
+ `Codex settings from ${link.homePhrase} (applied ${link.codexSettings.fetchedAt}):`,
672
+ ...link.codexSettings.lines.map(line => ` ${line}`),
673
+ ];
591
674
  }
592
675
 
593
676
  export function selectListenTarget(
@@ -760,7 +843,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
760
843
  // Asked before the local probes below so a connected client's report is hub-sourced from its
761
844
  // first line. Bounded and failure-tolerant: an offline hub degrades the remoteHub block, it
762
845
  // does not fail `ocx status`.
763
- const remoteHub = await collectRemoteHubStatus(clientConnection);
846
+ const remoteHub = await collectRemoteHubStatus({ ...clientConnection, transport: config.client?.transport });
764
847
  // Prefer identity-verified liveness (runtime-port + /healthz) over ocx.pid alone (#618).
765
848
  // Pass the already-resolved diagnostics config so findLiveProxy does not re-load and
766
849
  // warn on malformed config.json (status --json must stay stderr-clean).
@@ -960,6 +1043,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
960
1043
  },
961
1044
  connection: {
962
1045
  state: clientConnection.state,
1046
+ ...(config.client?.transport ? { transport: config.client.transport } : {}),
963
1047
  ...(clientConnection.reason ? { reason: clientConnection.reason } : {}),
964
1048
  ...(clientConnection.serverUrl ? { serverUrl: clientConnection.serverUrl } : {}),
965
1049
  ...(clientConnection.managementUrl ? { managementUrl: clientConnection.managementUrl } : {}),
@@ -74,6 +74,16 @@ import {
74
74
  assertNoClientDisconnectPending, assertClientConnectionUnchanged, sameClientConnectionOwner,
75
75
  } from "./state";
76
76
  import { assertClientCatalogCompatible, type CatalogCompatibilityDeps } from "./catalog-compatibility";
77
+ import { overlayHomeCodexSettings } from "../link/codex-settings";
78
+ import {
79
+ clearHomeCodexSettingsRecord,
80
+ fetchHomeCodexSettings,
81
+ resolveHomeCodexSettings,
82
+ recordHomeCodexSettings,
83
+ writeHomeCodexSettingsRecord,
84
+ type HomeCodexSettingsSource,
85
+ type LinkHomeDeps,
86
+ } from "./link-home";
77
87
  import { hubStateCachePath } from "./hub-state";
78
88
  import { ClientLinkStateError, clearClientLinkState, readClientLinkState } from "./link-state";
79
89
  import { isLinkPort } from "../link/ports";
@@ -110,6 +120,8 @@ export interface ClientConnectDeps {
110
120
  now?: () => Date;
111
121
  lifecycleLockDeps?: ClientLifecycleLockDeps;
112
122
  catalogCompatibility?: CatalogCompatibilityDeps;
123
+ /** Link mode: how the Home's Codex settings are read (#6957). */
124
+ linkHome?: LinkHomeDeps;
113
125
  }
114
126
 
115
127
  export interface RotateClientOptions {
@@ -687,7 +699,16 @@ export async function connectClient(
687
699
 
688
700
  const config = earlyConfig ?? loadConfig();
689
701
  const target = routingTarget(serverUrl, linkMode ? config.port : undefined, config);
690
- const injectConfig = { ...config, syncResumeHistory: false };
702
+ // A link Child writes the Home's OpenCodex-owned Codex settings, not its own (#6957). A read
703
+ // that fails here keeps the local values; the next `ocx sync` applies the Home's.
704
+ // Read only: the record is written after the connection commits, so a rollback leaves none.
705
+ const homeSettings = linkMode && linkMetadata && options.selectedClients.includes("codex")
706
+ ? await fetchHomeCodexSettings({ transport: "link", link: linkMetadata, apiKeyId }, admissionToken, deps.linkHome)
707
+ : undefined;
708
+ const injectConfig = {
709
+ ...(homeSettings?.kind === "fresh" ? overlayHomeCodexSettings(config, homeSettings.document.settings) : config),
710
+ syncResumeHistory: false,
711
+ };
691
712
  const preflight = await injectCodexConfig(config.port, injectConfig, {
692
713
  validateOnly: true,
693
714
  routingTarget: target,
@@ -734,6 +755,12 @@ export async function connectClient(
734
755
  commitClientConnection(connection);
735
756
  committed = true;
736
757
  }), deps.lifecycleLockDeps);
758
+ // Recorded only when the injection that applied it ran (not with --no-sync).
759
+ if (homeSettings?.kind === "fresh" && linkMetadata && injectionCommitted) {
760
+ try {
761
+ writeHomeCodexSettingsRecord({ linkId: linkMetadata.linkId, fetchedAt: now, document: homeSettings.document });
762
+ } catch { /* the next sync records it; the applied config is already committed */ }
763
+ }
737
764
  return connection;
738
765
  } catch (error) {
739
766
  const rollbackFailures: string[] = [];
@@ -780,7 +807,7 @@ export async function connectClient(
780
807
  export async function syncConnectedClient(
781
808
  _options: { restartCodex?: boolean } = {},
782
809
  deps: ClientConnectDeps = {},
783
- ): Promise<{ catalogWritten: boolean; cacheSynced: boolean; injected: boolean; stale: boolean }> {
810
+ ): Promise<{ catalogWritten: boolean; cacheSynced: boolean; injected: boolean; stale: boolean; homeSettings?: HomeCodexSettingsSource }> {
784
811
  if (siblingOfLivePort() !== null) throw new Error(siblingSkipMessage());
785
812
  const initial = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
786
813
  assertNoClientDisconnectPending();
@@ -825,21 +852,28 @@ export async function syncConnectedClient(
825
852
  if (next.pendingOperation || token.kind !== "present" || token.fingerprint !== next.tokenFingerprint) throw new Error("client_token_changed");
826
853
  };
827
854
  let injected = false;
855
+ let homeSettings: HomeCodexSettingsSource | undefined;
828
856
  if (next.selectedClients.includes("codex")) {
829
- const config = loadConfig();
857
+ let config = loadConfig();
858
+ // #6957: a link Child applies the Home's OpenCodex-owned Codex settings with the catalog.
859
+ if (isLinkConnection(next)) {
860
+ homeSettings = await resolveHomeCodexSettings(next, initial.token.token, deps.linkHome);
861
+ if (homeSettings.source !== "local") config = overlayHomeCodexSettings(config, homeSettings.document.settings);
862
+ }
830
863
  const result = await injectCodexConfig(config.port, { ...config, syncResumeHistory: false }, {
831
864
  routingTarget: routingTarget(next.serverUrl, isLinkConnection(next) ? config.port : undefined, config), catalogPath: DEFAULT_CATALOG_PATH,
832
865
  journalOwner: { kind: "client", apiKeyId: next.apiKeyId }, beforeClientWrite,
833
866
  });
834
867
  if (!result.success || result.status === "skipped") throw new Error(result.message);
835
868
  injected = true;
869
+ if (homeSettings && next.link) recordHomeCodexSettings(next.link.linkId, homeSettings);
836
870
  }
837
871
  const cacheSynced = withClientLifecycleSync(() => {
838
872
  beforeClientWrite();
839
873
  // Cache invalidation acquires K itself (N -> K -> C); never call it while C is held.
840
874
  return invalidateCodexModelsCache({ allowWhenDesiredDisabled: true });
841
875
  }, deps.lifecycleLockDeps);
842
- return { catalogWritten: downloaded !== undefined, cacheSynced, injected, stale };
876
+ return { catalogWritten: downloaded !== undefined, cacheSynced, injected, stale, ...(homeSettings ? { homeSettings } : {}) };
843
877
  }
844
878
 
845
879
  /**
@@ -1035,6 +1069,7 @@ export async function disconnectClient(
1035
1069
  if (!(error instanceof ClientLinkStateError)) throw error;
1036
1070
  }
1037
1071
  if (sidecarLinkId === connection.link.linkId) clearClientLinkState(connection.link.linkId);
1072
+ try { clearHomeCodexSettingsRecord(); } catch { /* derived data; never fails a disconnect */ }
1038
1073
  }
1039
1074
  removeHubStateCache();
1040
1075
  requireDesktopResult(finishRemoteDesktopCleanup(held, receipt.owner));
@@ -485,7 +485,7 @@ export async function fetchHubUsage(
485
485
  serverUrl: string,
486
486
  admissionToken: string,
487
487
  query: URLSearchParams,
488
- options: { timeoutMs?: number; fetchImpl?: typeof fetch } = {},
488
+ options: { timeoutMs?: number; fetchImpl?: typeof fetch; peer?: "hub" | "Home" } = {},
489
489
  ): Promise<HubUsageReport> {
490
490
  const origin = normalizeHubOrigin(serverUrl);
491
491
  if (!isHubTransportPermitted(origin)) {
@@ -498,9 +498,12 @@ export async function fetchHubUsage(
498
498
  }, options.timeoutMs);
499
499
  if (!response.ok) {
500
500
  try { await response.body?.cancel(); } catch { /* best effort */ }
501
- const message = response.status === 404 ? "Hub does not support client usage; upgrade the hub"
502
- : response.status === 401 || response.status === 403 ? "Hub rejected this client's usage credential"
503
- : `Hub usage request failed (${response.status})`;
501
+ // A Remote Link Child reads the same route from its Home (#6957).
502
+ const peer = options.peer ?? "hub";
503
+ const Peer = peer === "Home" ? "Home" : "Hub";
504
+ const message = response.status === 404 ? `${Peer} does not support client usage; upgrade the ${peer}`
505
+ : response.status === 401 || response.status === 403 ? `${Peer} rejected this ${peer === "Home" ? "Child's link key" : "client's usage credential"}`
506
+ : `${Peer} usage request failed (${response.status})`;
504
507
  throw new HubClientError(`hub_usage_http_${response.status}`, message, response.status);
505
508
  }
506
509
  if (!jsonCompatibleContentType(response)) {
@@ -527,16 +530,20 @@ export async function fetchHubUsage(
527
530
  */
528
531
  export async function fetchHubState(
529
532
  serverUrl: string,
530
- admissionToken: string,
533
+ admissionToken: string | null,
531
534
  options: { timeoutMs?: number; fetchImpl?: typeof fetch } = {},
532
535
  ): Promise<HubStateDTO> {
533
536
  const origin = normalizeHubOrigin(serverUrl);
534
537
  const response = await fetchBounded(options.fetchImpl ?? fetch, `${origin}/v1/hub-state`, {
535
538
  method: "GET",
536
- headers: new Headers({ Accept: "application/json", "x-opencodex-api-key": admissionToken }),
539
+ headers: new Headers({ Accept: "application/json", ...(admissionToken === null ? {} : { "x-opencodex-api-key": admissionToken }) }),
537
540
  }, options.timeoutMs, "headers");
538
541
  if (response.status === 404) {
539
- try { await response.body?.cancel(); } catch { /* best effort */ }
542
+ let body: unknown = null;
543
+ try { body = JSON.parse(await boundedText(response, 4096, { inactivityTimeoutMs: safeTimeout(options.timeoutMs) })); } catch { /* legacy/non-JSON 404 stays unsupported */ }
544
+ if (body && typeof body === "object" && !Array.isArray(body) && (body as { error?: { code?: unknown } }).error?.code === "hub_state_not_a_hub") {
545
+ throw new HubClientError("hub_state_not_a_hub", "the remote host does not publish hub state", 404);
546
+ }
540
547
  throw new HubClientError("hub_state_unsupported", "Hub does not serve /v1/hub-state; upgrade the hub", 404);
541
548
  }
542
549
  if (!response.ok) {
@@ -124,37 +124,43 @@ export function writeCachedHubState(owner: HubStateOwner, state: HubStateDTO, fe
124
124
  * banner reading `state unavailable (hub_state_http_507)` sends the reader looking for a client
125
125
  * bug when the hub has in fact answered and said something.
126
126
  */
127
- export function hubStateFailureReason(error: unknown): string {
127
+ export function hubStateFailureReason(error: unknown, peer: "hub" | "Home" = "hub"): string {
128
+ // A Remote Link Child reads the same route from its Home (#6957); the sentences name the peer
129
+ // the operator actually configured.
130
+ const holder = peer === "Home" ? "this Child's link key" : "this client's data key";
128
131
  if (error instanceof HubClientError) {
129
132
  switch (error.code) {
133
+ case "hub_state_not_a_hub":
134
+ return "the remote host does not publish hub state";
130
135
  case "hub_state_unsupported":
131
- return "this hub is too old to report its state; upgrade the hub";
136
+ return `this ${peer} is too old to report its state; upgrade the ${peer}`;
132
137
  case "hub_state_unauthorized":
133
- return "the hub rejected this client's data key";
138
+ return `the ${peer} rejected ${holder}`;
134
139
  case "hub_state_schema_invalid":
135
140
  case "hub_state_invalid":
136
- return "the hub returned an unreadable hub-state document";
141
+ return `the ${peer} returned an unreadable hub-state document`;
137
142
  case "hub_state_content_type_invalid":
138
143
  // Usually a captive portal, a TLS-terminating proxy or an error page in front of the
139
144
  // hub: the request reached SOMETHING, and that something is not the hub's API.
140
- return "the hub's state response was not JSON";
145
+ return `the ${peer}'s state response was not JSON`;
141
146
  case "body_too_large":
142
- return "the hub's state response exceeded the allowed size";
147
+ return `the ${peer}'s state response exceeded the allowed size`;
143
148
  case "unreachable":
144
- return "the hub is unreachable";
149
+ return `the ${peer} is unreachable`;
145
150
  case "redirect_refused":
146
- return "the hub redirected the state request";
151
+ return `the ${peer} redirected the state request`;
147
152
  default: {
148
153
  const status = error.code.startsWith("hub_state_http_")
149
154
  ? error.code.slice("hub_state_http_".length)
150
155
  : null;
151
156
  return status && /^\d+$/.test(status)
152
- ? `the hub answered HTTP ${status} to the state request`
157
+ ? `the ${peer} answered HTTP ${status} to the state request`
153
158
  : error.code;
154
159
  }
155
160
  }
156
161
  }
157
- return "the hub state could not be read";
162
+ if (error instanceof Error && error.name === "LinkRelayAuthenticationError") return error.message;
163
+ return `the ${peer} state could not be read`;
158
164
  }
159
165
 
160
166
  export interface ResolveHubStateOptions {
@@ -167,6 +173,8 @@ export interface ResolveHubStateOptions {
167
173
  * token for a different connection, and the operator acts on that difference.
168
174
  */
169
175
  withheldTokenReason?: string;
176
+ /** Identity-checked local Child transport that strips credentials and owns Home admission. */
177
+ localRelayFetchImpl?: typeof fetch;
170
178
  timeoutMs?: number;
171
179
  fetchImpl?: typeof fetch;
172
180
  now?: number;
@@ -174,6 +182,8 @@ export interface ResolveHubStateOptions {
174
182
  allowNetwork?: boolean;
175
183
  /** False skips the cache write, for read-only callers. */
176
184
  persist?: boolean;
185
+ /** Which peer the sentences name: a hub, or a Remote Link Child's Home. */
186
+ peer?: "hub" | "Home";
177
187
  }
178
188
 
179
189
  function withAge(
@@ -201,20 +211,21 @@ export async function resolveHubState(options: ResolveHubStateOptions): Promise<
201
211
  ? withAge("cache", cached.state, cached.fetchedAt, now, reason)
202
212
  : withAge("unavailable", null, undefined, now, reason);
203
213
  };
204
- if (!options.token) {
214
+ if (!options.token && !options.localRelayFetchImpl) {
205
215
  return fromCache(options.withheldTokenReason ?? "this client has no usable data-plane token");
206
216
  }
207
217
  if (options.allowNetwork === false) return fromCache("a live hub read was not attempted");
208
218
  let state: HubStateDTO;
209
219
  try {
210
- state = await fetchHubState(options.owner.serverUrl, options.token, {
220
+ state = await fetchHubState(options.owner.serverUrl, options.localRelayFetchImpl ? null : options.token, {
211
221
  timeoutMs: options.timeoutMs ?? DEFAULT_HUB_STATE_TIMEOUT_MS,
212
- ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
222
+ ...((options.localRelayFetchImpl ?? options.fetchImpl) ? { fetchImpl: options.localRelayFetchImpl ?? options.fetchImpl } : {}),
213
223
  });
214
224
  } catch (error) {
215
225
  // Deliberately no local-state fallback here. A stale cache is still the HUB's state; the
216
226
  // client's own providers and logins are not, at any age.
217
- return fromCache(hubStateFailureReason(error));
227
+ if (error instanceof HubClientError && error.code === "hub_state_not_a_hub") return withAge("unavailable", null, undefined, now, hubStateFailureReason(error, options.peer));
228
+ return fromCache(hubStateFailureReason(error, options.peer));
218
229
  }
219
230
  const fetchedAt = new Date(now).toISOString();
220
231
  if (options.persist !== false) writeCachedHubState(options.owner, state, fetchedAt);