@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -7,21 +7,23 @@ import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
7
7
  import { isProcessAlive } from "../lib/process-control";
8
8
  import { parseStrictSemver } from "../lib/strict-semver";
9
9
  import { startArgv } from "../lib/self-launch-argv";
10
- import { diagnoseService } from "../service/diagnostics";
11
- import { inspectGuardedManagerTarget } from "../service/guarded-manager-target";
10
+ import { inspectGuardedManagerTarget, type GuardedManagerDeps } from "../service/guarded-manager-target";
12
11
  import { acquireOwnershipMutationLease, unprivilegedOwnershipMutationEnvironment } from "../service/ownership-mutation-lease.mjs";
13
12
  import { serviceStatePaths } from "../service/state";
14
13
  import { findLiveProxy, probeEndpointLiveness, probeHostname, type LiveProxy } from "../server/proxy-liveness";
15
14
  import { waitForPortAvailable } from "../server/ports";
16
15
  import { UPDATE_RESTART_CHILD_ENV, type UpdateRestartChildMarker } from "./update-restart-child";
17
- import { assertUpdateRestartConfiguration, assertUpdateRestartHome, readUpdateRestartHome, type UpdateRestartHome } from "./update-restart-home";
16
+ import { assertUpdateRestartConfiguration, assertUpdateRestartHome, readUpdateRestartHome, type UpdateRestartHome, type UpdateRestartHomeDeps } from "./update-restart-home";
18
17
  import { observeAttestedUpdateReplacement, stopAttestedUpdateTarget } from "./update-restart-transport";
19
18
  import type { UpdateRestartCandidate } from "./update-restart-candidate";
20
19
  import { INCOMPLETE_INSTALL_RECOVERY } from "./restart-failure";
21
20
  import { computeVersionSkew } from "./version-skew";
21
+ import { createSupervisionLatch, inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
22
+ import { classifyUpdateRestartSystemdSupervision, probeUpdateRestartSupervision, resolveUpdateRestartSupervisor, runBoundedUpdateRestartSupervisor, UPDATE_RESTART_SYSTEMD_ARGS, type UpdateRestartSupervisionDeps } from "./update-restart-supervision";
22
23
 
23
24
  export interface UpdateRestartChild { pid?: number; exitCode: number | null; signalCode: string | null }
24
25
  export interface UpdateRestartIo {
26
+ inspectSupervision?: typeof inspectDesktopSupervision;
25
27
  now(): number;
26
28
  acquire(): { release(): void };
27
29
  home(): UpdateRestartHome;
@@ -37,7 +39,7 @@ export interface UpdateRestartIo {
37
39
  wait(ms: number): Promise<void>;
38
40
  }
39
41
  const ELIGIBILITY_REASONS = ["windows", "unsupported_platform", "foreground", "service", "shared_or_client",
40
- "package_tree_fenced", "unverifiable_ancestry", "target_changed", "configuration_changed"] as const;
42
+ "package_tree_fenced", "unverifiable_ancestry", "target_changed", "configuration_changed", "desktop"] as const;
41
43
  export type UpdateRestartEligibilityReason = (typeof ELIGIBILITY_REASONS)[number];
42
44
 
43
45
  /** Carries only a closed reason code across the eligibility/transport boundary. */
@@ -56,6 +58,7 @@ function sameRuntime(candidate: UpdateRestartCandidate, current: RuntimePortStat
56
58
 
57
59
  /** A terminal update transaction: it never enters generic restart recovery. */
58
60
  export async function runUpdateRestart(candidate: UpdateRestartCandidate, deadlineAt: number, io: UpdateRestartIo): Promise<UpdateRestartResult> {
61
+ const latch = createSupervisionLatch();
59
62
  let phase = "eligibility";
60
63
  let lease: { release(): void } | undefined;
61
64
  // Recorded here because the stop transport sanitizes anything thrown by beforeStop.
@@ -79,6 +82,13 @@ export async function runUpdateRestart(candidate: UpdateRestartCandidate, deadli
79
82
  eligibilityReason = currentRuntime?.siblingOfPort !== undefined ? "shared_or_client" : "target_changed";
80
83
  throw new Error("target");
81
84
  }
85
+ const supervision = (io.inspectSupervision ?? inspectDesktopSupervision)({ targetPid: candidate.target.pid });
86
+ const evidence = supervision.kind === "desktop" && supervision.runtimePid !== candidate.target.pid
87
+ ? { kind: "unknown" as const, reason: "unrelated-target", desktopSeen: false } : supervision;
88
+ if (latch.observe(evidence)) {
89
+ eligibilityReason = "desktop";
90
+ throw new UpdateRestartEligibilityError("desktop");
91
+ }
82
92
  try {
83
93
  if (!io.standalone(candidate.target)) throw new Error("target");
84
94
  } catch (error) {
@@ -147,6 +157,7 @@ export function describeUpdateRestartFailure(code: string, reason?: UpdateRestar
147
157
  switch (reason) {
148
158
  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
159
  case "unsupported_platform": return "Restart from a newer CLI requires macOS or Linux. Run `ocx status`, then use the owning lifecycle manager.";
160
+ case "desktop": return "OpenCodex Desktop supervises this proxy. Use the app's updater (tray → Check for Updates), or quit OpenCodex before updating and restarting from this CLI.";
150
161
  case "service": return "An installed or active service owns lifecycle control. Run `ocx status`, then use the owning installation's `ocx service restart`.";
151
162
  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
163
  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.";
@@ -180,29 +191,64 @@ export function describeUpdateRestartFailure(code: string, reason?: UpdateRestar
180
191
  }
181
192
  }
182
193
 
183
- function standalone(target: UpdateRestartCandidate["target"]): boolean {
184
- try { assertUpdateRestartConfiguration(target.hostname ?? ""); }
194
+ export interface UpdateRestartStandaloneDeps {
195
+ platform?: NodeJS.Platform;
196
+ expectedHome?: UpdateRestartHome;
197
+ checkConfiguration?: typeof assertUpdateRestartConfiguration;
198
+ command?: typeof readProcessCommandLine;
199
+ parent?: (pid: number) => string;
200
+ home?: UpdateRestartHomeDeps;
201
+ supervision?: UpdateRestartSupervisionDeps;
202
+ manager?: GuardedManagerDeps;
203
+ }
204
+
205
+ export function standalone(target: UpdateRestartCandidate["target"], deadlineAt: number, deps: UpdateRestartStandaloneDeps = {}): boolean {
206
+ const platform = deps.platform ?? process.platform;
207
+ try { (deps.checkConfiguration ?? assertUpdateRestartConfiguration)(target.hostname ?? ""); }
185
208
  catch { throw new UpdateRestartEligibilityError("configuration_changed"); }
186
- if (process.platform !== "darwin" && process.platform !== "linux") {
187
- throw new UpdateRestartEligibilityError(process.platform === "win32" ? "windows" : "unsupported_platform");
209
+ if (platform !== "darwin" && platform !== "linux") {
210
+ throw new UpdateRestartEligibilityError(platform === "win32" ? "windows" : "unsupported_platform");
188
211
  }
189
212
  const host = probeHostname(target.hostname).replace(/^\[|\]$/g, "");
190
213
  if (target.role === "client") throw new UpdateRestartEligibilityError("shared_or_client");
191
214
  if (target.packageTreeFenced) throw new UpdateRestartEligibilityError("package_tree_fenced");
192
215
  if (!isIP(host) || target.source !== "runtime") throw new UpdateRestartEligibilityError("unverifiable_ancestry");
193
- const command = readProcessCommandLine(target.pid);
216
+ const command = (deps.command ?? readProcessCommandLine)(target.pid);
194
217
  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);
218
+ let home: UpdateRestartHome;
219
+ try {
220
+ home = readUpdateRestartHome(deps.home);
221
+ if (deps.expectedHome && JSON.stringify(home) !== JSON.stringify(deps.expectedHome)) throw new Error("changed");
222
+ }
223
+ catch { throw new UpdateRestartEligibilityError("service"); }
224
+ const supervision = { ...deps.supervision, platform };
225
+ const managerCommand = resolveUpdateRestartSupervisor(supervision);
226
+ if (!managerCommand) throw new UpdateRestartEligibilityError("service");
227
+ if (probeUpdateRestartSupervision(deadlineAt, supervision, managerCommand) !== "inactive") throw new UpdateRestartEligibilityError("service");
228
+ const manager = inspectGuardedManagerTarget(target.pid, target.port, {
229
+ ...deps.manager, platform,
230
+ launchctl: args => {
231
+ const result = runBoundedUpdateRestartSupervisor(managerCommand, args, deadlineAt, supervision);
232
+ return { ...result, ok: result.status === 0 };
233
+ },
234
+ systemdShow: () => {
235
+ const result = runBoundedUpdateRestartSupervisor(managerCommand, UPDATE_RESTART_SYSTEMD_ARGS, deadlineAt, supervision);
236
+ if (classifyUpdateRestartSystemdSupervision(result) !== "inactive") throw new Error("update_restart_supervision_unverified");
237
+ return result.stdout;
238
+ },
239
+ });
197
240
  if (manager.kind !== "absent") throw new UpdateRestartEligibilityError(manager.kind === "bound" ? "service" : "unverifiable_ancestry");
198
241
  let parent: string;
199
242
  try {
200
- parent = execFileSync("ps", ["-o", "ppid=", "-p", String(target.pid)], {
243
+ parent = deps.parent ? deps.parent(target.pid) : execFileSync("ps", ["-o", "ppid=", "-p", String(target.pid)], {
201
244
  encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"],
202
245
  }).trim();
203
246
  } catch { throw new UpdateRestartEligibilityError("unverifiable_ancestry"); }
204
247
  if (!/^[1-9]\d*$/.test(parent)) throw new UpdateRestartEligibilityError("unverifiable_ancestry");
205
248
  if (parent !== "1") throw new UpdateRestartEligibilityError("foreground");
249
+ try {
250
+ if (JSON.stringify(readUpdateRestartHome(deps.home)) !== JSON.stringify(home)) throw new Error("changed");
251
+ } catch { throw new UpdateRestartEligibilityError("service"); }
206
252
  return true;
207
253
  }
208
254
 
@@ -213,7 +259,7 @@ export function restartFromCurrentInstallation(candidate: UpdateRestartCandidate
213
259
  return runUpdateRestart(candidate, deadlineAt, {
214
260
  now: Date.now,
215
261
  acquire: () => acquireOwnershipMutationLease(serviceStatePaths(), { waitMs: Math.max(0, Math.min(2000, deadlineAt - Date.now())) }),
216
- home: readUpdateRestartHome, checkHome: assertUpdateRestartHome, runtime: readRuntimePort, standalone,
262
+ home: readUpdateRestartHome, checkHome: home => assertUpdateRestartHome(home, deadlineAt), runtime: readRuntimePort, standalone: target => standalone(target, deadlineAt, { expectedHome: candidate.home }),
217
263
  runtimeReady: () => isRealBunBinary(executable),
218
264
  stop: async (target, deadline, beforeStop) => {
219
265
  const token = configuredAdminToken();
package/src/cli/v2.ts CHANGED
@@ -15,15 +15,14 @@ import { parseV2Command, V2_USAGE, type V2ParsedCommand } from "./v2-input";
15
15
  import { handleV2RuntimeCommand } from "./v2-runtime";
16
16
  import { localV2State, v2LocalSyncResult, type V2LocalSync } from "./v2-local-output";
17
17
  import { execFileSync } from "node:child_process";
18
- import { dirname } from "node:path";
19
- import { activeCodexConfigPath, getAgentsEnabled, getAgentsMaxDepth, getLogicalMaxThreads, getMultiAgentModeHintText, getSubagentDeveloperInstructions, hasAgentsMaxThreads, isMultiAgentV2Enabled, setMultiAgentModeHintText, transitionMultiAgentV2 } from "../codex/features";
18
+ import { getAgentsEnabled, getAgentsMaxDepth, getLogicalMaxThreads, getMultiAgentModeHintText, getSubagentDeveloperInstructions, hasAgentsMaxThreads, isMultiAgentV2Enabled, setMultiAgentModeHintText, transitionMultiAgentV2 } from "../codex/features";
20
19
 
21
20
  import { commandInvocation, type SpawnInvocation } from "../lib/win-exec";
22
21
  import { deleteConfigTopLevelKey, loadConfig, saveConfig } from "../config";
23
22
  import { resolveAndPersistCodexRuntime, type ResolveCodexRuntimeDeps } from "../codex/runtime";
24
23
 
25
24
  export interface V2CliDeps {
26
- execFile?: (file: string, args: string[], options?: SpawnInvocation["options"]) => void;
25
+ execFile?: (file: string, args: string[], options?: SpawnInvocation["options"] & { env?: NodeJS.ProcessEnv }) => void;
27
26
  isEnabled?: typeof isMultiAgentV2Enabled;
28
27
  hasMaxThreads?: typeof hasAgentsMaxThreads;
29
28
  sync?: (port?: number) => Promise<unknown>;
@@ -35,7 +34,8 @@ export interface V2CliDeps {
35
34
 
36
35
  export type CodexFeaturesInvocationDeps =
37
36
  & Parameters<typeof commandInvocation>[3]
38
- & Pick<ResolveCodexRuntimeDeps, "existsSync" | "execFileSync" | "configDir" | "readFileSync">;
37
+ & Pick<ResolveCodexRuntimeDeps, "existsSync" | "execFileSync" | "configDir" | "readFileSync">
38
+ & { requireAvailable?: boolean };
39
39
 
40
40
  /**
41
41
  * Shared invocation for `codex features enable|disable <feature>` — the single
@@ -51,14 +51,18 @@ export function codexFeaturesInvocation(
51
51
  platform: NodeJS.Platform = process.platform,
52
52
  deps: CodexFeaturesInvocationDeps = {},
53
53
  ): SpawnInvocation {
54
- const command = resolveAndPersistCodexRuntime({
54
+ const resolved = resolveAndPersistCodexRuntime({
55
55
  env: deps.env ?? process.env,
56
56
  platform,
57
57
  existsSync: deps.existsSync,
58
58
  execFileSync: deps.execFileSync,
59
59
  configDir: deps.configDir,
60
60
  readFileSync: deps.readFileSync,
61
- }).runtime.command || "codex";
61
+ });
62
+ if (deps.requireAvailable && resolved.runtime.source === "fallback" && resolved.runtime.version === null) {
63
+ throw Object.assign(new Error("spawn codex ENOENT"), { code: "ENOENT" });
64
+ }
65
+ const command = resolved.runtime.command || "codex";
62
66
  return commandInvocation(command, ["features", action, feature], platform, deps);
63
67
  }
64
68
 
@@ -70,27 +74,30 @@ export function codexFeaturesInvocation(
70
74
  */
71
75
  export function runCodexFeaturesCommand(
72
76
  action: "enable" | "disable",
73
- feature: string = "multi_agent_v2",
77
+ feature: string,
78
+ env: NodeJS.ProcessEnv,
79
+ validateBeforeSpawn: () => void,
80
+ preparedInvocation?: SpawnInvocation,
74
81
  ): void {
75
- const inv = codexFeaturesInvocation(action, feature);
82
+ const inv = preparedInvocation ?? codexFeaturesInvocation(action, feature, process.platform, { env });
83
+ validateBeforeSpawn();
76
84
  execFileSync(inv.file, inv.args,
77
85
  {
78
86
  stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true, encoding: "utf8",
79
- // The reader resolves $CODEX_HOME at call time (including the WSL Windows-home
80
- // detection); force the same home on the child so it never toggles a different
81
- // config than the one the postcondition re-reads.
82
- env: { ...process.env, CODEX_HOME: dirname(activeCodexConfigPath()) },
83
87
  ...inv.options,
88
+ env,
84
89
  });
85
90
  }
86
91
 
87
- function runCodexFeatures(action: "enable" | "disable", deps: V2CliDeps): void {
92
+ function runCodexFeatures(action: "enable" | "disable", deps: V2CliDeps, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void): void {
88
93
  if (deps.execFile) {
89
- const inv = (deps.featuresInvocation ?? codexFeaturesInvocation)(action);
90
- deps.execFile(inv.file, inv.args, inv.options);
94
+ const inv = deps.featuresInvocation ? deps.featuresInvocation(action)
95
+ : codexFeaturesInvocation(action, "multi_agent_v2", process.platform, { env });
96
+ validateBeforeSpawn();
97
+ deps.execFile(inv.file, inv.args, { ...inv.options, env });
91
98
  return;
92
99
  }
93
- runCodexFeaturesCommand(action);
100
+ runCodexFeaturesCommand(action, "multi_agent_v2", env, validateBeforeSpawn);
94
101
  }
95
102
 
96
103
  export function v2StatusLine(enabled: boolean): string {
@@ -182,7 +189,7 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
182
189
  }
183
190
  if (parsed.verb === "threads") {
184
191
  const enabled = (deps.isEnabled ?? isMultiAgentV2Enabled)();
185
- const result = transitionMultiAgentV2(enabled, next => runCodexFeatures(next ? "enable" : "disable", deps), { threadLimit: parsed.value });
192
+ const result = transitionMultiAgentV2(enabled, (next, env, validate) => runCodexFeatures(next ? "enable" : "disable", deps, env, validate), { threadLimit: parsed.value });
186
193
  if (!result.ok) return emit(false, ["Unable to update the native thread limit. Read ocx v2 status before retrying."]);
187
194
  changed = result.changed;
188
195
  return emit(true, [changed ? `max_threads = ${parsed.value} (${enabled ? "v2" : "v1"}) — applies to new sessions.` : `max_threads already ${parsed.value} — nothing to do.`]);
@@ -194,7 +201,7 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
194
201
  changed = (config.multiAgentMode ?? "default") !== parsed.value;
195
202
  if (parsed.value !== "default") {
196
203
  const target = parsed.value === "v2" && config.keepNativeChatGptOnV1 !== true;
197
- const transition = transitionMultiAgentV2(target, next => runCodexFeatures(next ? "enable" : "disable", deps));
204
+ const transition = transitionMultiAgentV2(target, (next, env, validate) => runCodexFeatures(next ? "enable" : "disable", deps, env, validate));
198
205
  if (!transition.ok) { changed = false; return emit(false, ["Native mode transition failed. Read ocx v2 status before retrying."]); }
199
206
  changed ||= transition.changed;
200
207
  }
@@ -207,7 +214,7 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
207
214
  unchangedKeep = (config.keepNativeChatGptOnV1 === true) === parsed.value;
208
215
  changed = !unchangedKeep;
209
216
  if (parsed.value && requiresGlobalV2Disabled(config.multiAgentMode, true)) {
210
- const transition = transitionMultiAgentV2(false, next => runCodexFeatures(next ? "enable" : "disable", deps));
217
+ const transition = transitionMultiAgentV2(false, (next, env, validate) => runCodexFeatures(next ? "enable" : "disable", deps, env, validate));
211
218
  if (!transition.ok) { changed = false; return emit(false, ["Native hybrid-mode transition failed. Read ocx v2 status before retrying."]); }
212
219
  changed ||= transition.changed;
213
220
  }
@@ -222,7 +229,7 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
222
229
  if (want && requiresGlobalV2Disabled(config.multiAgentMode, config.keepNativeChatGptOnV1 === true)) {
223
230
  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."]);
224
231
  }
225
- const transition = transitionMultiAgentV2(want, next => runCodexFeatures(next ? "enable" : "disable", deps));
232
+ const transition = transitionMultiAgentV2(want, (next, env, validate) => runCodexFeatures(next ? "enable" : "disable", deps, env, validate));
226
233
  if (!transition.ok) return emit(false, ["Native feature transition failed. Read ocx v2 status before retrying."]);
227
234
  changed = transition.changed;
228
235
  if (!changed) return emit(true, [`multi_agent_v2 already ${want ? "ON" : "OFF"} — nothing to do.`]);
@@ -0,0 +1,77 @@
1
+ import { readConfigDiagnostics } from "../config/diagnostics";
2
+ import { readRuntimePort } from "../config/process-state";
3
+ import { packageVersion } from "./help";
4
+ import { proxyIdentityAt } from "../server/proxy-liveness";
5
+ import { computeVersionSkew } from "./version-skew";
6
+
7
+ const LIFECYCLE = new Set(["start", "stop", "restart", "update", "service"]);
8
+ const NOTICE_DEADLINE_MS = 200;
9
+ let printed = false;
10
+
11
+ export function shouldNoticeVersionSkew(command: string | undefined, args: string[]): boolean {
12
+ const boundary = args.indexOf("--");
13
+ const prefix = boundary < 0 ? args : args.slice(0, boundary);
14
+ return LIFECYCLE.has(command ?? "")
15
+ && !prefix.some(arg => arg === "--json" || arg === "--help" || arg === "-h")
16
+ && prefix[1] !== "help";
17
+ }
18
+
19
+ export interface VersionSkewNoticeIo {
20
+ readRuntime?: typeof readRuntimePort;
21
+ readConfig?: typeof readConfigDiagnostics;
22
+ probe?: typeof proxyIdentityAt;
23
+ cliVersion?: () => string;
24
+ now?: () => number;
25
+ warn?: (message: string) => void;
26
+ }
27
+
28
+ /**
29
+ * One proxyIdentityAt (src/server/proxy-liveness.ts) call validates /healthz identity,
30
+ * expected PID and terminal-safe version within the total diagnostic budget.
31
+ */
32
+ export async function maybeNoticeVersionSkew(
33
+ command: string | undefined, args: string[], io: VersionSkewNoticeIo = {},
34
+ ): Promise<void> {
35
+ if (printed || !shouldNoticeVersionSkew(command, args)) return;
36
+ const now = io.now ?? Date.now;
37
+ const deadlineAt = now() + NOTICE_DEADLINE_MS;
38
+ let timer: ReturnType<typeof setTimeout> | undefined;
39
+ try {
40
+ const runtime = (io.readRuntime ?? readRuntimePort)();
41
+ let port: number;
42
+ let hostname: string | undefined;
43
+ let expectedPid: number | undefined;
44
+ if (runtime) {
45
+ ({ port, hostname } = runtime);
46
+ expectedPid = runtime.pid;
47
+ } else {
48
+ const diagnostics = (io.readConfig ?? readConfigDiagnostics)();
49
+ if (diagnostics.error) return;
50
+ port = diagnostics.config.port ?? 10100;
51
+ hostname = diagnostics.config.hostname;
52
+ }
53
+ const remaining = Math.floor(deadlineAt - now());
54
+ if (remaining <= 0) return;
55
+ const cliVersion = (io.cliVersion ?? packageVersion)();
56
+ if (now() >= deadlineAt) return;
57
+ const identity = await Promise.race([
58
+ (io.probe ?? proxyIdentityAt)(port, { hostname, expectedPid }, {
59
+ timeoutMs: Math.max(1, Math.floor(deadlineAt - now())),
60
+ attempts: 1, deadlineAt, nowFn: now,
61
+ }),
62
+ new Promise<null>(resolve => {
63
+ timer = setTimeout(() => resolve(null), Math.max(1, deadlineAt - now()));
64
+ }),
65
+ ]);
66
+ if (printed || !identity || now() >= deadlineAt) return;
67
+ const skew = computeVersionSkew(cliVersion, identity.version);
68
+ if (!skew.skewed) return;
69
+ printed = true;
70
+ (io.warn ?? console.error)(`ocx ${cliVersion} does not match the running proxy ${identity.version}. `
71
+ + "Check which opencodex installation you meant to use (`ocx status` shows both).");
72
+ } catch {
73
+ // Best-effort diagnostics must not fail a lifecycle command or change its exit code.
74
+ } finally {
75
+ if (timer !== undefined) clearTimeout(timer);
76
+ }
77
+ }
@@ -2,6 +2,7 @@
2
2
  import { getModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
3
3
  import { SCHEMA_REQUIRED_OUTPUT_BUDGET } from "./constants";
4
4
  import { expandFastExportModels } from "./fast-models";
5
+ import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
5
6
  import type { OpencodeCatalogModel, ExportModel, ExportClientId, ManagedContribution } from "./contracts";
6
7
  import type { OcxConfig } from "../../types";
7
8
  import { shouldInjectApiAuthHeader } from "../../codex/inject";
@@ -84,6 +85,21 @@ export function inputModalitiesForClient(
84
85
  return kept.length > 0 ? kept : null;
85
86
  }
86
87
 
88
+ /**
89
+ * Effort ladder a ZCode surface can offer as picker values, or undefined when the row
90
+ * has nothing selectable.
91
+ *
92
+ * Both ZCode exports — the legacy `v2/config.json` block's `reasoning.variants` and the
93
+ * `v2/provider_config.json` store's `optionSpecs.reasoningLevel.values` — carry the same
94
+ * Codex vocabulary: the client validates the chosen level against the emitted list and
95
+ * forwards it verbatim, and `/v1/responses` reads that vocabulary natively. `none` is a
96
+ * Codex omit-sentinel, not a picker option, so it never survives into the values.
97
+ */
98
+ export function zcodeSelectableEfforts(efforts: readonly string[] | undefined): string[] | undefined {
99
+ const selectable = sanitizeCodexReasoningEfforts(efforts)?.filter(effort => effort !== "none");
100
+ return selectable && selectable.length > 0 ? selectable : undefined;
101
+ }
102
+
87
103
  /**
88
104
  * Input modalities opencode's model schema accepts (opencode.ai/config.json, both
89
105
  * `modalities.input` and `modalities.output`). Wider than our internal `text | image | audio`
@@ -1,6 +1,6 @@
1
1
  // ZCode personal provider store export.
2
2
  import type { ExportContext, ManagedContribution, ManagedFragment } from "./contracts";
3
- import { authoritativeContextWindow, inputModalitiesForClient, normalizeExportModels } from "./model-metadata";
3
+ import { authoritativeContextWindow, inputModalitiesForClient, normalizeExportModels, zcodeSelectableEfforts } from "./model-metadata";
4
4
  import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
5
5
  import { formatSelectorConjunction } from "../../integrations/merge";
6
6
 
@@ -49,10 +49,30 @@ export interface ZcodeStoreProviderRule {
49
49
  };
50
50
  }
51
51
 
52
+ /**
53
+ * One model rule, as the client's own store loader accepts it.
54
+ *
55
+ * The capability fields are the client's schema, not ours: ZCode 3.14's CLI
56
+ * bundle parses each `providerModelRules` entry with a strict object whose
57
+ * `config.properties` admits `inputFormat.{supportsText..supportsPdf}` (booleans)
58
+ * and whose `config.optionSpecs` admits `reasoningLevel.values` (non-empty,
59
+ * deduplicated) plus an optional compiled `map`. With no `map`, the chosen level
60
+ * is validated against `values` and forwarded verbatim to the wire field the
61
+ * `openai-responses` api type uses — `reasoning.effort`, which is exactly the
62
+ * Codex vocabulary our ladder already carries, so no mapping is emitted.
63
+ */
52
64
  export interface ZcodeStoreModelRule {
53
65
  providerId: string;
54
66
  modelId: string;
55
- config: { properties: { contextWindow: number } };
67
+ config: {
68
+ properties: {
69
+ contextWindow?: number;
70
+ inputFormat?: { supportsImage: true };
71
+ };
72
+ optionSpecs?: {
73
+ reasoningLevel: { values: string[] };
74
+ };
75
+ };
56
76
  }
57
77
 
58
78
  /**
@@ -77,7 +97,7 @@ export function zcodeStoreSchemaEstablished(parsed: unknown): boolean {
77
97
  * carrying only the model would match another provider's rule for the same
78
98
  * model. The conjunction grammar reserves `,` and `]`, and a model id holding
79
99
  * either cannot be addressed unambiguously — that row ships without its
80
- * context window rather than with a selector that points somewhere else.
100
+ * model rule rather than with a selector that points somewhere else.
81
101
  */
82
102
  function modelRuleSelector(modelId: string): string | null {
83
103
  return formatSelectorConjunction([
@@ -128,12 +148,18 @@ export function buildZcodeStoreProviderRule(ctx: ExportContext): ZcodeStoreProvi
128
148
 
129
149
  /**
130
150
  * Everything this project owns inside the store: one provider rule, plus one
131
- * model rule per row that has an authoritative context window.
151
+ * model rule per row that has anything authoritative to assert.
132
152
  *
133
- * A row without one ships no model rule at all, which is the same
134
- * authoritative-window rule the legacy block follows — an emitted stand-in
135
- * would be a guessed capability. Reasoning ladders have no counterpart here
136
- * that this project has observed the client produce, so none is written.
153
+ * A model rule now exists when the row has any of: an authoritative context
154
+ * window, image input, or a selectable effort ladder. A row with none of the
155
+ * three ships no rule at all, which is the same no-guessed-capability rule the
156
+ * legacy block follows. Image input is asserted only when the catalog declares
157
+ * it — `supportsImage: true` is what gates the client's attachment UI, and a
158
+ * text-only row keeps the client's own defaults rather than being pinned to
159
+ * `false`. `reasoningLevel.values` carries the same ladder (minus the `none`
160
+ * sentinel) the legacy block's `reasoning.variants` carries, so both surfaces
161
+ * offer the picker the catalog declared instead of the store silently losing
162
+ * reasoning and vision the way #5348 lost the provider itself.
137
163
  */
138
164
  export function buildZcodeStoreContribution(ctx: ExportContext): ManagedContribution {
139
165
  const fragments: ManagedFragment[] = [{
@@ -141,15 +167,24 @@ export function buildZcodeStoreContribution(ctx: ExportContext): ManagedContribu
141
167
  value: buildZcodeStoreProviderRule(ctx),
142
168
  }];
143
169
  for (const model of normalizeExportModels(ctx.models)) {
144
- if (inputModalitiesForClient("pi", model.inputModalities) === null) continue;
170
+ const input = inputModalitiesForClient("pi", model.inputModalities);
171
+ if (input === null) continue;
145
172
  const contextWindow = authoritativeContextWindow(model.contextWindow);
146
- if (contextWindow === undefined) continue;
173
+ const supportsImage = input.includes("image");
174
+ const efforts = zcodeSelectableEfforts(model.reasoningEfforts);
175
+ if (contextWindow === undefined && !supportsImage && efforts === undefined) continue;
147
176
  const selector = modelRuleSelector(model.namespaced);
148
177
  if (selector === null) continue;
149
178
  const rule: ZcodeStoreModelRule = {
150
179
  providerId: OPENCODE_PROVIDER_ID,
151
180
  modelId: model.namespaced,
152
- config: { properties: { contextWindow } },
181
+ config: {
182
+ properties: {
183
+ ...(contextWindow !== undefined ? { contextWindow } : {}),
184
+ ...(supportsImage ? { inputFormat: { supportsImage: true } } : {}),
185
+ },
186
+ ...(efforts !== undefined ? { optionSpecs: { reasoningLevel: { values: efforts } } } : {}),
187
+ },
153
188
  };
154
189
  fragments.push({ path: [...ZCODE_STORE_MODEL_RULES_PATH, selector], value: rule });
155
190
  }
@@ -1,7 +1,6 @@
1
1
  // ZCode config export.
2
2
  import type { ExportContext, ManagedContribution } from "./contracts";
3
- import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment } from "./model-metadata";
4
- import { sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
3
+ import { normalizeExportModels, inputModalitiesForClient, exportModelLabel, authoritativeContextWindow, singleFragment, zcodeSelectableEfforts } from "./model-metadata";
5
4
  import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
6
5
 
7
6
 
@@ -86,9 +85,8 @@ export function buildZcodeClientConfig(ctx: ExportContext): ZcodeGeneratedConfig
86
85
  // `ultra` when present: ZCode forwards the selected variant to the wire field its
87
86
  // kind uses — `reasoning.effort` on `openai`, which is what `/v1/responses` reads
88
87
  // natively. Set `defaultVariant` only when it survives that filter.
89
- const efforts = sanitizeCodexReasoningEfforts(model.reasoningEfforts)
90
- ?.filter(effort => effort !== "none");
91
- if (efforts && efforts.length > 0) {
88
+ const efforts = zcodeSelectableEfforts(model.reasoningEfforts);
89
+ if (efforts !== undefined) {
92
90
  const defaultVariant = model.defaultReasoningEffort?.trim().toLowerCase();
93
91
  entry.reasoning = {
94
92
  enabled: true,
@@ -1367,7 +1367,7 @@ function buildAsideContribution(ctx: ExportContext): ManagedContribution {
1367
1367
  * the keyed `providers`, the `models` ARRAY whose identity is `id`, the
1368
1368
  * `openai-completions` dialect, the loopback placeholder, and the
1369
1369
  * `thinkingLevelMap` levels. Evidence:
1370
- * `devlog/_plan/260912_omo_client_integration/001_omo_contract.md`.
1370
+ * `devlog/_fin/260912_omo_client_integration/001_omo_contract.md`.
1371
1371
  *
1372
1372
  * The flag is passed HERE as well as in the spec's `build`, which is the one
1373
1373
  * thing Prime and Aside do not do. They pass the default on both paths, so they
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Detection of Codex-based omo (LazyCodex), the variant that reads Codex role files and
3
- * `codex.agents.<role>.model` in `~/.omo/omo.jsonc`.
3
+ * `"[codex]".agents.<role>.model` in `~/.omo/omo.jsonc`.
4
4
  *
5
5
  * "omo" names three products. Pi-based omo (senpi) owns `~/.omo/agent` and is the omo file
6
6
  * integration in `src/integrations/registry.ts`; OpenCode-based omo (oh-my-opencode) keeps its
@@ -1,5 +1,7 @@
1
1
  /**
2
- * omo (Codex / LazyCodex)'s per-role model setting, `codex.agents.<role>.model` in `~/.omo/omo.jsonc`.
2
+ * omo (Codex / LazyCodex)'s per-role model setting, `"[codex]".agents.<role>.model` in `~/.omo/omo.jsonc`.
3
+ * The section name carries its brackets: LazyCodex reads `[codex]`, and its strict loader drops a
4
+ * bare `codex` key as unknown, so a pick written there never reaches a role.
3
5
  *
4
6
  * LazyCodex 5.1.1 and later reads it, and callers reach this only after `detectLazyCodex`
5
7
  * says LazyCodex is installed. The file is omo's, so this writes only on an explicit
@@ -30,6 +32,17 @@ export function omoJsoncPath(env: NodeJS.ProcessEnv = process.env, home: string
30
32
 
31
33
  type JsonObject = Record<string, unknown>;
32
34
 
35
+ const CODEX_SECTION = "[codex]";
36
+
37
+ /** The reasoning levels LazyCodex accepts for a role; it names Codex's `none` `off`. */
38
+ const LAZYCODEX_REASONING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
39
+
40
+ /** The `reasoning` value LazyCodex reads for a Codex effort, or null when it has no such level. */
41
+ export function omoReasoningFor(effort: string): string | null {
42
+ const level = effort === "none" ? "off" : effort;
43
+ return LAZYCODEX_REASONING.has(level) ? level : null;
44
+ }
45
+
33
46
  function isObject(value: unknown): value is JsonObject {
34
47
  return typeof value === "object" && value !== null && !Array.isArray(value);
35
48
  }
@@ -71,7 +84,7 @@ function load(path: string): Loaded {
71
84
  }
72
85
 
73
86
  function agentsOf(doc: JsonObject): JsonObject | null | undefined {
74
- const codex = doc.codex;
87
+ const codex = doc[CODEX_SECTION];
75
88
  if (codex === undefined) return undefined;
76
89
  if (!isObject(codex)) return null;
77
90
  const agents = codex.agents;
@@ -104,21 +117,33 @@ function serialize(text: string, doc: JsonObject): string {
104
117
  return text.startsWith("\ufeff") ? `\ufeff${out}` : out;
105
118
  }
106
119
 
107
- export function writeOmoRoleModel(role: string, model: string, path: string = omoJsoncPath()): OmoRoleModelWriteStatus {
120
+ /**
121
+ * `reasoning` left undefined keeps the entry's level; null removes it, so a level LazyCodex cannot
122
+ * express does not leave a stale one behind for its next install to apply.
123
+ */
124
+ export function writeOmoRoleModel(
125
+ role: string,
126
+ model: string,
127
+ path: string = omoJsoncPath(),
128
+ reasoning?: string | null,
129
+ ): OmoRoleModelWriteStatus {
108
130
  const loaded = load(path);
109
131
  if (loaded.kind !== "document") return loaded.kind === "comments" ? "skipped_comments" : loaded.kind;
110
132
  const { doc, text } = loaded;
111
133
  // Only an absent key is created; an explicit null is a value the reader calls invalid.
112
- const codex = doc.codex === undefined ? {} : doc.codex;
134
+ const codex = doc[CODEX_SECTION] === undefined ? {} : doc[CODEX_SECTION];
113
135
  if (!isObject(codex)) return "invalid";
114
136
  const agents = codex.agents === undefined ? {} : codex.agents;
115
137
  if (!isObject(agents)) return "invalid";
116
138
  const entry = agents[role] === undefined ? {} : agents[role];
117
139
  if (!isObject(entry)) return "invalid";
118
- if (entry.model === model) return "unchanged";
119
- agents[role] = { ...entry, model };
140
+ const next: JsonObject = { ...entry, model };
141
+ if (reasoning === null) delete next.reasoning;
142
+ else if (reasoning !== undefined) next.reasoning = reasoning;
143
+ if (entry.model === model && entry.reasoning === next.reasoning) return "unchanged";
144
+ agents[role] = next;
120
145
  codex.agents = agents;
121
- doc.codex = codex;
146
+ doc[CODEX_SECTION] = codex;
122
147
  assertIntegrationWriteOwnership(path);
123
148
  atomicWriteFileNoFollowUnclaimed(path, serialize(text, doc));
124
149
  return "written";