@bitkyc08/opencodex 2.77.0 → 2.78.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 (222) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-BtfG5id-.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
  5. package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
  8. package/gui/dist/index.html +2 -2
  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/devin/cloud-direct/chat.ts +7 -2
  15. package/src/adapters/devin/trajectory.ts +49 -0
  16. package/src/adapters/devin.ts +13 -0
  17. package/src/adapters/ollama-native.ts +35 -19
  18. package/src/claude/intercept/client-hello.ts +103 -0
  19. package/src/claude/intercept/model-bindings.ts +3 -2
  20. package/src/claude/intercept/picker-listener.ts +279 -66
  21. package/src/cli/access-audio-input.ts +84 -0
  22. package/src/cli/access-audio-live.ts +99 -0
  23. package/src/cli/access-audio.ts +123 -0
  24. package/src/cli/access-data-client.ts +127 -0
  25. package/src/cli/access-data-plane.ts +104 -0
  26. package/src/cli/access-data-response.ts +76 -0
  27. package/src/cli/access.ts +65 -2
  28. package/src/cli/account-api.ts +30 -6
  29. package/src/cli/account-auth.ts +111 -42
  30. package/src/cli/account-key-quota.ts +118 -0
  31. package/src/cli/account-main.ts +11 -4
  32. package/src/cli/account-next-actions.ts +63 -0
  33. package/src/cli/account-policy-dto.ts +68 -0
  34. package/src/cli/account-policy.ts +167 -0
  35. package/src/cli/account-target.ts +8 -0
  36. package/src/cli/account.ts +34 -8
  37. package/src/cli/agent-runtime-settings.ts +130 -0
  38. package/src/cli/agent-settings.ts +101 -0
  39. package/src/cli/agent.ts +19 -2
  40. package/src/cli/alias.ts +7 -2
  41. package/src/cli/aside-profiles.ts +2 -2
  42. package/src/cli/capabilities-access-remote.ts +147 -0
  43. package/src/cli/capabilities-accounts.ts +195 -0
  44. package/src/cli/capabilities-agents-routing.ts +414 -0
  45. package/src/cli/capabilities-base.ts +783 -0
  46. package/src/cli/capabilities-command.ts +1 -0
  47. package/src/cli/capabilities-integrations.ts +264 -0
  48. package/src/cli/capabilities-lab.ts +214 -0
  49. package/src/cli/capabilities-observe-system.ts +662 -0
  50. package/src/cli/capabilities-provider-models.ts +302 -0
  51. package/src/cli/capabilities.ts +30 -1199
  52. package/src/cli/capability-types.ts +54 -0
  53. package/src/cli/catalog-command-result.ts +57 -0
  54. package/src/cli/chatgpt-command.ts +25 -2
  55. package/src/cli/claude-agent-startup-sync.ts +17 -0
  56. package/src/cli/claude-desktop-profile.ts +100 -0
  57. package/src/cli/claude-desktop.ts +39 -6
  58. package/src/cli/codex-shim-autorestore.ts +2 -1
  59. package/src/cli/codex-shim-readiness.ts +23 -3
  60. package/src/cli/combo-input.ts +88 -0
  61. package/src/cli/combo-result.ts +52 -0
  62. package/src/cli/combo-stats.ts +123 -0
  63. package/src/cli/combo.ts +37 -14
  64. package/src/cli/companion-timeline.ts +142 -0
  65. package/src/cli/companion-usage.ts +146 -0
  66. package/src/cli/companion.ts +12 -3
  67. package/src/cli/config-command.ts +24 -8
  68. package/src/cli/dispatch.ts +52 -79
  69. package/src/cli/doctor.ts +22 -4
  70. package/src/cli/help-catalog.ts +31 -14
  71. package/src/cli/help-recovery.ts +2 -2
  72. package/src/cli/help.ts +25 -11
  73. package/src/cli/index.ts +38 -27
  74. package/src/cli/injection-follow.ts +35 -0
  75. package/src/cli/inspect.ts +5 -0
  76. package/src/cli/integration-aside-sync.ts +69 -0
  77. package/src/cli/integration-cursor.ts +106 -0
  78. package/src/cli/integration-input.ts +60 -0
  79. package/src/cli/integration-journal.ts +49 -0
  80. package/src/cli/integration-plan-dto.ts +156 -0
  81. package/src/cli/integration-preview.ts +215 -0
  82. package/src/cli/integrations.ts +24 -18
  83. package/src/cli/json-input.ts +134 -0
  84. package/src/cli/link.ts +32 -4
  85. package/src/cli/local-sync-result.ts +41 -0
  86. package/src/cli/log-follow.ts +84 -0
  87. package/src/cli/log-view-filter.ts +146 -0
  88. package/src/cli/logout-command.ts +44 -0
  89. package/src/cli/model-picker-ordering.ts +137 -0
  90. package/src/cli/models-custom-input.ts +107 -0
  91. package/src/cli/models-custom-runtime.ts +112 -0
  92. package/src/cli/models-order.ts +194 -0
  93. package/src/cli/models-runtime-subcommands.ts +2 -0
  94. package/src/cli/models-runtime.ts +7 -0
  95. package/src/cli/models.ts +66 -81
  96. package/src/cli/observe-stream.ts +142 -0
  97. package/src/cli/observe.ts +75 -34
  98. package/src/cli/provider-batch.ts +77 -0
  99. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  100. package/src/cli/provider-result.ts +124 -0
  101. package/src/cli/provider-runtime.ts +15 -12
  102. package/src/cli/provider-settings.ts +143 -0
  103. package/src/cli/provider.ts +112 -34
  104. package/src/cli/registry.ts +62 -20
  105. package/src/cli/remote-workspace-hub.ts +83 -0
  106. package/src/cli/remote-workspace.ts +6 -1
  107. package/src/cli/restart-failure.ts +27 -0
  108. package/src/cli/root.ts +12 -0
  109. package/src/cli/route-policy-write.ts +137 -0
  110. package/src/cli/route-policy.ts +8 -0
  111. package/src/cli/runtime-api.ts +56 -79
  112. package/src/cli/secret-args.ts +149 -0
  113. package/src/cli/settings-result.ts +30 -0
  114. package/src/cli/storage.ts +77 -7
  115. package/src/cli/system-command.ts +60 -2
  116. package/src/cli/system-restart-client.ts +20 -3
  117. package/src/cli/system-settings-parity.ts +83 -0
  118. package/src/cli/update-restart-candidate.ts +21 -0
  119. package/src/cli/update-restart-child.ts +81 -0
  120. package/src/cli/update-restart-home.ts +38 -0
  121. package/src/cli/update-restart-transport.ts +193 -0
  122. package/src/cli/update-restart.ts +260 -0
  123. package/src/cli/usage-model-search.ts +58 -0
  124. package/src/cli/usage-report.ts +13 -5
  125. package/src/cli/v2-input.ts +66 -0
  126. package/src/cli/v2-local-output.ts +37 -0
  127. package/src/cli/v2-runtime.ts +90 -0
  128. package/src/cli/v2.ts +102 -144
  129. package/src/clients/config-export/dsh.ts +21 -0
  130. package/src/clients/config-export.ts +7 -1
  131. package/src/codex/catalog/build-entries.ts +1 -1
  132. package/src/codex/catalog/bundled.ts +9 -2
  133. package/src/codex/catalog/heal-observation.ts +66 -0
  134. package/src/codex/catalog/publication-observer.ts +19 -0
  135. package/src/codex/catalog/remote.ts +13 -15
  136. package/src/codex/catalog/restore.ts +19 -6
  137. package/src/codex/catalog/retained-sync.ts +58 -10
  138. package/src/codex/catalog/routed-removal.ts +116 -0
  139. package/src/codex/catalog/write-audit-contract.ts +21 -0
  140. package/src/codex/catalog/write-audit.ts +171 -0
  141. package/src/codex/catalog-self-heal.ts +284 -0
  142. package/src/codex/catalog-write-serialization.ts +71 -7
  143. package/src/codex/codex-home-owner.ts +124 -0
  144. package/src/codex/convergence.ts +55 -15
  145. package/src/codex/features.ts +8 -1
  146. package/src/codex/inject/restore.ts +46 -4
  147. package/src/codex/inject.ts +11 -1
  148. package/src/codex/internal/catalog-writer.ts +120 -13
  149. package/src/codex/journal.ts +49 -13
  150. package/src/codex/management-convergence.ts +3 -0
  151. package/src/codex/quota-types.ts +3 -0
  152. package/src/codex/quota.ts +13 -3
  153. package/src/codex/refresh.ts +20 -9
  154. package/src/codex/runtime.ts +7 -1
  155. package/src/codex/shim-diagnostics.ts +80 -0
  156. package/src/codex/shim-fingerprint.ts +7 -2
  157. package/src/codex/shim-migration.ts +153 -0
  158. package/src/codex/shim-overlay.ts +509 -0
  159. package/src/codex/shim-path-resolution.ts +14 -2
  160. package/src/codex/shim-probe.ts +4 -3
  161. package/src/codex/shim-state-file.ts +93 -7
  162. package/src/codex/shim.ts +15 -68
  163. package/src/codex/sync.ts +24 -4
  164. package/src/combos/failover.ts +19 -0
  165. package/src/combos/index.ts +1 -0
  166. package/src/combos/resolve.ts +12 -0
  167. package/src/config/atomic-write.ts +13 -8
  168. package/src/config/diagnostics.ts +19 -1
  169. package/src/config/load-degrade.ts +3 -0
  170. package/src/config/schema/chatgpt-desktop.ts +22 -0
  171. package/src/config/schema/config-schema.ts +32 -14
  172. package/src/config/schema/leaf-validators.ts +3 -2
  173. package/src/generated/compatibility-version.json +445 -129
  174. package/src/integrations/merge.ts +13 -5
  175. package/src/integrations/mutation-plan.ts +46 -3
  176. package/src/integrations/omp-yaml-source.ts +181 -1
  177. package/src/integrations/ownership-policy.ts +66 -0
  178. package/src/integrations/registry.ts +50 -1
  179. package/src/integrations/state.ts +46 -21
  180. package/src/integrations/target.ts +25 -2
  181. package/src/integrations/writer.ts +89 -31
  182. package/src/lib/package-tree-integrity.ts +42 -1
  183. package/src/lib/proxy-env.ts +3 -0
  184. package/src/lib/test-home-guard.ts +2 -1
  185. package/src/oauth/anthropic-account-refusal.ts +19 -8
  186. package/src/oauth/anthropic-routing.ts +17 -12
  187. package/src/oauth/anthropic.ts +27 -2
  188. package/src/oauth/index.ts +6 -8
  189. package/src/oauth/pool-settings-capability.ts +9 -2
  190. package/src/protocols/plan-snapshot.ts +1 -1
  191. package/src/protocols/settings.ts +28 -4
  192. package/src/server/caller-session-identity.ts +32 -0
  193. package/src/server/claude-messages.ts +11 -4
  194. package/src/server/index/serve-options.ts +12 -7
  195. package/src/server/index.ts +1 -1
  196. package/src/server/management/agent-settings-routes.ts +6 -5
  197. package/src/server/management/context.ts +8 -1
  198. package/src/server/management/cursor-integration-routes.ts +3 -3
  199. package/src/server/management/integration-routes.ts +5 -5
  200. package/src/server/management/model-routes.ts +3 -3
  201. package/src/server/management/native-integration-routes.ts +2 -2
  202. package/src/server/management/oauth-account-routes.ts +96 -30
  203. package/src/server/management/route-registry.ts +25 -24
  204. package/src/server/messages-native-eligibility.ts +3 -6
  205. package/src/server/messages-native-oauth.ts +82 -35
  206. package/src/server/messages-native.ts +78 -16
  207. package/src/server/responses/adapter-dispatch.ts +53 -28
  208. package/src/server/responses/core-auth.ts +10 -4
  209. package/src/server/responses/core-combo.ts +46 -6
  210. package/src/service/cli.ts +6 -1
  211. package/src/service/ownership-mutation-lease.d.mts +26 -0
  212. package/src/service/ownership-mutation-lease.mjs +76 -1
  213. package/src/types/config.ts +2 -0
  214. package/src/types/provider.ts +10 -10
  215. package/src/update/index.ts +2 -1
  216. package/src/update/update-failure-guidance.d.mts +6 -0
  217. package/src/update/update-failure-guidance.mjs +30 -5
  218. package/gui/dist/assets/App-COdEeqSj.css +0 -1
  219. package/gui/dist/assets/App-DEpCJEP_.js +0 -54
  220. package/gui/dist/assets/index-Cd7ittMv.css +0 -1
  221. package/gui/dist/assets/index-DwJKFJm5.js +0 -94
  222. package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
@@ -0,0 +1,260 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { isIP } from "node:net";
3
+ import { existsSync } from "node:fs";
4
+ import { getRuntimePortPath, readRuntimePort, readProcessCommandLine, isOcxStartCommandLine, type RuntimePortState } from "../config/process-state";
5
+ import { configuredAdminToken } from "../lib/admin-secrets";
6
+ import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
7
+ import { isProcessAlive } from "../lib/process-control";
8
+ import { parseStrictSemver } from "../lib/strict-semver";
9
+ import { startArgv } from "../lib/self-launch-argv";
10
+ import { diagnoseService } from "../service/diagnostics";
11
+ import { inspectGuardedManagerTarget } from "../service/guarded-manager-target";
12
+ import { acquireOwnershipMutationLease, unprivilegedOwnershipMutationEnvironment } from "../service/ownership-mutation-lease.mjs";
13
+ import { serviceStatePaths } from "../service/state";
14
+ import { findLiveProxy, probeEndpointLiveness, probeHostname, type LiveProxy } from "../server/proxy-liveness";
15
+ import { waitForPortAvailable } from "../server/ports";
16
+ import { UPDATE_RESTART_CHILD_ENV, type UpdateRestartChildMarker } from "./update-restart-child";
17
+ import { assertUpdateRestartConfiguration, assertUpdateRestartHome, readUpdateRestartHome, type UpdateRestartHome } from "./update-restart-home";
18
+ import { observeAttestedUpdateReplacement, stopAttestedUpdateTarget } from "./update-restart-transport";
19
+ import type { UpdateRestartCandidate } from "./update-restart-candidate";
20
+ import { INCOMPLETE_INSTALL_RECOVERY } from "./restart-failure";
21
+ import { computeVersionSkew } from "./version-skew";
22
+
23
+ export interface UpdateRestartChild { pid?: number; exitCode: number | null; signalCode: string | null }
24
+ export interface UpdateRestartIo {
25
+ now(): number;
26
+ acquire(): { release(): void };
27
+ home(): UpdateRestartHome;
28
+ checkHome(home: UpdateRestartHome): void;
29
+ runtime(): RuntimePortState | null;
30
+ standalone(target: UpdateRestartCandidate["target"]): boolean;
31
+ /** Whether the executable the replacement will run is complete (not the npm `bun` placeholder). */
32
+ runtimeReady(): boolean;
33
+ stop(candidate: UpdateRestartCandidate, deadlineAt: number, beforeStop: () => void): Promise<void>;
34
+ stopped(candidate: UpdateRestartCandidate, deadlineAt: number): Promise<boolean>;
35
+ start(marker: UpdateRestartChildMarker): UpdateRestartChild;
36
+ observe(deadlineAt: number, childPid: number): Promise<LiveProxy | null>;
37
+ wait(ms: number): Promise<void>;
38
+ }
39
+ const ELIGIBILITY_REASONS = ["windows", "unsupported_platform", "foreground", "service", "shared_or_client",
40
+ "package_tree_fenced", "unverifiable_ancestry", "target_changed", "configuration_changed"] as const;
41
+ export type UpdateRestartEligibilityReason = (typeof ELIGIBILITY_REASONS)[number];
42
+
43
+ /** Carries only a closed reason code across the eligibility/transport boundary. */
44
+ export class UpdateRestartEligibilityError extends Error {
45
+ constructor(readonly reason: UpdateRestartEligibilityReason) { super("Update restart eligibility refused"); }
46
+ }
47
+ export type UpdateRestartResult = { ok: true; live: LiveProxy }
48
+ | { ok: false; code: string; reason?: UpdateRestartEligibilityReason };
49
+
50
+ function sameRuntime(candidate: UpdateRestartCandidate, current: RuntimePortState | null): boolean {
51
+ const expected = candidate.runtime;
52
+ return !!current && current.pid === expected.pid && current.port === expected.port
53
+ && current.hostname === expected.hostname && current.attestationSecret === expected.attestationSecret
54
+ && current.siblingOfPort === undefined;
55
+ }
56
+
57
+ /** A terminal update transaction: it never enters generic restart recovery. */
58
+ export async function runUpdateRestart(candidate: UpdateRestartCandidate, deadlineAt: number, io: UpdateRestartIo): Promise<UpdateRestartResult> {
59
+ let phase = "eligibility";
60
+ let lease: { release(): void } | undefined;
61
+ // Recorded here because the stop transport sanitizes anything thrown by beforeStop.
62
+ let runtimeRefused = false;
63
+ let eligibilityReason: UpdateRestartEligibilityReason | undefined;
64
+ try {
65
+ if (!parseStrictSemver(candidate.cliVersion) || candidate.cliVersion === "0.0.0"
66
+ || computeVersionSkew(candidate.cliVersion, candidate.target.version).relation !== "cli-newer") {
67
+ throw new Error("version");
68
+ }
69
+ const withinDeadline = () => { if (io.now() >= deadlineAt) throw new Error("deadline"); };
70
+ withinDeadline();
71
+ lease = io.acquire();
72
+ const home = candidate.home;
73
+ if (JSON.stringify(io.home()) !== JSON.stringify(home)) throw new Error("home_changed");
74
+ const revalidate = () => {
75
+ withinDeadline();
76
+ io.checkHome(home);
77
+ const currentRuntime = io.runtime();
78
+ if (!sameRuntime(candidate, currentRuntime)) {
79
+ eligibilityReason = currentRuntime?.siblingOfPort !== undefined ? "shared_or_client" : "target_changed";
80
+ throw new Error("target");
81
+ }
82
+ try {
83
+ if (!io.standalone(candidate.target)) throw new Error("target");
84
+ } catch (error) {
85
+ if (error instanceof UpdateRestartEligibilityError && ELIGIBILITY_REASONS.includes(error.reason)) eligibilityReason = error.reason;
86
+ throw error;
87
+ }
88
+ if (!io.runtimeReady()) {
89
+ runtimeRefused = true;
90
+ throw new Error("runtime");
91
+ }
92
+ withinDeadline();
93
+ };
94
+ revalidate();
95
+ phase = "stop";
96
+ await io.stop(candidate, deadlineAt, revalidate);
97
+ withinDeadline();
98
+ phase = "settle";
99
+ if (!await io.stopped(candidate, deadlineAt)) throw new Error("stop_unconfirmed");
100
+ // Shutdown is confirmed from here on and nothing has launched yet.
101
+ phase = "prelaunch";
102
+ withinDeadline();
103
+ io.checkHome(home);
104
+ // An in-place npm install can still be swapping its Bun in; wait for it inside the deadline,
105
+ // then repeat the launch preconditions synchronously so the wait opens no gap before spawn.
106
+ phase = "runtime";
107
+ while (!io.runtimeReady()) {
108
+ withinDeadline();
109
+ await io.wait(Math.min(100, deadlineAt - io.now()));
110
+ }
111
+ phase = "prelaunch";
112
+ withinDeadline();
113
+ io.checkHome(home);
114
+ phase = "start";
115
+ const child = io.start({ home, version: candidate.cliVersion, port: candidate.target.port, hostname: candidate.runtime.hostname ?? "", deadlineAt });
116
+ // The child must acquire its own lease and validate this frozen handoff before preflight.
117
+ lease.release();
118
+ lease = undefined;
119
+ if (!Number.isSafeInteger(child.pid) || !child.pid || child.pid === candidate.target.pid) throw new Error("launch_unknown");
120
+ phase = "replacement";
121
+ while (io.now() < deadlineAt) {
122
+ if (child.exitCode !== null || child.signalCode !== null) throw new Error("child_exited");
123
+ io.checkHome(home);
124
+ const live = await io.observe(deadlineAt, child.pid);
125
+ withinDeadline();
126
+ if (live) {
127
+ io.checkHome(home);
128
+ if (live.source !== "runtime" || live.pid !== child.pid || live.port !== candidate.target.port
129
+ || (live.hostname ?? "") !== (candidate.target.hostname ?? "") || live.role === "client"
130
+ || live.packageTreeFenced || live.version !== candidate.cliVersion) throw new Error("replacement_mismatch");
131
+ return { ok: true, live };
132
+ }
133
+ await io.wait(Math.min(100, deadlineAt - io.now()));
134
+ }
135
+ throw new Error("deadline");
136
+ } catch {
137
+ return { ok: false, code: runtimeRefused ? "update_restart_runtime_incomplete" : `update_restart_${phase}_failed`,
138
+ ...(eligibilityReason ? { reason: eligibilityReason } : {}) };
139
+ }
140
+ finally { lease?.release(); }
141
+ }
142
+
143
+ /** Sanitized, actionable text for each terminal update-restart code; it claims only what that phase proved. */
144
+ export function describeUpdateRestartFailure(code: string, reason?: UpdateRestartEligibilityReason): string {
145
+ if (reason && (code === "update_restart_eligibility_failed" || code === "update_restart_stop_failed")) {
146
+ const action = (() => {
147
+ switch (reason) {
148
+ case "windows": return "Restart from a newer CLI is unavailable on Windows. Run `ocx status`, then restart through the owning service or desktop app.";
149
+ case "unsupported_platform": return "Restart from a newer CLI requires macOS or Linux. Run `ocx status`, then use the owning lifecycle manager.";
150
+ case "service": return "An installed or active service owns lifecycle control. Run `ocx status`, then use the owning installation's `ocx service restart`.";
151
+ case "foreground": return "The proxy is attached to a parent process. Run `ocx status`; if it runs in a terminal, restart it in that terminal, otherwise use its owning app or supervisor.";
152
+ case "shared_or_client": return "This is a shared or connected-client runtime. Run `ocx status` and restart through its owning Hub, service or desktop app.";
153
+ case "package_tree_fenced": return `The proxy's package files are unsettled. Wait for the install to finish. ${INCOMPLETE_INSTALL_RECOVERY}`;
154
+ case "unverifiable_ancestry": return "The proxy's process ancestry or manager could not be verified. Run `ocx status` to identify its owner before restarting.";
155
+ case "configuration_changed": return "The selected configuration or connection changed. Run `ocx status` to inspect the intended proxy before restarting.";
156
+ case "target_changed": return "The captured proxy identity changed. Run `ocx status` to inspect the intended proxy before restarting.";
157
+ }
158
+ })();
159
+ return `${action} Nothing was stopped by this restart attempt.`;
160
+ }
161
+ switch (code) {
162
+ case "update_restart_runtime_incomplete":
163
+ return `This installation's Bun runtime is incomplete; nothing was stopped. Wait for the install to finish, then run \`ocx restart\` again. ${INCOMPLETE_INSTALL_RECOVERY}`;
164
+ case "update_restart_eligibility_failed":
165
+ return "The running proxy is not eligible for an update restart from this CLI (it is supervised, shared, or changed); nothing was stopped. Run `ocx status` to inspect the intended proxy.";
166
+ case "update_restart_stop_failed":
167
+ return "The guarded stop of the old proxy could not be confirmed; inspect `ocx status` before retrying.";
168
+ case "update_restart_settle_failed":
169
+ return "The old proxy was not confirmed stopped, so no second proxy was started; inspect `ocx status` before retrying.";
170
+ case "update_restart_prelaunch_failed":
171
+ return "The old proxy stopped, but its home, ownership or deadline changed before launch, so nothing was launched. Check `ocx status`; if no proxy is running, run `ocx start`.";
172
+ case "update_restart_runtime_failed":
173
+ return `The old proxy stopped, but this installation's Bun runtime did not finish installing in time, so nothing was launched. ${INCOMPLETE_INSTALL_RECOVERY} Run \`ocx status\`; if no proxy is running, run \`ocx start\` once the install completes.`;
174
+ case "update_restart_start_failed":
175
+ return "The old proxy stopped, but the new proxy's launch could not be confirmed. Check `ocx status`; if no proxy is running, run `ocx start`.";
176
+ case "update_restart_replacement_failed":
177
+ return "The new proxy did not verify as the expected replacement in time; inspect `ocx status` before retrying.";
178
+ default:
179
+ return "Update restart could not be confirmed; inspect `ocx status` before retrying.";
180
+ }
181
+ }
182
+
183
+ function standalone(target: UpdateRestartCandidate["target"]): boolean {
184
+ try { assertUpdateRestartConfiguration(target.hostname ?? ""); }
185
+ catch { throw new UpdateRestartEligibilityError("configuration_changed"); }
186
+ if (process.platform !== "darwin" && process.platform !== "linux") {
187
+ throw new UpdateRestartEligibilityError(process.platform === "win32" ? "windows" : "unsupported_platform");
188
+ }
189
+ const host = probeHostname(target.hostname).replace(/^\[|\]$/g, "");
190
+ if (target.role === "client") throw new UpdateRestartEligibilityError("shared_or_client");
191
+ if (target.packageTreeFenced) throw new UpdateRestartEligibilityError("package_tree_fenced");
192
+ if (!isIP(host) || target.source !== "runtime") throw new UpdateRestartEligibilityError("unverifiable_ancestry");
193
+ const command = readProcessCommandLine(target.pid);
194
+ if (!command || !isOcxStartCommandLine(command)) throw new UpdateRestartEligibilityError("unverifiable_ancestry");
195
+ if (diagnoseService().installed) throw new UpdateRestartEligibilityError("service");
196
+ const manager = inspectGuardedManagerTarget(target.pid, target.port);
197
+ if (manager.kind !== "absent") throw new UpdateRestartEligibilityError(manager.kind === "bound" ? "service" : "unverifiable_ancestry");
198
+ let parent: string;
199
+ try {
200
+ parent = execFileSync("ps", ["-o", "ppid=", "-p", String(target.pid)], {
201
+ encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"],
202
+ }).trim();
203
+ } catch { throw new UpdateRestartEligibilityError("unverifiable_ancestry"); }
204
+ if (!/^[1-9]\d*$/.test(parent)) throw new UpdateRestartEligibilityError("unverifiable_ancestry");
205
+ if (parent !== "1") throw new UpdateRestartEligibilityError("foreground");
206
+ return true;
207
+ }
208
+
209
+ /** Production composition keeps stop/start side effects out of cli/index.ts. */
210
+ export function restartFromCurrentInstallation(candidate: UpdateRestartCandidate, deadlineAt: number, environment: NodeJS.ProcessEnv): Promise<UpdateRestartResult> {
211
+ const argv = startArgv(candidate.target.port);
212
+ const executable = process.execPath;
213
+ return runUpdateRestart(candidate, deadlineAt, {
214
+ now: Date.now,
215
+ acquire: () => acquireOwnershipMutationLease(serviceStatePaths(), { waitMs: Math.max(0, Math.min(2000, deadlineAt - Date.now())) }),
216
+ home: readUpdateRestartHome, checkHome: assertUpdateRestartHome, runtime: readRuntimePort, standalone,
217
+ runtimeReady: () => isRealBunBinary(executable),
218
+ stop: async (target, deadline, beforeStop) => {
219
+ const token = configuredAdminToken();
220
+ if (!token) throw new Error("update_restart_credential_unavailable");
221
+ await stopAttestedUpdateTarget({ target: target.target, secret: target.runtime.attestationSecret,
222
+ cliVersion: target.cliVersion, deadlineAt: deadline, beforeStop, adminToken: token });
223
+ },
224
+ stopped: async (target, deadline) => {
225
+ while (Date.now() < deadline && isProcessAlive(target.target.pid)) await Bun.sleep(Math.min(100, deadline - Date.now()));
226
+ if (Date.now() >= deadline || existsSync(getRuntimePortPath())) return false;
227
+ const endpoint = { port: target.target.port, hostname: target.target.hostname };
228
+ if (!await waitForPortAvailable(endpoint.port, probeHostname(endpoint.hostname),
229
+ { timeoutMs: Math.max(0, Math.min(1000, deadline - Date.now())), intervalMs: 50 })) return false;
230
+ if (Date.now() >= deadline) return false;
231
+ return await probeEndpointLiveness(endpoint, { timeoutMs: Math.min(250, deadline - Date.now()) }) === "dead";
232
+ },
233
+ start: marker => {
234
+ const env = unprivilegedOwnershipMutationEnvironment(environment);
235
+ env.OPENCODEX_HOME = marker.home.config.path;
236
+ env.CODEX_HOME = marker.home.codex.path;
237
+ env[UPDATE_RESTART_CHILD_ENV] = JSON.stringify(marker);
238
+ const child = spawn(executable, argv, { detached: true, stdio: "ignore", windowsHide: true, env });
239
+ child.on("error", () => {}); // exit/health evidence reports failure; never retry a possibly launched child.
240
+ child.unref();
241
+ return child;
242
+ },
243
+ observe: async (deadline, childPid) => {
244
+ const live = await findLiveProxy({ deadlineAt: deadline, attempts: 1 });
245
+ if (!live) return null;
246
+ if (live.pid !== childPid) throw new Error("update_restart_replacement_changed");
247
+ const runtime = readRuntimePort(childPid);
248
+ if (!runtime?.attestationSecret || runtime.port !== candidate.target.port
249
+ || runtime.hostname !== candidate.runtime.hostname || runtime.siblingOfPort !== undefined) {
250
+ throw new Error("update_restart_replacement_unattested");
251
+ }
252
+ const attested = await observeAttestedUpdateReplacement({ target: { ...live, pid: childPid },
253
+ secret: runtime.attestationSecret, version: candidate.cliVersion, deadlineAt: deadline });
254
+ const current = readRuntimePort(childPid);
255
+ if (JSON.stringify(current) !== JSON.stringify(runtime)) throw new Error("update_restart_replacement_changed");
256
+ return attested;
257
+ },
258
+ wait: ms => Bun.sleep(ms),
259
+ });
260
+ }
@@ -0,0 +1,58 @@
1
+ /** Model-row display search; server attribution and report totals stay unchanged. */
2
+ import { CliUsageError } from "./runtime-api";
3
+
4
+ export interface UsageModelView {
5
+ query: string;
6
+ matchedModelCount: number;
7
+ returnedModelCount: number;
8
+ limit: 100;
9
+ truncated: boolean;
10
+ }
11
+
12
+ interface SearchableModel {
13
+ model: string;
14
+ provider: string;
15
+ resolvedModel?: string;
16
+ requests: number;
17
+ totalTokens: number;
18
+ estimatedCostUsd?: number;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ /** Unlike exact attribution flags, an explicit empty search is a valid view. */
23
+ export function takeUsageSearchOption(args: string[]): string | undefined {
24
+ const indices = args.flatMap((arg, index) => arg === "--search" || arg.startsWith("--search=") ? [index] : []);
25
+ if (indices.length > 1) throw new CliUsageError("--search was given more than once");
26
+ const index = indices[0];
27
+ if (index === undefined) return undefined;
28
+ const arg = args[index]!;
29
+ if (arg.startsWith("--search=")) {
30
+ args.splice(index, 1);
31
+ return arg.slice("--search=".length);
32
+ }
33
+ const value = args[index + 1];
34
+ if (value === undefined || value.startsWith("--")) throw new CliUsageError("--search requires a value; use --search= for an empty search");
35
+ args.splice(index, 2);
36
+ return value;
37
+ }
38
+
39
+ export function selectUsageModelView<T extends { models?: unknown }>(report: T, rawQuery: string): Omit<T, "models"> & { models: SearchableModel[]; modelView: UsageModelView } {
40
+ if (!report || !Array.isArray(report.models)) throw new Error("Invalid usage model rows; retry after checking the runtime.");
41
+ const models = report.models.map((raw: unknown): SearchableModel => {
42
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Invalid usage model row.");
43
+ const row = raw as Record<string, unknown>;
44
+ if (typeof row.model !== "string" || typeof row.provider !== "string"
45
+ || (row.resolvedModel !== undefined && typeof row.resolvedModel !== "string")
46
+ || ![row.requests, row.totalTokens].every(value => typeof value === "number" && Number.isFinite(value) && value >= 0)
47
+ || (row.estimatedCostUsd !== undefined && (typeof row.estimatedCostUsd !== "number" || !Number.isFinite(row.estimatedCostUsd)))) {
48
+ throw new Error("Invalid usage model row.");
49
+ }
50
+ return row as SearchableModel;
51
+ });
52
+ const query = rawQuery.trim().toLowerCase();
53
+ const matching = models.toSorted((a, b) => b.totalTokens - a.totalTokens).filter(row => !query
54
+ || row.model.toLowerCase().includes(query) || row.provider.toLowerCase().includes(query)
55
+ || (row.resolvedModel ?? "").toLowerCase().includes(query));
56
+ const selected = matching.slice(0, 100);
57
+ return { ...report, models: selected, modelView: { query, matchedModelCount: matching.length, returnedModelCount: selected.length, limit: 100, truncated: matching.length > selected.length } };
58
+ }
@@ -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
+ }