@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
@@ -6,6 +6,7 @@ import { warnIfCodexCatalogRefreshPending } from "./account-catalog-refresh";
6
6
  import { isCodexResetCreditOperationId } from "../codex/reset-credit-recovery";
7
7
  import { BROWSER_LAUNCH_FAILED_NOTICE } from "../lib/browser-launch-notice";
8
8
  import {
9
+ RuntimeApiError,
9
10
  CliUsageError,
10
11
  printData,
11
12
  readSecretLine,
@@ -465,18 +466,25 @@ async function grokResetCoupons(argv: string[], deps: RuntimeApiDeps): Promise<v
465
466
  }
466
467
  rejectArgs(args, USAGE);
467
468
  const accountId = rawId ? (rawId === "main" ? "__main__" : rawId) : undefined;
468
- const result = consume
469
- ? await runtimeRequest("/api/grok/reset-coupons/consume", {
470
- method: "POST",
471
- // Spread, not `operationId: undefined`: the server distinguishes an absent
472
- // key from a caller who asked for a stable idempotency identity.
473
- body: JSON.stringify({ accountId, tokenId, ...(operationId === undefined ? {} : { operationId }) }),
474
- }, deps)
475
- : await runtimeRequest(
476
- `/api/grok/reset-coupons${accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""}`,
477
- {},
478
- deps,
479
- );
469
+ const effectiveOperationId = consume ? operationId ?? crypto.randomUUID() : undefined;
470
+ let result: unknown;
471
+ try {
472
+ result = consume
473
+ ? await runtimeRequest("/api/grok/reset-coupons/consume", {
474
+ method: "POST", body: JSON.stringify({ accountId, tokenId, operationId: effectiveOperationId }),
475
+ }, deps)
476
+ : await runtimeRequest(`/api/grok/reset-coupons${accountId ? `?accountId=${encodeURIComponent(accountId)}` : ""}`, {}, deps);
477
+ } catch (error) {
478
+ const apiError = error instanceof RuntimeApiError ? error : undefined;
479
+ const body = apiError?.body as { error?: { code?: string } } | null | undefined;
480
+ const code = body && typeof body === "object" ? body.error?.code : undefined;
481
+ if (consume && (!apiError || (!apiError.code && (
482
+ (!code && apiError.status >= 500) || ["attempt_unresolved", "attempt_in_progress", "attempt_reconcile_failed", "operation_state_changed", "redeem_failed"].includes(code ?? "")
483
+ )))) {
484
+ throw new RuntimeApiError(`Coupon redemption outcome is unconfirmed. Preserve --operation-id ${effectiveOperationId}; reuse it to inspect this attempt and do not create a replacement operation.`, apiError?.status ?? 503, apiError?.body ?? null);
485
+ }
486
+ throw error;
487
+ }
480
488
  printData(result, wantsJson);
481
489
  }
482
490
 
package/src/cli/agent.ts CHANGED
@@ -30,7 +30,7 @@ const USAGE = `Usage:
30
30
  ocx agent subagents <status|set|clear> [model,model...] [--json]
31
31
  ocx agent subagents force <model|-> [--json]
32
32
  ocx agent fallback <status|set|clear> [model,model...] [--poll-ms <5000-600000>] [--json]
33
- ocx agent roles [status|set <role> <model>|suggest [--model <id>] [--apply]] [--json]
33
+ ocx agent roles [status|set <role> <model> [--effort <level>]|suggest [--model <id>] [--apply]] [--json]
34
34
  ocx agent sidecar <status|web|vision> [--list] [--model <id|->]
35
35
  [--backend web:<openai|anthropic|xai|gemini|exa|-> vision:<openai|anthropic|routed|->]
36
36
  [--reasoning <level>] [--max-descriptions <n> (vision)] [--enabled <on|off>]
@@ -311,7 +311,7 @@ async function sidecar(argv: string[], deps: RuntimeApiDeps): Promise<void> {
311
311
  interface CodexAgentRolesStatus {
312
312
  lazycodex?: { detected?: boolean };
313
313
  omoJsonc?: { state?: string } | null;
314
- roles?: Array<{ role: string; model: string | null; omoJsoncModel: string | null }>;
314
+ roles?: Array<{ role: string; model: string | null; effort?: string | null; omoJsoncModel: string | null }>;
315
315
  }
316
316
 
317
317
  interface CodexRoleProposal {
@@ -393,7 +393,7 @@ async function roles(argv: string[], deps: RuntimeApiDeps): Promise<void> {
393
393
  const rows = result.roles ?? [];
394
394
  printData(result, wantsJson, [
395
395
  "omo (Codex / LazyCodex): detected",
396
- ...(rows.length === 0 ? ["No Codex agent roles found."] : rows.map(row => `${row.role}: ${row.model ?? "(no model pin)"}`)),
396
+ ...(rows.length === 0 ? ["No Codex agent roles found."] : rows.map(row => `${row.role}: ${row.model ?? "(no model pin)"}${row.effort ? ` (${row.effort})` : ""}`)),
397
397
  `omo.jsonc: ${result.omoJsonc?.state ?? "unknown"}`,
398
398
  ]);
399
399
  return;
@@ -403,18 +403,22 @@ async function roles(argv: string[], deps: RuntimeApiDeps): Promise<void> {
403
403
  return;
404
404
  }
405
405
  if (action !== "set") throw new CliUsageError(`unknown roles action ${action}`, USAGE);
406
+ const effort = takeOption(args, "--effort");
407
+ // An empty value is a supplied flag, not an absent one: dropping it would change the model and
408
+ // silently ignore the effort the operator asked for.
409
+ if (effort !== undefined && effort.trim() === "") throw new CliUsageError("--effort needs a reasoning level", USAGE);
406
410
  const role = args.shift();
407
411
  const model = args.shift();
408
412
  if (!role || !model) throw new CliUsageError("a role and a model are required", USAGE);
409
413
  rejectArgs(args, USAGE);
410
414
  const result = await runtimeRequest<{ toml?: { status?: string }; omoJsonc?: { status?: string } }>(
411
415
  `/api/codex-agent-roles/${encodeURIComponent(role)}`,
412
- { method: "PUT", body: JSON.stringify({ model }) },
416
+ { method: "PUT", body: JSON.stringify(effort !== undefined ? { model, effort } : { model }) },
413
417
  deps,
414
418
  );
415
419
  const omo = result.omoJsonc?.status;
416
420
  printData(result, wantsJson, [
417
- `${role}: ${model} (role TOML ${result.toml?.status ?? "unknown"})`,
421
+ `${role}: ${model}${effort ? ` (${effort})` : ""} (role TOML ${result.toml?.status ?? "unknown"})`,
418
422
  omo === "skipped_comments"
419
423
  ? "omo.jsonc: not written, because it contains comments that a rewrite would lose."
420
424
  : `omo.jsonc: ${omo ?? "unknown"}`,
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Recovery guidance for Aside profiles whose sync preference is off while their managed block is stale (#6757).
3
+ *
4
+ * Sync refreshes only profiles whose preference is on, and an OFF preference is the operator's decision, so an
5
+ * "off (stale)" profile needs an explicit, reviewed enable. This module renders text only: it issues no request,
6
+ * reads no state and never changes a preference. Sync cannot name real profile IDs without a status read that may
7
+ * run integration maintenance, so its empty result points at status, which already holds the rows.
8
+ */
9
+ export const ASIDE_SYNC_EMPTY_LINES: readonly string[] = [
10
+ "No eligible Aside profiles to synchronize. Sync refreshes only profiles whose sync preference is on.",
11
+ "List profiles with: ocx integration client status --client aside",
12
+ "For a profile shown as off (stale), review: ocx integration client preview --client aside --operation apply --profile <N>",
13
+ "Then, if the preview permits the change and you accept it: ocx integration client enable --client aside --profile <N>",
14
+ ];
15
+
16
+ function recoveryProfileId(row: unknown): number | null {
17
+ if (!row || typeof row !== "object") return null;
18
+ const { profileId, enabled, state } = row as Record<string, unknown>;
19
+ // Only an explicit OFF beside a stale block qualifies. Conflict and unsafe blocks need inspection, not an enable.
20
+ if (enabled !== false || state !== "stale") return null;
21
+ return typeof profileId === "number" && Number.isSafeInteger(profileId) && profileId >= 0 && !Object.is(profileId, -0)
22
+ ? profileId : null;
23
+ }
24
+
25
+ /** Per-profile preview-then-enable commands for status rows that are off with a stale managed block. */
26
+ export function asideProfileRecoveryLines(rows: readonly unknown[]): string[] {
27
+ const ids = [...new Set(rows.map(recoveryProfileId).filter((id): id is number => id !== null))];
28
+ return ids.flatMap(id => [
29
+ `Aside profile ${id} is off (stale). To reconnect it, review: ocx integration client preview --client aside --operation apply --profile ${id}`,
30
+ `Then, if the preview permits the change and you accept it: ocx integration client enable --client aside --profile ${id}`,
31
+ ]);
32
+ }
33
+
@@ -571,11 +571,11 @@ export const CAPABILITIES: readonly Capability[] = [
571
571
  command: ["claude","config"],
572
572
  summary: "Read or update Claude Code settings, including independent CLI first-party routing.",
573
573
  routes: [{"method":"GET","path":"/api/claude-code"},{"method":"PUT","path":"/api/claude-code"}],
574
- flags: [{"name":"--enabled","value":"string","summary":"on or off."},{"name":"--auth-mode","value":"string","summary":"auto, proxy or subscription."},{"name":"--system-env","value":"string","summary":"on or off."},{"name":"--fast-mode","value":"string","summary":"on or off."},{"name":"--auto-context","value":"string","summary":"on or off."},{"name":"--compact-window","value":"string","summary":"Positive token count or default to clear."},{"name":"--inject-agents","value":"string","summary":"on or off."},{"name":"--small-fast-model","value":"string","summary":"Model ID; - clears."},{"name":"--model-map","value":"string","summary":"Comma-separated from=to mappings; - clears."},{"name":"--blocked-skills","value":"string","summary":"Comma-separated names; - clears."},{"name":"--web-model","value":"string","summary":"Model ID; - clears."},{"name":"--web-backend","value":"string","summary":"openai, anthropic, xai, gemini, exa or -."},{"name":"--vision-model","value":"string","summary":"Model ID; - clears."},{"name":"--vision-backend","value":"string","summary":"openai, anthropic or -."},{"name":"--first-party","value":"string","summary":"on or off; must be the only setting."},{"name":"--json","value":"boolean","summary":"Emit the result as JSON."}],
574
+ flags: [{"name":"--enabled","value":"string","summary":"on or off."},{"name":"--auth-mode","value":"string","summary":"auto, proxy or subscription."},{"name":"--system-env","value":"string","summary":"on or off."},{"name":"--fast-mode","value":"string","summary":"on or off."},{"name":"--auto-context","value":"string","summary":"on or off."},{"name":"--compact-window","value":"string","summary":"Positive token count or default to clear."},{"name":"--context-accounting","value":"string","summary":"1m (default) or 200k."},{"name":"--inject-agents","value":"string","summary":"on or off."},{"name":"--small-fast-model","value":"string","summary":"Model ID; - clears."},{"name":"--model-map","value":"string","summary":"Comma-separated from=to mappings; - clears."},{"name":"--blocked-skills","value":"string","summary":"Comma-separated names; - clears."},{"name":"--web-model","value":"string","summary":"Model ID; - clears."},{"name":"--web-backend","value":"string","summary":"openai, anthropic, xai, gemini, exa or -."},{"name":"--vision-model","value":"string","summary":"Model ID; - clears."},{"name":"--vision-backend","value":"string","summary":"openai, anthropic or -."},{"name":"--first-party","value":"string","summary":"on or off; must be the only setting."},{"name":"--json","value":"boolean","summary":"Emit the result as JSON."}],
575
575
  mutates: true,
576
576
  json: "payload",
577
577
  details: ["At least one setting is required. --first-party must be set alone and writes Claude settings immediately.","Clear both helper model and backend with - to restore inheritance. Preserve shared_proxy_retained warnings."],
578
- usage: "ocx claude config status [--json]; ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>] [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>] [--compact-window <tokens|default>] [--inject-agents <on|off>] [--small-fast-model <id|->] [--model-map <from=to,...|->] [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->] [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]; or ocx claude config set --first-party <on|off> [--json]",
578
+ usage: "ocx claude config status [--json]; ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>] [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>] [--compact-window <tokens|default>] [--context-accounting <1m|200k>] [--inject-agents <on|off>] [--small-fast-model <id|->] [--model-map <from=to,...|->] [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->] [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]; or ocx claude config set --first-party <on|off> [--json]",
579
579
  },
580
580
  {
581
581
  command: ["claude","intercept","start"],
@@ -714,11 +714,11 @@ export const CAPABILITIES: readonly Capability[] = [
714
714
  command: ["agent","roles"],
715
715
  summary: "omo (Codex / LazyCodex): show each Codex agent role's model pin, set one role's model in its TOML and in omo.jsonc, or suggest a model for every role.",
716
716
  routes: [{"method":"GET","path":"/api/codex-agent-roles"},{"method":"PUT","path":"/api/codex-agent-roles/{role}"},{"method":"POST","path":"/api/codex-agent-roles/auto-assign"}],
717
- flags: [{"name":"--json","value":"boolean","summary":"Emit the role list, the write result or the proposals as JSON."},{"name":"--model","value":"string","summary":"suggest: size the roles with this model instead of the Codex default model."},{"name":"--apply","value":"boolean","summary":"suggest: write every proposal through the role model write."}],
717
+ flags: [{"name":"--json","value":"boolean","summary":"Emit the role list, the write result or the proposals as JSON."},{"name":"--effort","value":"string","summary":"set: also write the role's reasoning effort."},{"name":"--model","value":"string","summary":"suggest: size the roles with this model instead of the Codex default model."},{"name":"--apply","value":"boolean","summary":"suggest: write every proposal through the role model write."}],
718
718
  mutates: true,
719
719
  json: "payload",
720
- details: ["A bare invocation reads and never writes.","Requires Codex-based omo (LazyCodex): the omo@sisyphuslabs Codex plugin enabled in config.toml and installed; otherwise status lists no roles, and set and suggest are refused.","set rewrites only the root model value of $CODEX_HOME/agents/<role>.toml; omo.jsonc is skipped when absent or when it contains comments.","suggest sizes every role with one model call and prints proposals without writing; --apply writes each proposed model, and its effort when the role file already sets model_reasoning_effort."],
721
- usage: "ocx agent roles [status] [--json]; ocx agent roles set <role> <model> [--json]; ocx agent roles suggest [--model <id>] [--apply] [--json]",
720
+ details: ["A bare invocation reads and never writes.","Requires Codex-based omo (LazyCodex): the omo@sisyphuslabs Codex plugin enabled in config.toml and installed; otherwise status lists no roles, and set and suggest are refused.","set rewrites only the root model value of $CODEX_HOME/agents/<role>.toml, plus model_reasoning_effort with --effort; omo.jsonc gets the same model and, when LazyCodex has that level, the effort as reasoning; it is skipped when absent or when it contains comments.","suggest sizes every role with one model call and prints proposals without writing; --apply writes each proposed model, and its effort when the role file already sets model_reasoning_effort."],
721
+ usage: "ocx agent roles [status] [--json]; ocx agent roles set <role> <model> [--effort <level>] [--json]; ocx agent roles suggest [--model <id>] [--apply] [--json]",
722
722
  },
723
723
  {
724
724
  command: ["agent","injection"],
@@ -139,10 +139,10 @@ export const INTEGRATION_CAPABILITIES: readonly Capability[] = [
139
139
  },
140
140
  {
141
141
  command: ["claude", "config", "set"],
142
- usage: "ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>] [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>] [--compact-window <tokens|default>] [--inject-agents <on|off>] [--small-fast-model <id|->] [--model-map <from=to,...|->] [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->] [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]; or ocx claude config set --first-party <on|off> [--json]",
142
+ usage: "ocx claude config set [--enabled <on|off>] [--auth-mode <auto|proxy|subscription>] [--system-env <on|off>] [--fast-mode <on|off>] [--auto-context <on|off>] [--compact-window <tokens|default>] [--context-accounting <1m|200k>] [--inject-agents <on|off>] [--small-fast-model <id|->] [--model-map <from=to,...|->] [--blocked-skills <name,name|->] [--web-model <id|->] [--web-backend <openai|anthropic|xai|gemini|exa|->] [--vision-model <id|->] [--vision-backend <openai|anthropic|->] [--json]; or ocx claude config set --first-party <on|off> [--json]",
143
143
  summary: "Change Claude Code settings through the runtime owner.",
144
144
  routes: [{"method": "PUT", "path": "/api/claude-code"}],
145
- flags: [{"name": "--enabled", "value": "string", "summary": "on or off."}, {"name": "--auth-mode", "value": "string", "summary": "auto, proxy or subscription."}, {"name": "--system-env", "value": "string", "summary": "on or off."}, {"name": "--fast-mode", "value": "string", "summary": "on or off."}, {"name": "--auto-context", "value": "string", "summary": "on or off."}, {"name": "--compact-window", "value": "string", "summary": "Positive token count or default to clear."}, {"name": "--inject-agents", "value": "string", "summary": "on or off."}, {"name": "--small-fast-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--model-map", "value": "string", "summary": "Comma-separated from=to mappings; - clears."}, {"name": "--blocked-skills", "value": "string", "summary": "Comma-separated names; - clears."}, {"name": "--web-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--web-backend", "value": "string", "summary": "openai, anthropic, xai, gemini, exa or -."}, {"name": "--vision-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--vision-backend", "value": "string", "summary": "openai, anthropic or -."}, {"name": "--first-party", "value": "string", "summary": "on or off; must be the only setting."}, {"name": "--json", "value": "boolean", "summary": "Emit the result as JSON."}],
145
+ flags: [{"name": "--enabled", "value": "string", "summary": "on or off."}, {"name": "--auth-mode", "value": "string", "summary": "auto, proxy or subscription."}, {"name": "--system-env", "value": "string", "summary": "on or off."}, {"name": "--fast-mode", "value": "string", "summary": "on or off."}, {"name": "--auto-context", "value": "string", "summary": "on or off."}, {"name": "--compact-window", "value": "string", "summary": "Positive token count or default to clear."}, {"name": "--context-accounting", "value": "string", "summary": "1m (default) or 200k."}, {"name": "--inject-agents", "value": "string", "summary": "on or off."}, {"name": "--small-fast-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--model-map", "value": "string", "summary": "Comma-separated from=to mappings; - clears."}, {"name": "--blocked-skills", "value": "string", "summary": "Comma-separated names; - clears."}, {"name": "--web-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--web-backend", "value": "string", "summary": "openai, anthropic, xai, gemini, exa or -."}, {"name": "--vision-model", "value": "string", "summary": "Model ID; - clears."}, {"name": "--vision-backend", "value": "string", "summary": "openai, anthropic or -."}, {"name": "--first-party", "value": "string", "summary": "on or off; must be the only setting."}, {"name": "--json", "value": "boolean", "summary": "Emit the result as JSON."}],
146
146
  mutates: true,
147
147
  json: "payload",
148
148
  details: ["At least one setting is required. --first-party must be set alone and writes Claude settings immediately.", "Clear both helper model and backend with - to restore inheritance. Preserve shared_proxy_retained warnings."],
@@ -0,0 +1,132 @@
1
+ import { accessSync, constants, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { posix, win32 } from "node:path";
4
+ import { readDesktopCliRecord, type DesktopCliRecordRead, desktopCliRecordPath, DESKTOP_CLI_RECORD_MAX_BYTES } from "../lib/desktop-cli-record.mjs";
5
+ import { redactUserPath } from "../lib/redact";
6
+
7
+ export type CliCommandIssue = Extract<DesktopCliRecordRead, { issue: string }>["issue"]
8
+ | "cleanup-pending"
9
+ | "desktop-target-missing" | "desktop-target-unusable"
10
+ | "expected-command-missing" | "expected-command-unusable"
11
+ | "path-missing" | "path-first-not-desktop" | "path-unreadable"
12
+ | "path-scan-truncated" | "windows-current-directory-shadow";
13
+ export type CliPathCandidate = {
14
+ path: string; realpath: string; pathIndex: number; extension: string;
15
+ };
16
+ export type CliPathDiagnostics = {
17
+ configured: boolean;
18
+ recordPath: string;
19
+ packageHandoff: "enabled" | "disabled-on-windows";
20
+ recordState: DesktopCliRecordRead["state"];
21
+ expectedExecutable: string | null;
22
+ handoffTarget: string | null;
23
+ candidates: CliPathCandidate[];
24
+ pathFirst: CliPathCandidate | null;
25
+ desktopFirstOnPath: boolean | null;
26
+ currentDirectoryCandidate: CliPathCandidate | null;
27
+ shellResolution: "unobserved";
28
+ issues: CliCommandIssue[];
29
+ };
30
+ export type CliPathObservation =
31
+ | { kind: "usable"; realpath: string }
32
+ | { kind: "missing" | "unusable" | "unreadable" };
33
+ export type CliPathDiagnosticOptions = {
34
+ platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; cwd?: string; home?: string;
35
+ recordRead?: DesktopCliRecordRead;
36
+ observe?: (path: string) => CliPathObservation;
37
+ };
38
+
39
+ export function collectCliPathDiagnostics(options: CliPathDiagnosticOptions = {}): CliPathDiagnostics {
40
+ const platform = options.platform ?? process.platform;
41
+ const env = options.env ?? process.env;
42
+ const api = platform === "win32" ? win32 : posix;
43
+ const cwd = options.cwd ?? process.cwd();
44
+ const home = options.home ?? (platform === "win32" ? env.USERPROFILE || homedir() : homedir());
45
+ const read = options.recordRead ?? readDesktopCliRecord({ platform, env, home, recordPath: desktopCliRecordPath({ platform, env, home }) });
46
+ const configured = read.state === "ready";
47
+ const target = configured && read.state === "ready" ? read.record.cliExecutable : null;
48
+ const expected = configured ? (platform === "win32" ? target : api.join(home, ".opencodex-desktop", "bin", "ocx")) : null;
49
+ const issues: CliCommandIssue[] = [];
50
+ const add = (issue: CliCommandIssue): void => { if (!issues.includes(issue)) issues.push(issue); };
51
+ if (read.state === "invalid" || read.state === "unreadable") add(read.issue);
52
+ if (read.state === "disabled" && read.cleanupPending) add("cleanup-pending");
53
+ const observe = options.observe ?? ((path: string): CliPathObservation => {
54
+ try {
55
+ if (!statSync(path).isFile()) return { kind: "unusable" };
56
+ accessSync(path, platform === "win32" ? constants.F_OK : constants.X_OK);
57
+ return { kind: "usable", realpath: realpathSync(path) };
58
+ } catch (error) {
59
+ const code = (error as NodeJS.ErrnoException).code;
60
+ return { kind: code === "ENOENT" || code === "ENOTDIR" ? "missing" : code === "EACCES" ? "unusable" : "unreadable" };
61
+ }
62
+ });
63
+ const key = (path: string): string => platform === "win32" ? path.toLowerCase() : path;
64
+ const same = (a: string, b: string): boolean => key(a) === key(b);
65
+ const envValue = (name: string): string | undefined => platform === "win32"
66
+ ? env[Object.keys(env).find(key => key.toLowerCase() === name.toLowerCase()) ?? name] : env[name];
67
+ const path = envValue("PATH");
68
+ const rawExtensions = platform === "win32"
69
+ ? (envValue("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(ext => /^\.[A-Za-z0-9]+$/.test(ext)) : [];
70
+ const uniqueExtensions = [...new Set(rawExtensions.map(ext => ext.toLowerCase()))];
71
+ const extensions = platform === "win32" ? [...uniqueExtensions.slice(0, 32), ""] : [""];
72
+ if (uniqueExtensions.length > 32) add("path-scan-truncated");
73
+ const directories = path === undefined ? [] : path.split(platform === "win32" ? ";" : ":");
74
+ if (directories.length > 256) add("path-scan-truncated");
75
+ const candidates: CliPathCandidate[] = [];
76
+ const scan = (directory: string, pathIndex: number): CliPathCandidate[] => {
77
+ const result: CliPathCandidate[] = [];
78
+ const unquoted = platform === "win32" && directory.startsWith('"') && directory.endsWith('"') ? directory.slice(1, -1) : directory;
79
+ for (const extension of extensions) {
80
+ const candidate = api.resolve(cwd, unquoted || ".", `ocx${extension}`);
81
+ const found = observe(candidate);
82
+ if (found.kind === "unreadable") add("path-unreadable");
83
+ if (found.kind === "usable") result.push({ path: candidate, realpath: found.realpath, pathIndex, extension });
84
+ }
85
+ return result;
86
+ };
87
+ for (const [index, directory] of directories.slice(0, 256).entries()) candidates.push(...scan(directory, index));
88
+ const pathFirst = candidates[0] ?? null;
89
+ const currentDirectoryCandidate = platform === "win32" ? scan(cwd, -1)[0] ?? null : null;
90
+ let expectedRealpath: string | null = null;
91
+ if (target) {
92
+ const found = observe(target);
93
+ if (found.kind === "missing") add("desktop-target-missing");
94
+ else if (found.kind !== "usable") add("desktop-target-unusable");
95
+ }
96
+ if (expected) {
97
+ const found = observe(expected);
98
+ if (found.kind === "missing") add("expected-command-missing");
99
+ else if (found.kind !== "usable") add("expected-command-unusable");
100
+ else expectedRealpath = found.realpath;
101
+ if (!pathFirst) add("path-missing");
102
+ else if (!same(pathFirst.path, expected) && (!expectedRealpath || !same(pathFirst.realpath, expectedRealpath))) add("path-first-not-desktop");
103
+ if (currentDirectoryCandidate && !same(currentDirectoryCandidate.path, expected)
104
+ && (!expectedRealpath || !same(currentDirectoryCandidate.realpath, expectedRealpath))) add("windows-current-directory-shadow");
105
+ }
106
+ return {
107
+ configured, recordPath: read.path, packageHandoff: platform === "win32" ? "disabled-on-windows" : "enabled",
108
+ recordState: read.state, expectedExecutable: expected, handoffTarget: platform === "win32" ? null : target,
109
+ candidates, pathFirst,
110
+ desktopFirstOnPath: configured && !issues.includes("path-unreadable") && !issues.includes("path-scan-truncated")
111
+ ? Boolean(pathFirst && expected && expectedRealpath
112
+ && (same(pathFirst.path, expected) || same(pathFirst.realpath, expectedRealpath))) : null,
113
+ currentDirectoryCandidate, shellResolution: "unobserved", issues,
114
+ };
115
+ }
116
+
117
+ export function formatCliCommandLine(value: CliPathDiagnostics): string {
118
+ const first = value.pathFirst ? redactUserPath(value.pathFirst.path).replace(/[\x00-\x1f\x7f]/g, "?") : "none observed";
119
+ return `ocx command: PATH first=${first}; Desktop=${!value.configured ? "not configured" : value.desktopFirstOnPath === null ? "unobserved" : value.desktopFirstOnPath ? "first" : "not first"}; ${value.packageHandoff === "disabled-on-windows" ? "package handoff=disabled on Windows (user Path selects Desktop); " : ""}shell=unobserved${value.issues.length ? `; issues=${value.issues.join(",")}` : ""}`;
120
+ }
121
+
122
+ export function formatCliStatusHealthLabel(health: string, value: CliPathDiagnostics, local: boolean): string {
123
+ return `${health}${local ? " (local)" : ""}\n ${formatCliCommandLine(value)}`;
124
+ }
125
+
126
+ export function cliCommandDoctorChecks(value: CliPathDiagnostics): { level: "OK" | "WARN" | "FAIL"; message: string }[] {
127
+ const hard = value.issues.some(issue => issue.startsWith("record-") || issue.startsWith("desktop-target-") || issue.startsWith("expected-command-"));
128
+ const recovery = value.issues.some(issue => issue.startsWith("record-"))
129
+ ? ` The terminal-command record at ${redactUserPath(value.recordPath).replace(/[\x00-\x1f\x7f]/g, "?")} could not be used. Open OpenCodex Desktop to repair the terminal command.${value.packageHandoff === "disabled-on-windows" ? "" : " Or run with OCX_NO_DESKTOP_HANDOFF=1."}${value.issues.includes("record-too-large") ? ` The record limit is ${DESKTOP_CLI_RECORD_MAX_BYTES} bytes.` : ""}`
130
+ : value.issues.includes("cleanup-pending") ? " Open OpenCodex Desktop to finish terminal-command cleanup." : "";
131
+ return [{ level: hard ? "FAIL" : value.issues.length ? "WARN" : "OK", message: formatCliCommandLine(value) + recovery }];
132
+ }
@@ -0,0 +1,65 @@
1
+ import { createSupervisionLatch, inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
2
+ import type { DesktopSupervision, SupervisionInspector } from "../service/desktop-command-guard";
3
+ import { UpdateRestartRequired } from "./update-restart-candidate";
4
+ import { runProxyRestart, type ProxyRestartIo } from "./tray-proxy";
5
+
6
+ function evidenceForTarget(evidence: DesktopSupervision, pid: number | null | undefined): DesktopSupervision {
7
+ // Injected inspectors must obey the same target binding as the production inspector.
8
+ return evidence.kind === "desktop" && pid != null && evidence.runtimePid !== pid
9
+ ? { kind: "unknown", reason: "unrelated-target", desktopSeen: false } : evidence;
10
+ }
11
+
12
+ export function duplicateRuntimeMessage(
13
+ pid: number | null | undefined,
14
+ port: number,
15
+ inspect: SupervisionInspector = inspectDesktopSupervision,
16
+ ): string {
17
+ const latch = createSupervisionLatch();
18
+ const evidence = evidenceForTarget(inspect({ targetPid: pid ?? undefined }), pid);
19
+ if (pid != null && latch.observe(evidence)) {
20
+ const supervisor = evidence.kind === "desktop" ? `, ${evidence.app}` : "";
21
+ return `OpenCodex Desktop supervises the running proxy (pid ${pid}${supervisor}, port ${port}). `
22
+ + "Use the running proxy, or quit OpenCodex before starting it from this CLI.";
23
+ }
24
+ return `⚠️ Proxy already running (PID ${pid ?? "unknown"}, port ${port}). Use 'ocx stop' first.`;
25
+ }
26
+
27
+ export function desktopStopNotice(evidence: DesktopSupervision): string | null {
28
+ const latch = createSupervisionLatch();
29
+ return latch.observe(evidence)
30
+ ? "OpenCodex Desktop may start this proxy again after a short backoff. Use Stop Proxy or Quit in the OpenCodex menu to keep it stopped."
31
+ : null;
32
+ }
33
+
34
+ export function runDesktopAwareProxyRestart(
35
+ io: ProxyRestartIo,
36
+ inspect: SupervisionInspector = inspectDesktopSupervision,
37
+ info: (line: string) => void = console.log,
38
+ ) {
39
+ const latch = createSupervisionLatch();
40
+ let targetPid: number | undefined;
41
+ let supervisedRequest = false;
42
+ return runProxyRestart({
43
+ ...io,
44
+ requestInPlaceRestart: async previous => {
45
+ targetPid = previous.pid ?? undefined;
46
+ const evidence = evidenceForTarget(inspect({ targetPid }), targetPid);
47
+ supervisedRequest = latch.observe(evidence);
48
+ const result = await io.requestInPlaceRestart(previous);
49
+ if (evidence.kind === "desktop" && result.accepted) {
50
+ info("Restart requested; OpenCodex Desktop starts the replacement.");
51
+ }
52
+ if (supervisedRequest && !result.accepted && !result.uncertain && result.error instanceof UpdateRestartRequired) {
53
+ info("OpenCodex Desktop supervises this proxy. Use the app's updater (tray → Check for Updates), or quit OpenCodex before updating and restarting from this CLI.");
54
+ return { accepted: false as const, uncertain: false, error: new Error("restart_desktop_update_required") };
55
+ }
56
+ return result;
57
+ },
58
+ startWhenStopped: async recovering => {
59
+ const blocked = latch.observe(evidenceForTarget(inspect({ targetPid }), targetPid));
60
+ // An accepted supervised handoff keeps its replacement intent through temporary absence.
61
+ if (supervisedRequest || blocked) return { status: "skipped" as const };
62
+ return io.startWhenStopped(recovering);
63
+ },
64
+ });
65
+ }
@@ -37,6 +37,8 @@ import { parseStopApproval, type StopApproval } from "./stop-approval";
37
37
  import type { ResolveArgs } from "./resolve";
38
38
  import type { ClientConnectionState } from "../client/state";
39
39
  import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
40
+ import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
41
+ import { desktopStopNotice } from "./desktop-runtime-guidance";
40
42
 
41
43
  export interface CliDispatchDeps {
42
44
  args: string[];
@@ -51,6 +53,7 @@ export interface CliDispatchDeps {
51
53
  spawnDetached: (argv: readonly string[]) => void;
52
54
  handleStart: () => Promise<void>;
53
55
  handleStop: (approval?: StopApproval) => Promise<StopOutcome>;
56
+ inspectDesktopSupervision?: typeof inspectDesktopSupervision;
54
57
  handleEnsure: (options?: { existingIsSuccess?: boolean }) => Promise<boolean>;
55
58
  handleResolve: (args: ResolveArgs) => Promise<number>;
56
59
  handleTrayProxyStart: (existingIsSuccess?: boolean) => Promise<boolean>;
@@ -112,6 +115,8 @@ const commandRunners: Record<string, CommandRunner> = {
112
115
  // re-start the proxy immediately, so warning there would contradict the next line.
113
116
  const warning = "⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').";
114
117
  if (!parsed.json) {
118
+ const notice = desktopStopNotice((deps.inspectDesktopSupervision ?? inspectDesktopSupervision)());
119
+ if (notice) console.error(notice);
115
120
  // handleStop returns the structured outcome now; an object is always truthy, so
116
121
  // the warning must key on .ok — otherwise a failed stop would still claim downtime.
117
122
  if ((await deps.handleStop()).ok) console.log(warning);
package/src/cli/doctor.ts CHANGED
@@ -7,11 +7,13 @@
7
7
  * it never sets proxy env, relocates state dirs, mutates quota, or changes
8
8
  * networking. See devlog/_plan/260630_wsl-account-autoswitch/30_*.
9
9
  */
10
+ import { cliCommandDoctorChecks, collectCliPathDiagnostics } from "./cli-path-diagnostics";
10
11
  import { accessSync, constants, existsSync, readFileSync } from "node:fs";
11
12
  import { homedir } from "node:os";
12
13
  import { dirname, join } from "node:path";
13
14
  import { getConfigDir, getConfigPath, readConfigDiagnostics } from "../config";
14
15
  import { readPid } from "../config/process-state";
16
+ import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
15
17
  import { fetchLiveStartupHealth, probeUncleanExitState, selectStatusStartupHealth } from "./status";
16
18
  import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
17
19
  import { directLocalHttpFetch } from "../server/direct-local-http";
@@ -1324,6 +1326,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1324
1326
  console.log(` ${row.exists ? "ok " : "-- "} ${row.label}: ${row.path}${flags ? ` (${flags})` : ""}`);
1325
1327
  }
1326
1328
 
1329
+ console.log("\nocx command selection");
1330
+ for (const check of cliCommandDoctorChecks(collectCliPathDiagnostics())) {
1331
+ console.log(` ${check.level === "OK" ? "ok " : "!! "} ${check.message}`);
1332
+ if (check.level === "FAIL") recordDoctorFailure();
1333
+ }
1334
+
1327
1335
  // Runs without the proxy on purpose: the worst accumulation happens when the proxy will
1328
1336
  // not start, which is exactly when the in-process periodic reclaim never ticks.
1329
1337
  const reclaimTemps = args.includes(RECLAIM_RESPONSE_TEMPS_FLAG);
@@ -1379,7 +1387,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1379
1387
  configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
1380
1388
  });
1381
1389
  const liveStartup = live ? await fetchLiveStartupHealth(live) : null;
1382
- const startup = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(doctorConfig));
1390
+ const { startup } = selectStatusStartupHealth(liveStartup, () => collectStartupHealth(doctorConfig),
1391
+ live?.pid != null ? () => inspectDesktopSupervision({ targetPid: live.pid! }) : undefined, live?.pid);
1383
1392
  console.log("\nCodex restart safety");
1384
1393
  console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
1385
1394
  console.log(` ${formatStartupRoutingDetail(startup)}`);
@@ -1685,8 +1694,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1685
1694
  const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
1686
1695
  const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
1687
1696
  if (!startup.rebootSafe) {
1688
- const command = startup.recommendedCommand ?? startup.commands.restoreNative;
1689
- hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${command}'.`);
1697
+ if (startup.recommendedAction || startup.desktop?.supervisor) {
1698
+ hints.push(`Codex is pinned to the local proxy without persistent startup protection. ${startup.recommendedAction ?? "Reopen OpenCodex and check Start at Login."}`);
1699
+ } else {
1700
+ const command = startup.recommendedCommand ?? startup.commands.restoreNative;
1701
+ hints.push(`Codex is pinned to the local proxy without persistent startup protection. After restart, requests can reconnect indefinitely. Run '${command}'.`);
1702
+ }
1690
1703
  }
1691
1704
  if (anyDrvfs) {
1692
1705
  hints.push("State dir is on a Windows-mounted (/mnt) drive. Prefer the Linux home (~) under WSL for token/lock reliability.");
package/src/cli/index.ts CHANGED
@@ -8,6 +8,8 @@ import { homedir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { currentServingCommand, deferServiceChildToNewerRuntime, markDelegatedServiceReady, recordServingRuntime } from "../config/serving-runtimes";
10
10
  import { packageVersion } from "../lib/package-version";
11
+ import { codexHomeIsAbsent } from "../codex/codex-home-owner";
12
+ import { getCodexHome } from "../codex/paths";
11
13
  import { admitUpdateRestartChild } from "./update-restart-child";
12
14
  import { UpdateRestartRequired } from "./update-restart-candidate";
13
15
  import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
@@ -75,7 +77,7 @@ import {
75
77
  pendingTeardownsAreExactly,
76
78
  quarantinePendingTeardown,
77
79
  } from "../config/pending-teardown";
78
- import { collectStatus, deadProxyRoutingAdviceLines, detectMissingCodexCatalogPath, hubStatusLines, missingCodexCatalogLines, remoteHubBannerLine, remoteHubStatusLines, unusedProxyWarningLines } from "./status";
80
+ import { collectStatus, deadProxyRoutingAdviceLines, detectMissingCodexCatalogPath, hubStatusLines, missingCodexCatalogLines, remoteHubBannerLine, remoteHubStatusLines, runtimeSupervisorLine, unusedProxyWarningLines } from "./status";
79
81
  import { endpointsToProve, everyEndpointProvenDownAsync, sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
80
82
  import { takeFlag } from "./runtime-api";
81
83
  import { parseStartOptions, StartArgsError } from "./start-args";
@@ -85,11 +87,11 @@ import {
85
87
  recheckRestartFailedStart, reobserveRestartReplacement,
86
88
  restartStartOutcome,
87
89
  waitForProxyReplacement,
88
- runProxyRestart,
89
90
  runTrayProxyStart,
90
91
  type ProxyRestartLive,
91
92
  type ProxyRestartStartOutcome,
92
93
  } from "./tray-proxy";
94
+ import { duplicateRuntimeMessage, runDesktopAwareProxyRestart } from "./desktop-runtime-guidance";
93
95
  import { reportRestartFailure } from "./restart-failure";
94
96
  import { requestBoundSystemRestart } from "./system-restart-client";
95
97
  import { installCrashGuards } from "../lib/crash-guard";
@@ -329,7 +331,7 @@ async function chooseListenPort(
329
331
  throw new StartCommandExit(serviceStayOutExitCode());
330
332
  }
331
333
  if (decision === "refuse-live-proxy") {
332
- console.error(`⚠️ Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}). Use 'ocx stop' first.`);
334
+ console.error(duplicateRuntimeMessage(holder?.pid, preferred));
333
335
  throw new StartCommandExit(1);
334
336
  }
335
337
  if (decision === "refuse-unidentified-holder") {
@@ -444,7 +446,7 @@ async function handleStart(options: { block?: boolean } = {}) {
444
446
  process.exit(serviceStayOutExitCode());
445
447
  }
446
448
  if (decision === "refuse" || decision === "await-parent") {
447
- console.error(`⚠️ Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}). Use 'ocx stop' first.`);
449
+ console.error(duplicateRuntimeMessage(owner.live.pid ?? owner.pidSnapshot, owner.live.port));
448
450
  process.exit(1);
449
451
  }
450
452
  // Sibling path. The new instance takes over this home's ocx.pid / runtime-port.json while
@@ -654,10 +656,12 @@ async function handleStart(options: { block?: boolean } = {}) {
654
656
  removeRuntimePort(process.pid);
655
657
  if (teardown.restoreNativeCodex && !currentExternalCodexModelProvider()) {
656
658
  try {
657
- const restored = restoreNativeCodex();
658
- if (!restored.success) {
659
- cleanupSucceeded = false;
660
- console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
659
+ if (!codexHomeIsAbsent(getCodexHome())) {
660
+ const restored = restoreNativeCodex();
661
+ if (!restored.success) {
662
+ cleanupSucceeded = false;
663
+ console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
664
+ }
661
665
  }
662
666
  } catch (error) {
663
667
  cleanupSucceeded = false;
@@ -928,7 +932,7 @@ async function handleProxyRestart(
928
932
  startWhenStopped: (recoveringLiveRestart: boolean) => Promise<ProxyRestartStartOutcome>,
929
933
  ): Promise<boolean> {
930
934
  const deadlineAt = Date.now() + PROXY_RESTART_OBSERVE_MS;
931
- const result = await runProxyRestart({
935
+ const result = await runDesktopAwareProxyRestart({
932
936
  findLive: () => discoverStableProxyForRestart({
933
937
  findLive: () => findLiveProxy({ deadlineAt, attempts: 2, acceptPackageTreeFenced: true }),
934
938
  expired: () => Date.now() >= deadlineAt,
@@ -1785,6 +1789,7 @@ async function handleStatus() {
1785
1789
  console.log(` Config: ${status.json.paths.config}${local}`);
1786
1790
  console.log(` PID file: ${status.json.paths.pid}${local}`);
1787
1791
  console.log(` Runtime: ${status.json.paths.runtime}${local}`);
1792
+ if (runtimeSupervisorLine(status.json.startup)) console.log(` ${runtimeSupervisorLine(status.json.startup)}`);
1788
1793
  console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}${local}`);
1789
1794
  // On a client this is the local default, which routing does not use — the hub applies its own.
1790
1795
  console.log(` Default provider: ${status.json.defaultProvider}${local}`);
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { redactUserPath } from "../lib/redact";
3
3
  import type { RefusalReason } from "../integrations/mutation-plan";
4
+ import { ASIDE_SYNC_EMPTY_LINES } from "./aside-profile-recovery";
4
5
  import { refreshAsideProfilesThroughServer } from "./aside-profiles";
5
6
  import { runCatalogAction } from "./catalog-command-result";
6
7
  import { CliUsageError, RuntimeApiError, printData, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
@@ -56,7 +57,7 @@ export function handleIntegrationAsideSync(argv: string[], deps: AsideSyncCliDep
56
57
  const parsed = resultsSchema.safeParse(outcomes);
57
58
  if (!parsed.success) throw new Error("Invalid Aside synchronization outcome");
58
59
  const results = parsed.data;
59
- const lines = results.length === 0 ? ["No eligible Aside profiles to synchronize. Check integration status and profile sync preferences."]
60
+ const lines = results.length === 0 ? [...ASIDE_SYNC_EMPTY_LINES]
60
61
  : results.flatMap(row => [
61
62
  `Aside profile ${row.profileId}: ${row.ok ? row.changed ? "updated" : "unchanged" : "failed"}.`,
62
63
  ...(row.reason ? [row.reason] : []),