@bitkyc08/opencodex 2.90.0 → 2.92.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 (250) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/App-DMps4OLk.css +1 -0
  3. package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
  4. package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
  5. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  6. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  9. package/package.json +4 -3
  10. package/src/adapters/anthropic.ts +2 -5
  11. package/src/adapters/claude-cli/usage-admission.ts +14 -0
  12. package/src/adapters/google-vertex-endpoint.ts +9 -1
  13. package/src/adapters/google-vertex-key-project.ts +223 -0
  14. package/src/adapters/google.ts +13 -3
  15. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  16. package/src/adapters/openai-chat.ts +2 -2
  17. package/src/adapters/openai-responses/passthrough.ts +2 -1
  18. package/src/adapters/openai-responses/request-strips.ts +92 -0
  19. package/src/adapters/openai-responses/transport.ts +6 -0
  20. package/src/adapters/registry.ts +5 -0
  21. package/src/adapters/workbuddy-schema.ts +61 -0
  22. package/src/adapters/workbuddy.ts +68 -0
  23. package/src/bridge/response-json.ts +1 -1
  24. package/src/bridge/sse.ts +1 -1
  25. package/src/chat/inbound.ts +20 -2
  26. package/src/chat/outbound.ts +47 -8
  27. package/src/chat/reasoning-details.ts +137 -0
  28. package/src/cli/capabilities-agents-routing.ts +53 -3
  29. package/src/cli/capabilities-base.ts +15 -2
  30. package/src/cli/capabilities-observe-system.ts +4 -4
  31. package/src/cli/codex-cli-update.ts +8 -0
  32. package/src/cli/codex-shim-autorestore.ts +5 -0
  33. package/src/cli/combo-result.ts +6 -0
  34. package/src/cli/combo-stats.ts +3 -0
  35. package/src/cli/combo.ts +66 -42
  36. package/src/cli/dispatch.ts +30 -0
  37. package/src/cli/fallback.ts +127 -0
  38. package/src/cli/help.ts +1 -0
  39. package/src/cli/index.ts +1 -2
  40. package/src/cli/launcher-context.ts +49 -2
  41. package/src/cli/link-apply-restart.ts +72 -0
  42. package/src/cli/link-child.ts +6 -2
  43. package/src/cli/link-cleanup.ts +95 -0
  44. package/src/cli/link-enrollment.ts +4 -0
  45. package/src/cli/link.ts +123 -35
  46. package/src/cli/provider.ts +46 -3
  47. package/src/cli/registry.ts +13 -1
  48. package/src/cli/restart-failure.ts +4 -0
  49. package/src/cli/runtime-api.ts +1 -1
  50. package/src/cli/status-codex-settings.ts +56 -0
  51. package/src/cli/status-fallback.ts +29 -0
  52. package/src/cli/status-install.ts +52 -0
  53. package/src/cli/status.ts +20 -1
  54. package/src/cli/system-restart-client.ts +32 -1
  55. package/src/cli/update-restart-child.ts +4 -3
  56. package/src/cli/update-restart-home.ts +20 -8
  57. package/src/cli/usage-compare.ts +68 -12
  58. package/src/client/connect.ts +82 -6
  59. package/src/client/home-unreachable.ts +16 -0
  60. package/src/client/hub-relay.ts +8 -2
  61. package/src/client/link-home.ts +119 -6
  62. package/src/client/link-ingress.ts +1 -1
  63. package/src/client/link-parity-reporter.ts +107 -0
  64. package/src/client/link-readiness.ts +97 -0
  65. package/src/client/link-relay-transport.ts +84 -9
  66. package/src/client/link-relay.ts +98 -15
  67. package/src/client/link-tunnel.ts +45 -18
  68. package/src/client/machine-listener.ts +103 -3
  69. package/src/client/runtime-restart.ts +127 -0
  70. package/src/client/runtime.ts +155 -25
  71. package/src/codex/account-store.ts +17 -3
  72. package/src/codex/app-server-restart-service.ts +6 -5
  73. package/src/codex/auth-context.ts +10 -6
  74. package/src/codex/catalog/effort.ts +9 -2
  75. package/src/codex/catalog/gather-capture.ts +4 -2
  76. package/src/codex/catalog/parsing.ts +4 -0
  77. package/src/codex/catalog/provider-models.ts +14 -8
  78. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  79. package/src/codex/cli-install-provenance.ts +418 -0
  80. package/src/codex/cli-installation-targets.ts +5 -1
  81. package/src/codex/cli-update-plan.ts +43 -16
  82. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  83. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  84. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  85. package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
  86. package/src/codex/pool-refresh-backoff.ts +3 -1
  87. package/src/codex/quota-history.ts +7 -0
  88. package/src/codex/quota.ts +10 -0
  89. package/src/codex/subagent-model-fallback.ts +24 -1
  90. package/src/codex/user-identity.ts +35 -6
  91. package/src/codex/windows-current-sid.ts +162 -0
  92. package/src/combos/index.ts +1 -0
  93. package/src/combos/jev-quota-activation.ts +5 -1
  94. package/src/combos/jev-quota-route.ts +1 -1
  95. package/src/combos/jev-quota.ts +9 -1
  96. package/src/combos/jev-session-pin-config.ts +51 -0
  97. package/src/combos/jev.ts +6 -0
  98. package/src/combos/resolve.ts +12 -0
  99. package/src/combos/types.ts +19 -0
  100. package/src/config/diagnostics.ts +5 -0
  101. package/src/config/load-degrade.ts +2 -0
  102. package/src/config/provider-validation.ts +7 -0
  103. package/src/config/schema/config-schema.ts +3 -0
  104. package/src/config/schema/fallback-policy.ts +40 -0
  105. package/src/config/schema/leaf-validators.ts +1 -0
  106. package/src/generated/compatibility-version.json +375 -195
  107. package/src/lib/authorization-scheme.ts +95 -0
  108. package/src/lib/decision-quota-sink.ts +10 -0
  109. package/src/lib/provider-client-headers.ts +1 -0
  110. package/src/lib/redact.ts +2 -2
  111. package/src/lib/system-restart-contract.ts +24 -0
  112. package/src/lib/test-home-guard.ts +78 -11
  113. package/src/lib/translator-budget.ts +4 -4
  114. package/src/lib/upstream-retry.ts +4 -1
  115. package/src/link/child-parity.ts +124 -0
  116. package/src/link/codex-settings.ts +37 -0
  117. package/src/link/compatibility.ts +119 -0
  118. package/src/link/diagnostics.ts +193 -0
  119. package/src/link/management-routes.ts +2 -0
  120. package/src/link/peer-report.ts +100 -0
  121. package/src/link/relay-auth.ts +6 -0
  122. package/src/link/remote-platform.ts +78 -0
  123. package/src/link/ssh-argv.ts +19 -0
  124. package/src/link/status-projection.ts +63 -2
  125. package/src/link/supervisor.ts +45 -12
  126. package/src/link/tunnel-history.ts +109 -0
  127. package/src/link/tunnel-state.ts +80 -17
  128. package/src/messaging/remote-files.ts +24 -1
  129. package/src/oauth/generic-account-failover.ts +91 -7
  130. package/src/oauth/index.ts +16 -1
  131. package/src/oauth/kimi.ts +2 -0
  132. package/src/oauth/model-discovery-auth.ts +5 -0
  133. package/src/oauth/nous.ts +4 -1
  134. package/src/oauth/store.ts +6 -2
  135. package/src/oauth/token-guardian.ts +7 -0
  136. package/src/oauth/types.ts +9 -0
  137. package/src/oauth/workbuddy.ts +139 -0
  138. package/src/protocols/codecs/chat.ts +3 -3
  139. package/src/protocols/encoders/adapter-events.ts +26 -2
  140. package/src/protocols/encoders/chat.ts +55 -3
  141. package/src/providers/antigravity-quota-identity.ts +37 -0
  142. package/src/providers/base-url-choices.ts +8 -5
  143. package/src/providers/derive.ts +23 -3
  144. package/src/providers/mistral-transport.ts +37 -4
  145. package/src/providers/model-discovery.ts +2 -0
  146. package/src/providers/model-rename-fields.ts +1 -0
  147. package/src/providers/quota/account-cache.ts +3 -2
  148. package/src/providers/quota/antigravity.ts +27 -22
  149. package/src/providers/quota/report-cache.ts +13 -4
  150. package/src/providers/quota/vendor-probes-key.ts +60 -0
  151. package/src/providers/quota/workbuddy.ts +83 -0
  152. package/src/providers/quota-decision-antigravity.ts +64 -0
  153. package/src/providers/quota-decision-publication.ts +11 -0
  154. package/src/providers/quota-decision-snapshot.ts +21 -4
  155. package/src/providers/quota-types.ts +2 -0
  156. package/src/providers/quota.ts +3 -1
  157. package/src/providers/registry/entries-core.ts +9 -0
  158. package/src/providers/registry/entries-extended.ts +20 -7
  159. package/src/providers/registry/model-ids.ts +3 -0
  160. package/src/providers/registry/model-seeds.ts +27 -28
  161. package/src/providers/registry/types.ts +19 -1
  162. package/src/providers/resolved-model-policy.ts +4 -4
  163. package/src/providers/routed-config.ts +15 -0
  164. package/src/providers/stale-model-roster-migration.ts +83 -15
  165. package/src/providers/workbuddy-credit-balance.ts +81 -0
  166. package/src/providers/workbuddy-models.ts +37 -0
  167. package/src/providers/workbuddy.ts +60 -0
  168. package/src/quota-pool/assemble.ts +326 -0
  169. package/src/quota-pool/contract.ts +58 -0
  170. package/src/quota-pool/identity.ts +203 -0
  171. package/src/remote/features.ts +74 -0
  172. package/src/remote/protocol.ts +2 -0
  173. package/src/remote/served-by.ts +57 -0
  174. package/src/responses/parser.ts +7 -0
  175. package/src/responses/reasoning-envelope.ts +12 -0
  176. package/src/responses/reasoning-replay-cache.ts +18 -14
  177. package/src/router.ts +2 -0
  178. package/src/routing/analytics.ts +2 -3
  179. package/src/routing/fallback-policy.ts +290 -0
  180. package/src/routing/fallback-state.ts +179 -0
  181. package/src/server/audio-client.ts +2 -1
  182. package/src/server/audio-dictation.ts +2 -1
  183. package/src/server/audio-upstream.ts +2 -1
  184. package/src/server/auth-cors.ts +7 -5
  185. package/src/server/background-lifecycle.ts +4 -0
  186. package/src/server/chat-completions.ts +22 -2
  187. package/src/server/chat-native-reasoning.ts +10 -3
  188. package/src/server/chat-native.ts +13 -2
  189. package/src/server/claude-messages.ts +3 -0
  190. package/src/server/gui-session.ts +2 -1
  191. package/src/server/index/link-listener.ts +11 -1
  192. package/src/server/index/link-management.ts +47 -0
  193. package/src/server/index/link-relay-sessions.ts +20 -7
  194. package/src/server/index/optional-listeners.ts +11 -2
  195. package/src/server/index/serve-options.ts +19 -2
  196. package/src/server/live.ts +5 -0
  197. package/src/server/local-management-read-client.ts +3 -1
  198. package/src/server/management/combo-routes.ts +10 -4
  199. package/src/server/management/context.ts +1 -1
  200. package/src/server/management/fallback-routes.ts +75 -0
  201. package/src/server/management/link-enrollment.ts +21 -3
  202. package/src/server/management/link-route-state.ts +2 -0
  203. package/src/server/management/link-routes.ts +16 -2
  204. package/src/server/management/provider-routes.ts +1 -1
  205. package/src/server/management/quota-pool-routes.ts +24 -0
  206. package/src/server/management/route-registry.ts +3 -0
  207. package/src/server/management-api.ts +16 -0
  208. package/src/server/management-auth.ts +2 -1
  209. package/src/server/port-reclaim.ts +51 -9
  210. package/src/server/request-log-conversation.ts +49 -0
  211. package/src/server/request-log.ts +2 -0
  212. package/src/server/responses/agent-task-recovery.ts +1 -1
  213. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  214. package/src/server/responses/compact.ts +12 -4
  215. package/src/server/responses/core-auth.ts +11 -1
  216. package/src/server/responses/core-combo.ts +125 -32
  217. package/src/server/responses/core-lifetime.ts +2 -2
  218. package/src/server/responses/core-normalize.ts +6 -3
  219. package/src/server/responses/core-opaque-recovery.ts +1 -0
  220. package/src/server/responses/core-options.ts +5 -0
  221. package/src/server/responses/core-replay.ts +2 -1
  222. package/src/server/responses/encrypted-payload.ts +3 -3
  223. package/src/server/responses/jev-combo-choice.ts +39 -0
  224. package/src/server/responses/jev-session-pin.ts +203 -0
  225. package/src/server/responses/main-hard-lock-routing.ts +54 -2
  226. package/src/server/responses/passthrough-delivery.ts +4 -0
  227. package/src/server/responses/policy-fallback.ts +44 -1
  228. package/src/server/responses/request-prepare.ts +19 -6
  229. package/src/server/responses/request-transport.ts +25 -6
  230. package/src/server/responses/run-turn-execution.ts +2 -4
  231. package/src/server/responses/sidecar-execution.ts +2 -4
  232. package/src/server/responses-phase-inference.ts +267 -0
  233. package/src/server/restart-replacement.ts +50 -2
  234. package/src/server/sse-payload-rewrite.ts +14 -1
  235. package/src/server/ws-bridge.ts +5 -0
  236. package/src/service/cli.ts +25 -2
  237. package/src/service/definition.ts +20 -8
  238. package/src/service/diagnostics.ts +20 -6
  239. package/src/service/status-json.ts +116 -0
  240. package/src/types/config.ts +13 -0
  241. package/src/types/fallback.ts +38 -0
  242. package/src/types/provider.ts +9 -1
  243. package/src/types/request.ts +1 -0
  244. package/src/types/wire.ts +1 -0
  245. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  246. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  247. package/src/usage/jev-stats.ts +54 -14
  248. package/gui/dist/assets/App-dnkJ85te.css +0 -1
  249. package/gui/dist/assets/index-TmaHa54u.js +0 -94
  250. package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
@@ -150,7 +150,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
150
150
  },
151
151
  {
152
152
  name: "link",
153
- usage: "ocx link <port|issue|status|revoke|reconnect>",
153
+ usage: "ocx link <port|issue|status|revoke|reconnect|cleanup>",
154
154
  summary: "Allocate and manage a loopback remote home link.",
155
155
  details: [
156
156
  "Port: ocx link port [--json]",
@@ -158,6 +158,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
158
158
  "Status: ocx link status [--json]",
159
159
  "Revoke: ocx link revoke --link-id <id> [--force --yes] [--json]",
160
160
  "Reconnect: ocx link reconnect --link-id <id> [--json]",
161
+ "Cleanup: ocx link cleanup [--link-id <id>] [--json]",
161
162
  "Issue, status, revoke, and reconnect use the running proxy's loopback management API and admin token.",
162
163
  ],
163
164
  },
@@ -434,6 +435,17 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
434
435
  "Works both online (via live proxy API) and offline (modifies persisted config safely with atomic writes).",
435
436
  ],
436
437
  },
438
+ {
439
+ name: "fallback",
440
+ usage: "ocx fallback <status|off|on|clear|local> [--kind model|provider|all] [--provider <name> | --route <selector> | --device <id|alias>] [--json]",
441
+ summary: "Turn model and provider fallback off or on per scope, and record per-device local fallback.",
442
+ details: [
443
+ "A refused fallback answers fallback_refused with the status of the failure that triggered it, and names the command that allows it.",
444
+ "Precedence: a device set to off always refuses; otherwise route, then provider, then global; unset allows.",
445
+ "Rotation among the accounts or keys of one provider pool is not a fallback and is not governed here.",
446
+ "Live proxy only. On a Remote Link Child, status reads the Home; writes are refused. Run write commands on the Home.",
447
+ ],
448
+ },
437
449
  {
438
450
  name: "agent",
439
451
  usage: "ocx agent <status|injection|effort|subagents|fallback|sidecar> ...",
@@ -16,6 +16,10 @@ export function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: f
16
16
  console.error("The proxy home or lifecycle ownership could not be verified. Run `ocx status`; if a service owns the proxy, use its installation's `ocx service restart`, otherwise use the owning app or supervisor. No changes were made.");
17
17
  } else if (code === "restart_package_tree_unsettled") {
18
18
  console.error(`The proxy's package files are unsettled. Wait for the install to finish, then run \`ocx restart\`. ${INCOMPLETE_INSTALL_RECOVERY}`);
19
+ } else if (code === "restart_supervised_service") {
20
+ console.error("Restart not performed; the running client runtime was kept. Its background service owns it: run `ocx service restart`.");
21
+ } else if (code === "restart_supervised_desktop") {
22
+ console.error("Restart not performed; the running client runtime was kept. The OpenCodex desktop app owns it: restart it from the app.");
19
23
  } else {
20
24
  console.error("Proxy restart request could not be confirmed; no fallback stop/start was attempted. Run `ocx status` to inspect ownership and runtime state.");
21
25
  }
@@ -202,7 +202,7 @@ export async function runtimeRequest<T = unknown>(
202
202
  if (!linkChildReadable(method, path)) {
203
203
  throw new RuntimeApiError(linkChildRefusal(link, `${method} ${pathname}`), 503, null, "link_child_management_unavailable");
204
204
  }
205
- const { status, body } = await readLinkHomeJson(link, path, deps.linkHome);
205
+ const { status, body } = await readLinkHomeJson(link, path, deps.linkHome, init.signal ? { signal: init.signal } : {});
206
206
  if (status < 200 || status >= 300) throw new RuntimeApiError(responseMessage(body, status), status, body);
207
207
  return body as T;
208
208
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The OCX-6 fields of `ocx status --json`: which Codex settings this machine applied, when, and
3
+ * when its client runtime started.
4
+ *
5
+ * - `appliedCodexSettingsDigest` (any device): the digest of the values this machine applies. On a
6
+ * Remote Link Child that is its apply record (the Home's values its last sync wrote); on a Home or
7
+ * standalone install, its own projection, which is also exactly what it serves its Children. Null
8
+ * on a hub client and on a Child that has no apply record yet.
9
+ * - `lastApplyAt` (Child): epoch ms of the last successful apply of a different Home settings
10
+ * document; null elsewhere.
11
+ * - `runtimeStartedAt` (Child): epoch ms the live client runtime reports as its start; null when no
12
+ * client runtime answers.
13
+ */
14
+ import type { LiveProxy } from "../server/proxy-liveness";
15
+ import type { OcxConfig } from "../types";
16
+
17
+ export interface CliAppliedCodexSettingsStatus {
18
+ appliedCodexSettingsDigest: string | null;
19
+ lastApplyAt: number | null;
20
+ runtimeStartedAt: number | null;
21
+ }
22
+
23
+ export interface AppliedCodexSettingsStatusDeps {
24
+ readStartedAt?: (live: LiveProxy) => Promise<number | null>;
25
+ }
26
+
27
+ export async function collectAppliedCodexSettingsStatus(
28
+ config: OcxConfig,
29
+ live: LiveProxy | null,
30
+ deps: AppliedCodexSettingsStatusDeps = {},
31
+ ): Promise<CliAppliedCodexSettingsStatus> {
32
+ const { codexSettingsDigest, projectHomeCodexSettings } = await import("../link/codex-settings");
33
+ if (config.runtimeRole !== "client") {
34
+ return {
35
+ appliedCodexSettingsDigest: codexSettingsDigest(projectHomeCodexSettings(config, "0.0.0").settings),
36
+ lastApplyAt: null,
37
+ runtimeStartedAt: null,
38
+ };
39
+ }
40
+ const linkId = config.client?.transport === "link" ? config.client.link?.linkId : undefined;
41
+ if (!linkId) return { appliedCodexSettingsDigest: null, lastApplyAt: null, runtimeStartedAt: null };
42
+ const { readHomeCodexSettingsRecord } = await import("../client/link-home");
43
+ const record = readHomeCodexSettingsRecord(linkId);
44
+ const appliedAt = record ? Date.parse(record.appliedAt) : Number.NaN;
45
+ let runtimeStartedAt: number | null = null;
46
+ if (live?.role === "client") {
47
+ const read = deps.readStartedAt ?? (async (target: LiveProxy) =>
48
+ (await import("./link-apply-restart")).readClientRuntimeStartedAt(target));
49
+ runtimeStartedAt = await read(live);
50
+ }
51
+ return {
52
+ appliedCodexSettingsDigest: record ? codexSettingsDigest(record.document.settings) : null,
53
+ lastApplyAt: Number.isFinite(appliedAt) ? appliedAt : null,
54
+ runtimeStartedAt,
55
+ };
56
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `ocx status --json` `fallback` (OCX-11). The live proxy's `GET /api/fallback` when it answers
3
+ * (on a Remote Link Child that read is the Home's, over the link); otherwise the policy from this
4
+ * machine's config with no events, marked `source: "local"`. Never fails the status report.
5
+ */
6
+ import type { OcxConfig } from "../types";
7
+ import { buildFallbackState, type FallbackStateView } from "../routing/fallback-state";
8
+ import { runtimeRequest, type RuntimeApiDeps } from "./runtime-api";
9
+
10
+ export type StatusFallback = (FallbackStateView | ReturnType<typeof buildFallbackState>) & { source: "live" | "home" | "local" };
11
+
12
+ const STATUS_FALLBACK_TIMEOUT_MS = 2_000;
13
+
14
+ export async function collectStatusFallback(
15
+ config: Pick<OcxConfig, "fallback">,
16
+ options: { live: boolean; linkChild: boolean; deps?: RuntimeApiDeps },
17
+ ): Promise<StatusFallback> {
18
+ if (options.live) {
19
+ try {
20
+ const state = await runtimeRequest<FallbackStateView>("/api/fallback", {
21
+ signal: AbortSignal.timeout(STATUS_FALLBACK_TIMEOUT_MS),
22
+ }, options.deps ?? {});
23
+ if (state && typeof state === "object" && state.schema === "ocx.fallback/1") {
24
+ return { ...state, source: options.linkChild ? "home" : "live" };
25
+ }
26
+ } catch { /* an older or unreachable proxy: report the local policy instead */ }
27
+ }
28
+ return { ...buildFallbackState(options.linkChild ? {} : config), source: "local" };
29
+ }
@@ -0,0 +1,52 @@
1
+ import { detectInstallOwnership, type InstallOwnership } from "../update";
2
+
3
+ /** How this ocx is installed, from the updater's own ownership detector. */
4
+ export type InstallMethod = "bun" | "npm" | "pnpm" | "mise" | "source" | "standalone" | "unknown";
5
+
6
+ /**
7
+ * `ocx status --json` `install`. Additive within `schemaVersion: 1`.
8
+ *
9
+ * The verdict is the one `ocx update` acts on (`src/update/install-detection.mjs`), read
10
+ * from the running package's own location — PATH is never consulted, because a machine can
11
+ * carry several package managers at once. `evidence` names what decided it without adding
12
+ * a path: the package layout, mise's backend record (tool alias and backend only), the
13
+ * compiled standalone binary, or nothing when detection itself failed.
14
+ */
15
+ export type InstallDetectionStatus = {
16
+ method: InstallMethod;
17
+ evidence: {
18
+ basis: "package-layout" | "mise-backend-record" | "standalone-binary" | "unavailable";
19
+ mise?: { tool: string; backend: string };
20
+ error?: "metadata_unreadable" | "metadata_inconsistent" | "detection_failed";
21
+ };
22
+ };
23
+
24
+ export function installDetectionStatus(
25
+ detect: () => InstallOwnership = detectInstallOwnership,
26
+ ): InstallDetectionStatus {
27
+ let ownership: InstallOwnership;
28
+ try {
29
+ ownership = detect();
30
+ } catch {
31
+ return { method: "unknown", evidence: { basis: "unavailable", error: "detection_failed" } };
32
+ }
33
+ switch (ownership.installer) {
34
+ case "standalone":
35
+ return { method: "standalone", evidence: { basis: "standalone-binary" } };
36
+ case "mise":
37
+ if (ownership.owner) {
38
+ return {
39
+ method: "mise",
40
+ evidence: { basis: "mise-backend-record", mise: { tool: ownership.owner.tool, backend: ownership.owner.backend } },
41
+ };
42
+ }
43
+ return { method: "mise", evidence: { basis: "mise-backend-record", error: ownership.error ?? "metadata_unreadable" } };
44
+ case "bun":
45
+ case "npm":
46
+ case "pnpm":
47
+ case "source":
48
+ return { method: ownership.installer, evidence: { basis: "package-layout" } };
49
+ default:
50
+ return { method: "unknown", evidence: { basis: "unavailable", error: "detection_failed" } };
51
+ }
52
+ }
package/src/cli/status.ts CHANGED
@@ -36,6 +36,8 @@ export type { ListenTarget } from "./status-probes";
36
36
  import { checkProxyHealth, probeUncleanExitState, type ListenTarget } from "./status-probes";
37
37
  import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
38
38
  import { fetchBoundLocalManagementRead } from "../server/local-management-read-client";
39
+ import { collectAppliedCodexSettingsStatus, type CliAppliedCodexSettingsStatus } from "./status-codex-settings";
40
+ import { installDetectionStatus, type InstallDetectionStatus } from "./status-install";
39
41
 
40
42
  /**
41
43
  * The state of the data-plane admission secret the SERVICE will use. State only -- never the value.
@@ -118,6 +120,8 @@ export type CliRemoteHubStatus = {
118
120
 
119
121
  export type CliStatusJson = {
120
122
  mainAccountHardLock?: CodexMainAccountPolicyHealth;
123
+ /** OCX-11 fallback state; see src/cli/status-fallback.ts. */
124
+ fallback?: import("./status-fallback").StatusFallback;
121
125
  schemaVersion: 1;
122
126
  /**
123
127
  * This machine's topology role, named rather than inferred (#4236).
@@ -229,8 +233,10 @@ export type CliStatusJson = {
229
233
  */
230
234
  versionSkew: VersionSkew;
231
235
  cliCommand: CliPathDiagnostics;
236
+ /** Installer ownership from the updater's detector (OCX-10); additive, `schemaVersion` stays 1. */
237
+ install: InstallDetectionStatus;
232
238
  startupSource: "live" | "local" | "local-supervision-override";
233
- };
239
+ } & CliAppliedCodexSettingsStatus;
234
240
 
235
241
  export type CliStatusView = {
236
242
  json: CliStatusJson;
@@ -853,12 +859,22 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
853
859
  const mainAccountHardLock = options.mainAccountPolicy && live
854
860
  ? (await (await import("../oauth/health")).fetchCodexHealthFromLiveProxy(undefined, async () => live)).mainAccountHardLock
855
861
  : undefined;
862
+ // Same `--json`-only gate as the hard-lock read above: the human report stays one probe.
863
+ const fallback = options.mainAccountPolicy
864
+ ? await (await import("./status-fallback")).collectStatusFallback(config, {
865
+ live: Boolean(live), linkChild: config.client?.transport === "link",
866
+ deps: { findLiveProxy: async () => live },
867
+ })
868
+ : undefined;
856
869
  const pidFile = readPid();
857
870
  // Preserve an authoritative null from orphan/legacy liveness — do not restore pidFile.
858
871
  const pid = resolveStatusPid(live, pidFile);
859
872
  // No extra request: findLiveProxy's identity probe already parsed and validated the
860
873
  // healthz body, so the version came back with the liveness result.
861
874
  const versionSkew = computeVersionSkew(packageVersion(), live?.version);
875
+ // OCX-6: what this machine applied and, on a Child, when its runtime started. Additive and
876
+ // always present (null when unknown), so `schemaVersion` stays 1.
877
+ const appliedCodexSettings = await collectAppliedCodexSettingsStatus(config, live);
862
878
  const listen = live
863
879
  ? {
864
880
  port: live.port,
@@ -1005,6 +1021,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
1005
1021
  healthLabel: formatCliStatusHealthLabel(health.label, cliCommand, remoteHub.connected),
1006
1022
  json: {
1007
1023
  ...(mainAccountHardLock ? { mainAccountHardLock } : {}),
1024
+ ...(fallback ? { fallback } : {}),
1008
1025
  schemaVersion: 1,
1009
1026
  runtimeRole: config.runtimeRole ?? "standalone",
1010
1027
  proxy: {
@@ -1067,7 +1084,9 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
1067
1084
  // print it under the wrong heading (#2701).
1068
1085
  versionSkew,
1069
1086
  cliCommand,
1087
+ install: installDetectionStatus(),
1070
1088
  startupSource,
1089
+ ...appliedCodexSettings,
1071
1090
  },
1072
1091
  };
1073
1092
  }
@@ -1,4 +1,5 @@
1
1
  import { readRuntimePort } from "../config/process-state";
2
+ import { readBoundedResponseBytes } from "../lib/bounded-body";
2
3
  import {
3
4
  LOCAL_ATTESTATION_CHALLENGE_HEADER,
4
5
  LOCAL_ATTESTATION_PROOF_HEADER,
@@ -6,6 +7,8 @@ import {
6
7
  verifyLocalAttestationProof,
7
8
  } from "../lib/local-management-attestation";
8
9
  import {
10
+ CLIENT_RESTART_REQUEST_MIN_REMAINING_MS,
11
+ RESTART_SUPERVISED_ERROR,
9
12
  SYSTEM_RESTART_CAPABILITY_HEADER,
10
13
  SYSTEM_RESTART_CAPABILITY_VERSION,
11
14
  SYSTEM_RESTART_EXPECTED_PID_HEADER,
@@ -182,6 +185,13 @@ export async function requestBoundSystemRestart(
182
185
  );
183
186
  if (!capability) return rejected("restart_capability_unavailable");
184
187
 
188
+ // A connected client runtime keeps serving when its replacement fails, but its rollback takes
189
+ // up to CLIENT_RESTART_BUDGET_MS. Ask only while that still ends before this CLI stops observing,
190
+ // or the recovery start below could race the rollback for the port (OCX-6).
191
+ if (body.role === "client" && deadlineAt - now() < CLIENT_RESTART_REQUEST_MIN_REMAINING_MS) {
192
+ return rejected("restart_deadline_expired");
193
+ }
194
+
185
195
  const requestBudget = remaining(deadlineAt, now, SYSTEM_RESTART_REQUEST_TIMEOUT_MS);
186
196
  if (requestBudget <= 0) return rejected("restart_deadline_expired");
187
197
  try {
@@ -194,10 +204,31 @@ export async function requestBoundSystemRestart(
194
204
  },
195
205
  signal: AbortSignal.timeout(requestBudget),
196
206
  });
197
- return response.ok ? { accepted: true } : rejected(`restart_request_http_${response.status}`);
207
+ if (response.ok) return { accepted: true };
208
+ if (response.status === 409) {
209
+ const supervisor = await supervisedRefusal(response);
210
+ if (supervisor) return rejected(`${RESTART_SUPERVISED_ERROR}_${supervisor}`);
211
+ }
212
+ return rejected(`restart_request_http_${response.status}`);
198
213
  } catch {
199
214
  // The server may have accepted the restart before the response connection failed.
200
215
  // The coordinator observes the original PID for replacement instead of replaying.
201
216
  return uncertain("restart_request_outcome_unknown");
202
217
  }
203
218
  }
219
+
220
+ /**
221
+ * A client runtime under the desktop app or a service answers 409 `restart_supervised` and keeps
222
+ * serving (OCX-6). Read that small body and return the validated supervisor, or null.
223
+ */
224
+ async function supervisedRefusal(response: Response): Promise<"service" | "desktop" | null> {
225
+ try {
226
+ const read = await readBoundedResponseBytes(response, { maxBytes: 1024, signal: AbortSignal.timeout(SYSTEM_RESTART_REQUEST_TIMEOUT_MS) });
227
+ if (read.oversized) return null;
228
+ const body = JSON.parse(new TextDecoder().decode(read.bytes)) as { error?: { code?: unknown; supervisor?: unknown } } | null;
229
+ if (body?.error?.code !== RESTART_SUPERVISED_ERROR) return null;
230
+ return body.error.supervisor === "service" || body.error.supervisor === "desktop" ? body.error.supervisor : null;
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
@@ -20,10 +20,11 @@ function parseMarker(raw: string): UpdateRestartChildMarker {
20
20
  || typeof value.home.serviceRecord.digest !== "string" || !/^[a-f0-9]{64}$/.test(value.home.serviceRecord.digest)) throw new Error("update_restart_child_marker_invalid");
21
21
  for (const directory of [value.home.config, value.home.codex]) {
22
22
  // Windows reports 64-bit NTFS file ids (MFT sequence in the high 16 bits) as doubles above
23
- // 2^53. Parent and child read them through the same stat API, so any finite integer is a
24
- // faithful identity token; assertUpdateRestartHome still compares path, dev and ino exactly.
23
+ // 2^53, where neighbouring ids collapse. dev and ino may be large finite integers; fileId
24
+ // carries the exact "dev:ino" that assertUpdateRestartHome actually relies on.
25
25
  if (!directory || typeof directory.path !== "string" || !directory.path
26
- || !Number.isInteger(directory.dev) || !Number.isInteger(directory.ino)) {
26
+ || !Number.isInteger(directory.dev) || !Number.isInteger(directory.ino)
27
+ || typeof directory.fileId !== "string" || !/^\d{1,20}:\d{1,20}$/.test(directory.fileId)) {
27
28
  throw new Error("update_restart_child_marker_invalid");
28
29
  }
29
30
  }
@@ -1,4 +1,4 @@
1
- import { realpathSync, statSync } from "node:fs";
1
+ import { realpathSync, statSync, type BigIntStats } from "node:fs";
2
2
  import { readClientConnectionState } from "../client/state";
3
3
  import { readConfigDiagnostics } from "../config/diagnostics";
4
4
  import { getConfigDir } from "../config/paths";
@@ -6,23 +6,34 @@ import { currentCodexHome } from "../service/state";
6
6
  import { captureUpdateRestartServiceRecord, type UpdateRestartServiceRecord, type UpdateRestartServiceRecordDeps } from "./update-restart-service-record";
7
7
  import { probeUpdateRestartSupervision, type UpdateRestartSupervisionDeps } from "./update-restart-supervision";
8
8
 
9
+ /**
10
+ * `fileId` is the exact identity ("dev:ino" from a bigint stat). `dev` and `ino` are doubles and
11
+ * cannot be: an NTFS file id carries the MFT sequence in its high bits, so above 2^53 a replacement
12
+ * directory with a nearby MFT index can round to the same `ino` as the one it replaced.
13
+ */
14
+ export interface UpdateRestartDirectory { path: string; dev: number; ino: number; fileId: string }
15
+
9
16
  export interface UpdateRestartHome {
10
- config: { path: string; dev: number; ino: number };
11
- codex: { path: string; dev: number; ino: number };
17
+ config: UpdateRestartDirectory;
18
+ codex: UpdateRestartDirectory;
12
19
  revision: number;
13
20
  serviceRecord: UpdateRestartServiceRecord;
14
21
  }
15
22
 
16
- function physicalDirectory(path: string): UpdateRestartHome["config"] {
23
+ export type UpdateRestartDirectoryStat = Pick<BigIntStats, "dev" | "ino" | "isDirectory">;
24
+
25
+ function physicalDirectory(path: string, stat: (path: string) => UpdateRestartDirectoryStat): UpdateRestartDirectory {
17
26
  const canonical = realpathSync.native(path);
18
- const stat = statSync(canonical);
19
- if (!stat.isDirectory()) throw new Error("update_restart_home_unverified");
20
- return { path: canonical, dev: stat.dev, ino: stat.ino };
27
+ const observed = stat(canonical);
28
+ if (!observed.isDirectory()) throw new Error("update_restart_home_unverified");
29
+ return { path: canonical, dev: Number(observed.dev), ino: Number(observed.ino), fileId: `${observed.dev}:${observed.ino}` };
21
30
  }
22
31
 
23
32
  export interface UpdateRestartHomeDeps {
24
33
  record?: UpdateRestartServiceRecordDeps;
25
34
  supervision?: UpdateRestartSupervisionDeps;
35
+ /** Test seam for the directory identity source; production reads a bigint stat. */
36
+ statDirectory?: (path: string) => UpdateRestartDirectoryStat;
26
37
  }
27
38
 
28
39
  /** Capture eligibility from the same bytes that supply the frozen fingerprint. */
@@ -32,7 +43,8 @@ export function readUpdateRestartHome(deps: UpdateRestartHomeDeps = {}): UpdateR
32
43
  if (owner.kind !== "none" || (state.kind === "state" && platform !== "darwin" && platform !== "linux")) {
33
44
  throw new Error("update_restart_owner_unverified");
34
45
  }
35
- return { config: physicalDirectory(getConfigDir()), codex: physicalDirectory(currentCodexHome()), revision: owner.revision, serviceRecord };
46
+ const stat = deps.statDirectory ?? (path => statSync(path, { bigint: true }));
47
+ return { config: physicalDirectory(getConfigDir(), stat), codex: physicalDirectory(currentCodexHome(), stat), revision: owner.revision, serviceRecord };
36
48
  }
37
49
 
38
50
  export function assertUpdateRestartHome(expected: UpdateRestartHome, deadlineAt = Date.now() + 2000, deps: UpdateRestartHomeDeps = {}): void {
@@ -8,7 +8,11 @@ import { readClientConnectionState, sameClientConnectionOwner } from "../client/
8
8
  import { fetchHubUsage } from "../client/hub-client";
9
9
  import { readServiceApiTokenState, serviceApiTokenFingerprint } from "../lib/service-secrets";
10
10
  import { runningProxyUpdateHeaders } from "../oauth/login-cli";
11
+ import { readRuntimePort, type RuntimePortState } from "../config/process-state";
12
+ import { isLocalAttestationSecret } from "../lib/local-management-attestation";
11
13
  import { redactSecretString } from "../lib/redact";
14
+ import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
15
+ import { fetchBoundLocalManagementRead, type LocalManagementReadDeps } from "../server/local-management-read-client";
12
16
  import { rangeWindow, USAGE_SURFACES } from "../usage/summary";
13
17
  import type { UsageTimeWindow } from "../usage/time-range";
14
18
  import {
@@ -16,8 +20,7 @@ import {
16
20
  printData,
17
21
  rejectArgs,
18
22
  RuntimeApiError,
19
- runtimeRequest,
20
- runtimeBaseUrl,
23
+ PROXY_NOT_RUNNING_MESSAGE,
21
24
  takeFlag,
22
25
  takeOption,
23
26
  takeOptionWithSyntax,
@@ -29,7 +32,10 @@ export const USAGE_COMPARE_USAGE = `Usage:
29
32
  [--provider <name>] [--model <id>] [--api-key-id <id>] [--json]`;
30
33
 
31
34
  export type CompareRange = "today" | "7d" | "30d";
32
- export type UsageCompareDeps = RuntimeApiDeps & { now?: () => number };
35
+ export type UsageCompareDeps = RuntimeApiDeps & Pick<LocalManagementReadDeps, "directFetch"> & {
36
+ now?: () => number;
37
+ readRuntime?: typeof readRuntimePort;
38
+ };
33
39
 
34
40
  const METRICS = ["requests", "totalTokens", "inputTokens", "outputTokens", "cachedInputTokens", "estimatedCostUsd"] as const;
35
41
  type Metric = typeof METRICS[number];
@@ -189,10 +195,60 @@ function captureUsageOwner() {
189
195
  const headers = runningProxyUpdateHeaders();
190
196
  const fingerprint = connection.kind === "connected" ? connection.value.tokenFingerprint
191
197
  : serviceApiTokenFingerprint(headers.get("X-OpenCodex-API-Key") ?? "");
192
- return { connection, token, headers, fingerprint };
198
+ return { connection, token, fingerprint };
193
199
  }
194
200
  type UsageOwner = ReturnType<typeof captureUsageOwner>;
195
201
 
202
+ function pinUsageRuntime(deps: UsageCompareDeps, owner: UsageOwner): (suffix: string) => Promise<ScopedUsage> {
203
+ const readRuntime = deps.readRuntime ?? readRuntimePort;
204
+ const initial = readRuntimePortForComparison();
205
+ const pinned = { ...initial };
206
+ let liveIdentity: string | undefined;
207
+ function readRuntimePortForComparison(): RuntimePortState {
208
+ const runtime = readRuntime();
209
+ if (!runtime || !isLocalAttestationSecret(runtime.attestationSecret)) {
210
+ throw new Error("Usage runtime unavailable: restart the proxy with an attested runtime record.");
211
+ }
212
+ return runtime;
213
+ }
214
+ function assertRuntime(): RuntimePortState {
215
+ const current = readRuntimePortForComparison();
216
+ if (current.pid !== pinned.pid || current.port !== pinned.port || current.hostname !== pinned.hostname
217
+ || current.attestationSecret !== pinned.attestationSecret) {
218
+ throw new Error("Usage runtime changed during comparison; no comparison was produced. Retry for the current runtime.");
219
+ }
220
+ return current;
221
+ }
222
+ return async suffix => {
223
+ assertRuntime();
224
+ const live = await (deps.findLiveProxy ?? findLiveProxy)();
225
+ assertRuntime();
226
+ assertUsageOwner(owner);
227
+ if (!live) throw new RuntimeApiError(PROXY_NOT_RUNNING_MESSAGE, 503, null, "proxy_not_running");
228
+ const identity = JSON.stringify([live.pid, live.port, live.hostname, live.source, live.role]);
229
+ if (live.source !== "runtime" || live.pid !== pinned.pid || live.port !== pinned.port
230
+ || probeHostname(live.hostname) !== probeHostname(pinned.hostname) || live.role === "client"
231
+ || (liveIdentity !== undefined && liveIdentity !== identity)) {
232
+ throw new Error("Usage runtime changed during comparison; no comparison was produced. Retry for the current runtime.");
233
+ }
234
+ liveIdentity = identity;
235
+ const read = await fetchBoundLocalManagementRead(live, `/api/usage?${suffix.replace(/^\?/, "")}`, {
236
+ fetchImpl: deps.fetchImpl, directFetch: deps.directFetch, readRuntime: assertRuntime,
237
+ timeoutMs: 60_000, requireResponseProof: true,
238
+ });
239
+ assertRuntime();
240
+ if (read.kind === "unavailable") {
241
+ throw new RuntimeApiError(`Local usage read unavailable (${read.reason}). No comparison was produced.`, 503, null);
242
+ }
243
+ if (!read.response.ok) {
244
+ throw new RuntimeApiError(`Usage request failed (HTTP ${read.response.status}). No comparison was produced.`, read.response.status, null);
245
+ }
246
+ const result = await read.response.json() as ScopedUsage;
247
+ assertRuntime();
248
+ return result;
249
+ };
250
+ }
251
+
196
252
  function assertUsageOwner(owner: UsageOwner): void {
197
253
  const current = readClientConnectionState();
198
254
  const expected = owner.connection;
@@ -216,6 +272,7 @@ async function readUsage(
216
272
  apiKeyId: string | undefined,
217
273
  deps: RuntimeApiDeps,
218
274
  owner: UsageOwner,
275
+ localRead?: (suffix: string) => Promise<ScopedUsage>,
219
276
  ): Promise<ScopedUsage> {
220
277
  assertUsageOwner(owner);
221
278
  const { connection, token } = owner;
@@ -233,16 +290,15 @@ async function readUsage(
233
290
  result = await fetchHubUsage(connection.value.serverUrl, token.token, new URLSearchParams(suffix.replace(/^\?/, "")), {
234
291
  fetchImpl: peerFetch, timeoutMs: 60_000, ...(link ? { peer: "Home" as const } : {}),
235
292
  });
236
- } else if (apiKeyId === undefined) {
237
- result = await runtimeRequest(`/api/usage${suffix}`, { headers: owner.headers }, deps);
238
293
  } else {
239
294
  try {
240
- result = await runtimeRequest(`/api/usage${suffix}`, { redirect: "error", credentials: "omit", headers: owner.headers }, deps);
295
+ result = await localRead!(suffix);
241
296
  } catch (error) {
242
- if (error instanceof RuntimeApiError && error.code === "proxy_not_running") throw error;
297
+ if (apiKeyId === undefined) throw error;
298
+ if (error instanceof RuntimeApiError) throw error;
243
299
  throw new Error("Key-scoped usage could not be read. Check runtime access and retry.");
244
300
  }
245
- if (result.filter?.apiKeyId !== apiKeyId) {
301
+ if (apiKeyId !== undefined && result.filter?.apiKeyId !== apiKeyId) {
246
302
  throw new Error("The server did not confirm the requested API key scope. Upgrade and restart the proxy, then retry.");
247
303
  }
248
304
  }
@@ -276,9 +332,9 @@ export async function handleUsageCompareCommand(argv: string[], deps: UsageCompa
276
332
  const windows = compareUsageWindows(range, now);
277
333
  const filters = { range, surface, provider, model, apiKeyId };
278
334
  const owner = captureUsageOwner();
279
- const readDeps = owner.connection.kind === "disconnected" ? { ...deps, baseUrl: await runtimeBaseUrl(deps) } : deps;
280
- const currentReport = await readUsage(query({ ...filters, since: windows.current.since, until: windows.current.until }), windows.current, apiKeyId, readDeps, owner);
281
- const previousReport = await readUsage(query({ ...filters, since: windows.previous.since, until: windows.previous.until }), windows.previous, apiKeyId, readDeps, owner);
335
+ const localRead = owner.connection.kind === "disconnected" ? pinUsageRuntime(deps, owner) : undefined;
336
+ const currentReport = await readUsage(query({ ...filters, since: windows.current.since, until: windows.current.until }), windows.current, apiKeyId, deps, owner, localRead);
337
+ const previousReport = await readUsage(query({ ...filters, since: windows.previous.since, until: windows.previous.until }), windows.previous, apiKeyId, deps, owner, localRead);
282
338
  const current = totalsOf(currentReport);
283
339
  const previous = totalsOf(previousReport);
284
340
  const deltaTotals: Totals = {};