@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -12,6 +12,8 @@
12
12
  * (as in `formatAccountTable`), plain text, no ANSI colour.
13
13
  */
14
14
 
15
+ import type { UsageModelView } from "./usage-model-search";
16
+
15
17
  interface CostRow {
16
18
  provider: string;
17
19
  model?: string;
@@ -43,7 +45,7 @@ interface UsageReportInput {
43
45
  models?: CostRow[];
44
46
  providers?: CostRow[];
45
47
  days?: { date: string; requests: number; totalTokens: number; estimatedCostUsd?: number }[];
46
- filter?: { provider: string | null; model: string | null; matched: boolean; comboOverlap: boolean };
48
+ filter?: { apiKeyId?: string | null; provider: string | null; model: string | null; matched: boolean; comboOverlap: boolean };
47
49
  /**
48
50
  * Per-account totals the API already returns and the CLI discarded (#2700).
49
51
  *
@@ -106,11 +108,12 @@ function describeScope(data: UsageReportInput): string {
106
108
  const parts = [`Usage — ${interval}`];
107
109
  if (data.surface && data.surface !== "all") parts.push(`surface=${data.surface}`);
108
110
  if (data.filter?.provider) parts.push(`provider=${data.filter.provider}`);
111
+ if (data.filter?.apiKeyId) parts.push(`api-key-id=${data.filter.apiKeyId}`);
109
112
  if (data.filter?.model) parts.push(`model=${data.filter.model}`);
110
113
  return terminalText(parts.join(", "));
111
114
  }
112
115
 
113
- export function formatUsageReport(data: UsageReportInput): string[] {
116
+ export function formatUsageReport(data: UsageReportInput, options?: { modelView?: UsageModelView }): string[] {
114
117
  const summary = data.summary ?? {};
115
118
  const lines: string[] = [describeScope(data), ""];
116
119
  if (data.source === "hub" && data.scope === "client") {
@@ -119,9 +122,14 @@ export function formatUsageReport(data: UsageReportInput): string[] {
119
122
  if (data.usageIncomplete === true) {
120
123
  lines.push("WARNING: Usage is incomplete; some records could not be included. Totals and rankings reflect readable records only.", "");
121
124
  }
125
+ const view = options?.modelView;
126
+ if (view) {
127
+ lines.push(`Model search ${JSON.stringify(view.query)}: ${view.returnedModelCount} of ${view.matchedModelCount} matching model rows (limit ${view.limit}). Report totals are unchanged.`, "");
128
+ if (view.matchedModelCount === 0) lines.push("No model rows match this search. Change --search or use --search= to show the model view.", "");
129
+ }
122
130
 
123
131
  if (data.filter && !data.filter.matched) {
124
- const what = [data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
132
+ const what = [data.filter.apiKeyId && `API key "${data.filter.apiKeyId}"`, data.filter.provider && `provider "${data.filter.provider}"`, data.filter.model && `model "${data.filter.model}"`]
125
133
  .filter(Boolean).join(" and ");
126
134
  lines.push(data.usageIncomplete === true
127
135
  ? `No matching readable usage records for ${terminalText(what)} in this range; skipped records may contain matches.`
@@ -181,10 +189,10 @@ export function formatUsageReport(data: UsageReportInput): string[] {
181
189
  ));
182
190
  }
183
191
 
184
- const models = (data.models ?? []).filter(row => row.requests > 0);
192
+ const models = view ? (data.models ?? []) : (data.models ?? []).filter(row => row.requests > 0);
185
193
  if (models.length > 0) {
186
194
  lines.push("");
187
- const shown = models.slice(0, MAX_MODEL_ROWS);
195
+ const shown = models.slice(0, view?.limit ?? MAX_MODEL_ROWS);
188
196
  lines.push(...table(
189
197
  ["MODEL", "PROVIDER", "REQUESTS", "TOKENS", "EST. COST"],
190
198
  shown.map(row => [row.model ?? "-", row.provider, count(row.requests), count(row.totalTokens), usd(row.estimatedCostUsd)]),
@@ -0,0 +1,66 @@
1
+ import { CliUsageError } from "./runtime-api";
2
+
3
+ type V2Controls = { json: boolean; live: boolean; acknowledge: boolean };
4
+ export type V2ParsedCommand = V2Controls & (
5
+ | { verb: "status" | "on" | "off" }
6
+ | { verb: "mode"; value: "v1" | "default" | "v2" }
7
+ | { verb: "keep-native-v1"; value: boolean }
8
+ | { verb: "threads"; value: number }
9
+ | { verb: "mode-hint"; value: string | null }
10
+ );
11
+
12
+ export const V2_USAGE = "Usage: ocx v2 [status|on|off|mode <v1|default|v2>|keep-native-v1 <on|off>|threads <n>|mode-hint <text|--clear>] [--live] [--json]\nFor literal reserved hint text: ocx v2 mode-hint [--live] [--json] -- <text>\nOnly live mode accepts --acknowledge-surface-advisory.";
13
+
14
+ /** Parse all controls before touching native/config state. A terminator protects literal hints. */
15
+ export function parseV2Command(argv: readonly string[]): V2ParsedCommand {
16
+ const boundary = argv.indexOf("--");
17
+ const prefix = boundary < 0 ? argv : argv.slice(0, boundary);
18
+ const suffix = boundary < 0 ? [] : argv.slice(boundary + 1);
19
+ const controls: V2Controls = { json: false, live: false, acknowledge: false };
20
+ const operands: string[] = [];
21
+ for (const arg of prefix) {
22
+ const flag = arg === "--json" ? "json" : arg === "--live" ? "live"
23
+ : arg === "--acknowledge-surface-advisory" ? "acknowledge" : undefined;
24
+ if (["--json=", "--live=", "--acknowledge-surface-advisory="].some(prefix => arg.startsWith(prefix))) {
25
+ throw new CliUsageError("v2: control flags do not take values; use -- for literal hint text", V2_USAGE);
26
+ }
27
+ if (flag) {
28
+ if (controls[flag]) throw new CliUsageError("v2: duplicate control flag", V2_USAGE);
29
+ controls[flag] = true;
30
+ } else operands.push(arg);
31
+ }
32
+ const verb = (operands.shift() ?? "status").trim().toLowerCase();
33
+ const invalid = (message: string): never => { throw new CliUsageError(message, V2_USAGE); };
34
+ if (controls.acknowledge && (!controls.live || verb !== "mode")) {
35
+ invalid("v2: advisory acknowledgment requires explicit live mode");
36
+ }
37
+ if (boundary >= 0 && (verb !== "mode-hint" || operands.length !== 0 || suffix.length !== 1)) {
38
+ invalid("v2: the terminator requires exactly one literal mode-hint operand");
39
+ }
40
+ if (verb === "status" || verb === "on" || verb === "off") {
41
+ if (operands.length) invalid("v2: unexpected operands or flags");
42
+ return { ...controls, verb };
43
+ }
44
+ const values = boundary >= 0 ? suffix : operands;
45
+ if (values.length !== 1) invalid(verb === "keep-native-v1" ? "v2 keep-native-v1: expected on|off" : "v2: exactly one value is required");
46
+ const raw = values[0]!;
47
+ if (verb === "mode-hint") {
48
+ if (!raw.trim()) invalid("v2 mode-hint: pass nonblank text, or --clear to unset it");
49
+ // Legacy unreserved hyphen-leading hint strings remain ordinary content.
50
+ return { ...controls, verb, value: boundary < 0 && raw === "--clear" ? null : raw };
51
+ }
52
+ const value = raw.trim().toLowerCase();
53
+ if (verb === "mode" && (value === "v1" || value === "default" || value === "v2")) {
54
+ return { ...controls, verb, value };
55
+ }
56
+ if (verb === "keep-native-v1" && (value === "on" || value === "off")) {
57
+ return { ...controls, verb, value: value === "on" };
58
+ }
59
+ if (verb === "keep-native-v1") invalid("v2 keep-native-v1: expected on|off");
60
+ if (verb === "threads") {
61
+ const count = Number(value);
62
+ if (Number.isSafeInteger(count) && count >= 1) return { ...controls, verb, value: count };
63
+ invalid("v2 threads: pass a safe integer >= 1");
64
+ }
65
+ return invalid("v2: invalid command or value");
66
+ }
@@ -0,0 +1,37 @@
1
+ import { loadConfig } from "../config";
2
+ import { multiAgentSurfaceAdvisory, resolveMultiAgentMode } from "../config/multi-agent-surface";
3
+ import { getAgentsEnabled, getAgentsMaxDepth, getLogicalMaxThreads, getMultiAgentModeHintText,
4
+ getSubagentDeveloperInstructions, hasAgentsMaxThreads, isMultiAgentV2Enabled } from "../codex/features";
5
+ import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../codex/multi-agent-mode-policy";
6
+ import type { CodexSyncResult } from "../codex/sync";
7
+ import { projectLocalSyncResult, type LocalSyncResult } from "./local-sync-result";
8
+
9
+ /** Rebuild local state from actual post-write readers; no paths, credentials, or logs. */
10
+ export function localV2State(isEnabled = isMultiAgentV2Enabled, hasMaxThreads = hasAgentsMaxThreads) {
11
+ const config = loadConfig();
12
+ const enabled = isEnabled();
13
+ return { enabled, agentsMaxThreadsConflict: enabled && hasMaxThreads(),
14
+ multiAgentMode: resolveMultiAgentMode(config), keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
15
+ multiAgentSurfaceAdvisory: multiAgentSurfaceAdvisory(config), maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
16
+ agentsEnabled: getAgentsEnabled(), agentsMaxDepth: getAgentsMaxDepth(),
17
+ agentsMaxDepthAppliesWhenV2Disabled: !enabled, subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
18
+ multiAgentModeHintText: getMultiAgentModeHintText(), multiAgentModeHintRecommendation: MULTI_AGENT_MODE_HINT_RECOMMENDATION };
19
+ }
20
+
21
+ export type V2LocalSync = LocalSyncResult | { status: "unverified"; ok: false };
22
+
23
+ /** The existing injected sync contract returns unknown. Validate before the typed shared projection. */
24
+ export function v2LocalSyncResult(value: unknown): V2LocalSync {
25
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { status: "unverified", ok: false };
26
+ const source = value as Record<string, unknown>;
27
+ if (typeof source.status !== "string" || !["applied", "catalog-only", "skipped", "refused"].includes(source.status)
28
+ || typeof source.ok !== "boolean" || typeof source.catalogExists !== "boolean"
29
+ || typeof source.catalogWritten !== "boolean" || typeof source.cacheSynced !== "boolean"
30
+ || typeof source.added !== "number" || !Number.isSafeInteger(source.added) || source.added < 0
31
+ || (source.catalogPath !== null && typeof source.catalogPath !== "string") || typeof source.message !== "string"
32
+ || (source.refreshOutcome !== undefined && source.refreshOutcome !== "committed" && source.refreshOutcome !== "refused")) {
33
+ return { status: "unverified", ok: false };
34
+ }
35
+ // The checked fields are the complete mandatory backend contract. Private text is never emitted.
36
+ return projectLocalSyncResult(source as unknown as CodexSyncResult);
37
+ }
@@ -0,0 +1,90 @@
1
+ import { SUBAGENT_SURFACE_GUIDE_URL } from "../config/multi-agent-surface";
2
+ import { printCatalogResult, runCatalogAction } from "./catalog-command-result";
3
+ import { serializeManagementJson } from "./json-input";
4
+ import { RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest, type RuntimeApiDeps } from "./runtime-api";
5
+ import type { V2ParsedCommand } from "./v2-input";
6
+
7
+ function record(value: unknown): value is Record<string, unknown> {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+ function mode(value: unknown): value is "v1" | "default" | "v2" {
11
+ return value === "v1" || value === "default" || value === "v2";
12
+ }
13
+ function nullableString(value: unknown): value is string | null { return value === null || typeof value === "string"; }
14
+
15
+ /** Allowlisted task fields only: neither helper errors nor arbitrary response spreads. */
16
+ function stateDto(value: unknown): Record<string, unknown> {
17
+ if (!record(value) || !mode(value.multiAgentMode)) throw new Error("Invalid v2 state");
18
+ const result: Record<string, unknown> = { multiAgentMode: value.multiAgentMode };
19
+ for (const key of ["enabled", "agentsMaxThreadsConflict", "keepNativeChatGptOnV1", "agentsMaxDepthAppliesWhenV2Disabled"] as const) {
20
+ if (typeof value[key] !== "boolean") throw new Error("Invalid v2 boolean");
21
+ result[key] = value[key];
22
+ }
23
+ if (value.agentsEnabled !== null && typeof value.agentsEnabled !== "boolean") throw new Error("Invalid agents state");
24
+ if (value.maxConcurrentThreadsPerSession !== null && !(typeof value.maxConcurrentThreadsPerSession === "number"
25
+ && Number.isSafeInteger(value.maxConcurrentThreadsPerSession) && value.maxConcurrentThreadsPerSession >= 1)) throw new Error("Invalid thread state");
26
+ if (value.agentsMaxDepth !== null && !(typeof value.agentsMaxDepth === "number" && Number.isInteger(value.agentsMaxDepth)
27
+ && value.agentsMaxDepth >= -2147483648 && value.agentsMaxDepth <= 2147483647)) throw new Error("Invalid depth state");
28
+ for (const key of ["subagentDeveloperInstructions", "multiAgentModeHintText"] as const) {
29
+ if (!nullableString(value[key])) throw new Error("Invalid text state");
30
+ result[key] = value[key];
31
+ }
32
+ const recommendation = value.multiAgentModeHintRecommendation;
33
+ if (!record(recommendation) || typeof recommendation.revision !== "string" || typeof recommendation.text !== "string") throw new Error("Invalid hint recommendation");
34
+ const advisory = value.multiAgentSurfaceAdvisory;
35
+ if (!record(advisory) || typeof advisory.required !== "boolean" || advisory.mode !== value.multiAgentMode
36
+ || advisory.recommended !== "v1" || typeof advisory.version !== "number" || !Number.isSafeInteger(advisory.version)
37
+ || advisory.version < 1 || advisory.docsUrl !== SUBAGENT_SURFACE_GUIDE_URL) throw new Error("Invalid surface advisory");
38
+ return { ...result, agentsEnabled: value.agentsEnabled, agentsMaxDepth: value.agentsMaxDepth,
39
+ maxConcurrentThreadsPerSession: value.maxConcurrentThreadsPerSession,
40
+ multiAgentModeHintRecommendation: { revision: recommendation.revision, text: recommendation.text },
41
+ multiAgentSurfaceAdvisory: { required: advisory.required, mode: advisory.mode, recommended: "v1",
42
+ version: advisory.version, docsUrl: advisory.docsUrl } };
43
+ }
44
+
45
+ function bodyFor(parsed: V2ParsedCommand): Record<string, unknown> {
46
+ switch (parsed.verb) {
47
+ case "on": case "off": return { enabled: parsed.verb === "on" };
48
+ case "mode": return { multiAgentMode: parsed.value,
49
+ ...(parsed.acknowledge ? { multiAgentSurfaceAdvisoryAcknowledged: true } : {}) };
50
+ case "keep-native-v1": return { keepNativeChatGptOnV1: parsed.value };
51
+ case "threads": return { maxConcurrentThreadsPerSession: parsed.value };
52
+ case "mode-hint": return { multiAgentModeHintText: parsed.value };
53
+ case "status": return {};
54
+ }
55
+ }
56
+
57
+ export async function handleV2RuntimeCommand(parsed: V2ParsedCommand, deps: RuntimeApiDeps = {}): Promise<number> {
58
+ return runCatalogAction(async () => {
59
+ const read = parsed.verb === "status";
60
+ const body = read ? undefined : serializeManagementJson(bodyFor(parsed));
61
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
62
+ let response: unknown;
63
+ try {
64
+ response = await runtimeRequest("/api/v2", { method: read ? "GET" : "PUT", redirect: "error", ...(body ? { body } : {}) }, pinned);
65
+ } catch (error) {
66
+ if (error instanceof RuntimeApiError && error.status === 502) {
67
+ console.error("Error: Native V2 settings may be partially applied. Read ocx v2 status --live before retrying; no rollback is implied.");
68
+ return 1;
69
+ }
70
+ throw error;
71
+ }
72
+ const state = stateDto(response);
73
+ const lines = [`multi_agent_v2: ${state.enabled ? "ON" : "OFF"}`, `multi_agent_mode: ${state.multiAgentMode}`,
74
+ `keep_native_chatgpt_on_v1: ${state.keepNativeChatGptOnV1 ? "ON" : "OFF"}`,
75
+ `max_threads: ${state.maxConcurrentThreadsPerSession ?? "unset"}`,
76
+ `agents.enabled: ${state.agentsEnabled ?? "unset (upstream default)"}`,
77
+ `agents.max_depth: ${state.agentsMaxDepth ?? "unset"}${state.enabled ? " (V1-only — ignored while V2 is enabled)" : ""}`,
78
+ `subagent_developer_instructions: ${JSON.stringify(state.subagentDeveloperInstructions)}`,
79
+ `multi_agent_mode_hint_text: ${JSON.stringify(state.multiAgentModeHintText)}`];
80
+ if (state.agentsMaxThreadsConflict) lines.push("Native thread-limit conflict: inspect the selected target's Codex configuration before starting Codex.");
81
+ if (state.enabled && state.agentsEnabled === false) lines.push("agents.enabled=false does not disable multi-agent while the global V2 feature is enabled.");
82
+ if (record(state.multiAgentSurfaceAdvisory) && state.multiAgentSurfaceAdvisory.required) {
83
+ lines.push(`Surface advisory: v1 is recommended. ${SUBAGENT_SURFACE_GUIDE_URL}`);
84
+ }
85
+ if (read) { printData({ target: "live", ...state }, parsed.json, lines); return 0; }
86
+ if (!record(response) || response.ok !== true) throw new Error("Invalid write receipt");
87
+ return printCatalogResult({ ok: true, target: "live", ...state }, response.catalogRefresh, parsed.json,
88
+ [...lines, "V2 settings saved. Applies to new sessions."]);
89
+ });
90
+ }
package/src/cli/v2.ts CHANGED
@@ -10,6 +10,10 @@
10
10
  * config keys, with byte-for-byte rollback when the feature command fails.
11
11
  * - nothing in the catalog build path calls this module; no auto-flip exists.
12
12
  */
13
+ import { CliUsageError, terminalSafeText, type RuntimeApiDeps } from "./runtime-api";
14
+ import { parseV2Command, V2_USAGE, type V2ParsedCommand } from "./v2-input";
15
+ import { handleV2RuntimeCommand } from "./v2-runtime";
16
+ import { localV2State, v2LocalSyncResult, type V2LocalSync } from "./v2-local-output";
13
17
  import { execFileSync } from "node:child_process";
14
18
  import { dirname } from "node:path";
15
19
  import { activeCodexConfigPath, getAgentsEnabled, getAgentsMaxDepth, getLogicalMaxThreads, getMultiAgentModeHintText, getSubagentDeveloperInstructions, hasAgentsMaxThreads, isMultiAgentV2Enabled, setMultiAgentModeHintText, transitionMultiAgentV2 } from "../codex/features";
@@ -23,6 +27,9 @@ export interface V2CliDeps {
23
27
  isEnabled?: typeof isMultiAgentV2Enabled;
24
28
  hasMaxThreads?: typeof hasAgentsMaxThreads;
25
29
  sync?: (port?: number) => Promise<unknown>;
30
+ runtimeApi?: RuntimeApiDeps;
31
+ /** Internal seam: bypass executable discovery only when an exec implementation is injected. */
32
+ featuresInvocation?: (action: "enable" | "disable") => SpawnInvocation;
26
33
  log?: Pick<Console, "log" | "error">;
27
34
  }
28
35
 
@@ -79,7 +86,7 @@ export function runCodexFeaturesCommand(
79
86
 
80
87
  function runCodexFeatures(action: "enable" | "disable", deps: V2CliDeps): void {
81
88
  if (deps.execFile) {
82
- const inv = codexFeaturesInvocation(action);
89
+ const inv = (deps.featuresInvocation ?? codexFeaturesInvocation)(action);
83
90
  deps.execFile(inv.file, inv.args, inv.options);
84
91
  return;
85
92
  }
@@ -106,13 +113,10 @@ function requiresGlobalV2Disabled(multiAgentMode: string | undefined, keepNative
106
113
  return multiAgentMode === "v2" && keepNativeChatGptOnV1;
107
114
  }
108
115
 
109
- export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: () => Promise<number | undefined>): Promise<number> {
116
+ function printLocalStatus(deps: V2CliDeps): number {
110
117
  const log = deps.log ?? console;
111
118
  const isEnabled = deps.isEnabled ?? isMultiAgentV2Enabled;
112
119
  const hasMaxThreads = deps.hasMaxThreads ?? hasAgentsMaxThreads;
113
- const verb = (args[0] ?? "status").trim().toLowerCase();
114
-
115
- if (verb === "status") {
116
120
  log.log(v2StatusLine(isEnabled()));
117
121
  const cfg = loadConfig();
118
122
  const mode = cfg.multiAgentMode ?? "default";
@@ -140,153 +144,107 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
140
144
  log.log("WARNING: [agents] max_threads is set — codex refuses to start while multi_agent_v2 is enabled. Remove it from config.toml (concurrency lives in features.multi_agent_v2.max_concurrent_threads_per_session).");
141
145
  }
142
146
  return 0;
147
+ }
148
+
149
+ /** Parse before selecting either the native writer or the runtime management target. */
150
+ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: () => Promise<number | undefined>): Promise<number> {
151
+ const log = deps.log ?? console;
152
+ let parsed: V2ParsedCommand;
153
+ try { parsed = parseV2Command(args); }
154
+ catch (error) {
155
+ log.error(error instanceof CliUsageError ? error.message : "v2: invalid command");
156
+ for (const line of V2_USAGE.split("\n")) log.error(line);
157
+ const boundary = args.indexOf("--");
158
+ return (boundary < 0 ? args : args.slice(0, boundary)).includes("--live") ? 2 : 1;
143
159
  }
144
- if (verb === "mode-hint") {
145
- const value = args[1];
146
- if (value === undefined) {
147
- log.error("v2 mode-hint: pass the hint text, or --clear to unset it.");
148
- return 1;
149
- }
150
- if (value === "--clear") {
151
- const result = setMultiAgentModeHintText(null);
152
- if (!result.ok) {
153
- log.error(`v2 mode-hint: ${result.error}`);
154
- return 1;
155
- }
156
- log.log(result.changed
157
- ? "multi_agent_mode_hint_text cleared — effort-derived policy resumes (new sessions)."
158
- : "multi_agent_mode_hint_text already unset — nothing to do.");
159
- return 0;
160
- }
161
- // `--clear` is the only reserved token; hints are otherwise arbitrary
162
- // nonblank text and may legitimately begin with a hyphen.
163
- if (value.trim().length === 0) {
164
- log.error("v2 mode-hint: pass the hint text, or --clear to unset it.");
165
- return 1;
166
- }
167
- const result = setMultiAgentModeHintText(value);
168
- if (!result.ok) {
169
- log.error(`v2 mode-hint: ${result.error}`);
170
- return 1;
160
+ if (parsed.live) return handleV2RuntimeCommand(parsed, deps.runtimeApi);
161
+ let changed: boolean | null = false;
162
+ let sync: V2LocalSync = { status: "not-attempted", ok: false };
163
+ const emit = (ok: boolean, lines: string[]): number => {
164
+ // A read-back failure cannot claim rollback or overwrite already observed mutation evidence.
165
+ let state: ReturnType<typeof localV2State> | null = null;
166
+ try { state = localV2State(deps.isEnabled, deps.hasMaxThreads); }
167
+ catch { ok = false; lines = ["Local V2 outcome is unverified. Read ocx v2 status before retrying; no rollback is implied."]; }
168
+ if (parsed.json) log.log(JSON.stringify({ ok, target: "local", action: parsed.verb, changed, state, sync }));
169
+ else for (const line of lines) (ok ? log.log : log.error).call(log, terminalSafeText(line));
170
+ return ok ? 0 : 1;
171
+ };
172
+ try {
173
+ if (parsed.verb === "status") {
174
+ if (!parsed.json) return printLocalStatus(deps);
175
+ return emit(true, []);
171
176
  }
172
- log.log(result.changed
173
- ? `multi_agent_mode_hint_text set (new sessions).`
174
- : "multi_agent_mode_hint_text already set — nothing to do.");
175
- return 0;
176
- }
177
- if (verb === "threads") {
178
- const value = Number((args[1] ?? "").trim());
179
- if (!Number.isInteger(value) || value < 1) {
180
- log.error("v2 threads: pass an integer >= 1 (features.multi_agent_v2.max_concurrent_threads_per_session)");
181
- return 1;
177
+ if (parsed.verb === "mode-hint") {
178
+ const result = setMultiAgentModeHintText(parsed.value);
179
+ if (!result.ok) return emit(false, ["Unable to write the native mode hint. Check Codex support and ownership, then read ocx v2 status."]);
180
+ changed = result.changed;
181
+ return emit(true, [result.changed ? "multi_agent_mode_hint_text saved — applies to new sessions." : "multi_agent_mode_hint_text already set — nothing to do."]);
182
182
  }
183
- const enabled = isEnabled();
184
- const result = transitionMultiAgentV2(enabled, next => runCodexFeatures(next ? "enable" : "disable", deps), { threadLimit: value });
185
- if (!result.ok) { log.error(`v2 threads: ${result.error}`); return 1; }
186
- log.log(result.changed
187
- ? `max_threads = ${value} (${enabled ? "v2" : "v1"}) — applies to new sessions.`
188
- : `max_threads already ${value} — nothing to do.`);
189
- return 0;
190
- }
191
- if (verb === "mode") {
192
- const modeArg = (args[1] ?? "").trim().toLowerCase();
193
- if (modeArg !== "v1" && modeArg !== "default" && modeArg !== "v2") {
194
- log.error("v2 mode: expected v1|default|v2");
195
- return 1;
183
+ if (parsed.verb === "threads") {
184
+ const enabled = (deps.isEnabled ?? isMultiAgentV2Enabled)();
185
+ const result = transitionMultiAgentV2(enabled, next => runCodexFeatures(next ? "enable" : "disable", deps), { threadLimit: parsed.value });
186
+ if (!result.ok) return emit(false, ["Unable to update the native thread limit. Read ocx v2 status before retrying."]);
187
+ changed = result.changed;
188
+ return emit(true, [changed ? `max_threads = ${parsed.value} (${enabled ? "v2" : "v1"}) — applies to new sessions.` : `max_threads already ${parsed.value} — nothing to do.`]);
196
189
  }
197
- const cfg = loadConfig();
198
- if (modeArg !== "default") {
199
- const target = modeArg === "v2" && cfg.keepNativeChatGptOnV1 !== true;
200
- const transition = transitionMultiAgentV2(target, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
201
- if (!transition.ok) {
202
- log.error(`multi-agent mode transition failed: ${transition.error}`);
203
- return 1;
190
+ const config = loadConfig();
191
+ let successLines: string[];
192
+ let unchangedKeep: boolean | undefined;
193
+ if (parsed.verb === "mode") {
194
+ changed = (config.multiAgentMode ?? "default") !== parsed.value;
195
+ if (parsed.value !== "default") {
196
+ const target = parsed.value === "v2" && config.keepNativeChatGptOnV1 !== true;
197
+ const transition = transitionMultiAgentV2(target, next => runCodexFeatures(next ? "enable" : "disable", deps));
198
+ if (!transition.ok) { changed = false; return emit(false, ["Native mode transition failed. Read ocx v2 status before retrying."]); }
199
+ changed ||= transition.changed;
204
200
  }
205
- }
206
- if (modeArg === "default") deleteConfigTopLevelKey(cfg, "multiAgentMode");
207
- else cfg.multiAgentMode = modeArg as "v1" | "v2";
208
- saveConfig(cfg);
209
- try {
210
- const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
211
- await sync(findPort ? await findPort() : undefined);
212
- } catch (err) {
213
- log.error(`catalog resync failed: ${err instanceof Error ? err.message : String(err)} — run 'ocx sync' manually.`);
214
- return 1;
215
- }
216
- log.log(multiAgentModeLine(modeArg));
217
- log.log("Applies to NEW sessions; running sessions keep their pinned multi-agent version.");
218
- return 0;
219
- }
220
- if (verb === "keep-native-v1") {
221
- const flag = (args[1] ?? "").trim().toLowerCase();
222
- if (flag !== "on" && flag !== "off") {
223
- log.error("v2 keep-native-v1: expected on|off");
224
- return 1;
225
- }
226
- const cfg = loadConfig();
227
- const next = flag === "on";
228
- const already = cfg.keepNativeChatGptOnV1 === true === next;
229
- if (next && requiresGlobalV2Disabled(cfg.multiAgentMode, true)) {
230
- const transition = transitionMultiAgentV2(false, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
231
- if (!transition.ok) {
232
- log.error(`keep-native-v1 transition failed: ${transition.error}`);
233
- return 1;
201
+ if (parsed.value === "default") deleteConfigTopLevelKey(config, "multiAgentMode");
202
+ else config.multiAgentMode = parsed.value;
203
+ saveConfig(config);
204
+ successLines = [multiAgentModeLine(parsed.value, config.keepNativeChatGptOnV1 === true),
205
+ "Applies to NEW sessions; running sessions keep their pinned multi-agent version."];
206
+ } else if (parsed.verb === "keep-native-v1") {
207
+ unchangedKeep = (config.keepNativeChatGptOnV1 === true) === parsed.value;
208
+ changed = !unchangedKeep;
209
+ if (parsed.value && requiresGlobalV2Disabled(config.multiAgentMode, true)) {
210
+ const transition = transitionMultiAgentV2(false, next => runCodexFeatures(next ? "enable" : "disable", deps));
211
+ if (!transition.ok) { changed = false; return emit(false, ["Native hybrid-mode transition failed. Read ocx v2 status before retrying."]); }
212
+ changed ||= transition.changed;
213
+ }
214
+ if (parsed.value) config.keepNativeChatGptOnV1 = true;
215
+ else deleteConfigTopLevelKey(config, "keepNativeChatGptOnV1");
216
+ saveConfig(config);
217
+ successLines = [parsed.value
218
+ ? "keep_native_chatgpt_on_v1: ON — ChatGPT-native rows stay v1 when mode is v2 (new sessions)."
219
+ : "keep_native_chatgpt_on_v1: OFF — ChatGPT-native rows follow v1/base/v2 (new sessions)."];
220
+ } else {
221
+ const want = parsed.verb === "on";
222
+ if (want && requiresGlobalV2Disabled(config.multiAgentMode, config.keepNativeChatGptOnV1 === true)) {
223
+ return emit(false, ["v2 on: incompatible with keep-native-v1 while mode is v2 — Codex's global multi_agent_v2 overrides the native v1 catalog pin. Run 'ocx v2 keep-native-v1 off' first."]);
234
224
  }
225
+ const transition = transitionMultiAgentV2(want, next => runCodexFeatures(next ? "enable" : "disable", deps));
226
+ if (!transition.ok) return emit(false, ["Native feature transition failed. Read ocx v2 status before retrying."]);
227
+ changed = transition.changed;
228
+ if (!changed) return emit(true, [`multi_agent_v2 already ${want ? "ON" : "OFF"} — nothing to do.`]);
229
+ successLines = [v2StatusLine(want),
230
+ "Applies to NEW sessions; running sessions keep their pinned multi-agent version. Restart the Codex app (or wait out its picker cache) to see the ladder change."];
235
231
  }
236
- if (next) cfg.keepNativeChatGptOnV1 = true;
237
- else deleteConfigTopLevelKey(cfg, "keepNativeChatGptOnV1");
238
- saveConfig(cfg);
232
+ // Preserve the legacy graph: mode/keep and changed toggles sync even with no port.
239
233
  try {
240
- const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
241
- await sync(findPort ? await findPort() : undefined);
242
- } catch (err) {
243
- log.error(`catalog resync failed: ${err instanceof Error ? err.message : String(err)} — run 'ocx sync' manually.`);
244
- return 1;
245
- }
246
- if (already) {
247
- log.log(next
248
- ? "keep_native_chatgpt_on_v1 already ON — catalog re-synced."
249
- : "keep_native_chatgpt_on_v1 already OFF — catalog re-synced.");
250
- return 0;
251
- }
252
- log.log(next
253
- ? "keep_native_chatgpt_on_v1: ON — ChatGPT-native rows stay v1 when mode is v2 (new sessions)."
254
- : "keep_native_chatgpt_on_v1: OFF — ChatGPT-native rows follow v1/base/v2 (new sessions).");
255
- return 0;
256
- }
257
- if (verb !== "on" && verb !== "off") {
258
- log.error(`v2: unknown verb '${verb}' (expected status|on|off|mode <v1|default|v2>|keep-native-v1 <on|off>|threads <n>|mode-hint <text|--clear>)`);
259
- return 1;
260
- }
261
-
262
- const want = verb === "on";
263
- if (want) {
264
- const cfg = loadConfig();
265
- if (requiresGlobalV2Disabled(cfg.multiAgentMode, cfg.keepNativeChatGptOnV1 === true)) {
266
- log.error("v2 on: incompatible with keep-native-v1 while mode is v2 — Codex's global multi_agent_v2 overrides the native v1 catalog pin. Run 'ocx v2 keep-native-v1 off' first.");
267
- return 1;
234
+ const port = findPort ? await findPort() : undefined;
235
+ const result = deps.sync ? await deps.sync(port)
236
+ : await (await import("../codex/sync")).syncModelsToCodex(port, undefined, parsed.json ? null : log);
237
+ sync = v2LocalSyncResult(result);
238
+ } catch { sync = { status: "failed", ok: false }; }
239
+ if (sync.ok && parsed.verb === "keep-native-v1" && unchangedKeep) {
240
+ const outcome = sync.status !== "skipped" && "catalog" in sync && sync.catalog?.converged
241
+ ? "catalog re-synced." : "catalog sync skipped by policy.";
242
+ successLines = [`keep_native_chatgpt_on_v1 already ${parsed.value ? "ON" : "OFF"} — ${outcome}`];
268
243
  }
244
+ return emit(sync.ok, sync.ok ? successLines
245
+ : ["catalog resync failed or is unverified; local V2 settings landed. Read ocx v2 status, then run ocx sync."]);
246
+ } catch {
247
+ changed = null;
248
+ return emit(false, ["Local V2 operation failed; native or saved settings may have changed. Read ocx v2 status before retrying; no rollback is implied."]);
269
249
  }
270
- const transition = transitionMultiAgentV2(want, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
271
- if (!transition.ok) {
272
- log.error(`codex features ${want ? "enable" : "disable"} multi_agent_v2 failed: ${transition.error}`);
273
- return 1;
274
- }
275
- if (!transition.changed) {
276
- log.log(`multi_agent_v2 already ${want ? "ON" : "OFF"} — nothing to do.`);
277
- return 0;
278
- }
279
-
280
- // Resync catalog so multi-agent surface metadata stays fresh in both the
281
- // on-disk catalog and models_cache.json after the toggle flip.
282
- try {
283
- const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
284
- await sync(findPort ? await findPort() : undefined);
285
- } catch (err) {
286
- log.error(`catalog resync failed (flag IS flipped): ${err instanceof Error ? err.message : String(err)} — run 'ocx sync' manually.`);
287
- return 1;
288
- }
289
- log.log(v2StatusLine(want));
290
- log.log("Applies to NEW sessions; running sessions keep their pinned multi-agent version. Restart the Codex app (or wait out its picker cache) to see the ladder change.");
291
- return 0;
292
250
  }