@bitkyc08/opencodex 2.84.0 → 2.86.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 (246) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
  5. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  6. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/package.json +1 -2
  9. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  10. package/src/adapters/anthropic/client-identity.ts +1 -1
  11. package/src/adapters/anthropic/passthrough.ts +33 -4
  12. package/src/adapters/azure.ts +19 -1
  13. package/src/adapters/client-fingerprint.ts +24 -0
  14. package/src/adapters/coding-agent/turn.ts +22 -9
  15. package/src/adapters/cursor/arg-codec.ts +17 -5
  16. package/src/adapters/cursor/catalog.ts +8 -0
  17. package/src/adapters/cursor/effort-map.ts +3 -0
  18. package/src/adapters/cursor/freeform-args.ts +75 -0
  19. package/src/adapters/cursor/protobuf-events.ts +36 -23
  20. package/src/adapters/google-antigravity-wire.ts +22 -1
  21. package/src/adapters/google.ts +2 -2
  22. package/src/adapters/openai-chat/response-events.ts +59 -1
  23. package/src/adapters/openai-chat.ts +8 -5
  24. package/src/adapters/qoder/adapter.ts +40 -1
  25. package/src/advisor/consult.ts +220 -0
  26. package/src/advisor/context.ts +226 -0
  27. package/src/advisor/disclosure.ts +20 -0
  28. package/src/advisor/runtime.ts +305 -0
  29. package/src/advisor/settings.ts +135 -0
  30. package/src/advisor/state.ts +369 -0
  31. package/src/advisor/synthetic-tool.ts +33 -0
  32. package/src/bridge/errors.ts +5 -0
  33. package/src/chat/inbound.ts +30 -6
  34. package/src/chat/outbound.ts +5 -0
  35. package/src/claude/desktop-3p-library.ts +43 -3
  36. package/src/claude/desktop-3p.ts +22 -8
  37. package/src/claude/desktop-picker-profile.ts +11 -4
  38. package/src/claude/desktop-profile.ts +10 -1
  39. package/src/claude/desktop-remote-store-io.ts +5 -3
  40. package/src/claude/inbound.ts +121 -50
  41. package/src/claude/intercept/local-ca.ts +8 -1
  42. package/src/claude/model-default-settings.ts +154 -0
  43. package/src/claude/model-info.ts +7 -7
  44. package/src/claude/outbound.ts +13 -6
  45. package/src/cli/advisor.ts +151 -0
  46. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  47. package/src/cli/capabilities-agents-routing.ts +18 -0
  48. package/src/cli/capabilities-base.ts +1 -1
  49. package/src/cli/chatgpt-command.ts +35 -14
  50. package/src/cli/claude.ts +46 -6
  51. package/src/cli/dispatch.ts +5 -1
  52. package/src/cli/doctor.ts +9 -0
  53. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  54. package/src/cli/export-command.ts +1 -1
  55. package/src/cli/help.ts +2 -1
  56. package/src/cli/index.ts +13 -4
  57. package/src/cli/integration-plan-dto.ts +2 -0
  58. package/src/cli/registry.ts +15 -3
  59. package/src/cli/restart-scope.ts +3 -1
  60. package/src/clients/config-export/contracts.ts +1 -0
  61. package/src/clients/config-export/model-metadata.ts +3 -2
  62. package/src/clients/config-export/qoder.ts +93 -0
  63. package/src/clients/config-export.ts +17 -4
  64. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  65. package/src/codex/auth-context.ts +79 -5
  66. package/src/codex/autostart-health.ts +4 -0
  67. package/src/codex/catalog/build-entries.ts +11 -2
  68. package/src/codex/catalog/configured-tiers.ts +50 -0
  69. package/src/codex/catalog/effort.ts +4 -1
  70. package/src/codex/catalog/model-hints.ts +21 -3
  71. package/src/codex/catalog/native-ultrafast.ts +106 -0
  72. package/src/codex/catalog/parsing.ts +5 -0
  73. package/src/codex/catalog/provider-models.ts +4 -0
  74. package/src/codex/catalog/retained-sync.ts +2 -0
  75. package/src/codex/catalog/routed-gather.ts +22 -4
  76. package/src/codex/catalog/vendor-context.ts +82 -0
  77. package/src/codex/config-write-lock.ts +40 -2
  78. package/src/codex/convergence.ts +2 -0
  79. package/src/codex/desktop-app/types.ts +2 -0
  80. package/src/codex/desktop-app/windows.ts +29 -16
  81. package/src/codex/desktop-app-restart.ts +4 -2
  82. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  83. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  84. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  85. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  86. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  87. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  88. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  89. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  90. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  91. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  92. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  93. package/src/codex/desktop-compatibility/service.ts +30 -0
  94. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  95. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  96. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  97. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  98. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  99. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  100. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  101. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  102. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  103. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  104. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  105. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  106. package/src/codex/history-provider.ts +1 -1
  107. package/src/codex/inject/config-write-section.ts +4 -1
  108. package/src/codex/inject/remove.ts +18 -6
  109. package/src/codex/journal.ts +16 -1
  110. package/src/codex/main-account.ts +99 -15
  111. package/src/codex/native-main-refresh-mode.ts +217 -0
  112. package/src/codex/routing/health-store.ts +1 -1
  113. package/src/codex/routing/pool-recovery.ts +79 -0
  114. package/src/codex/shim-diagnostics.ts +6 -0
  115. package/src/codex/shim-overlay.ts +7 -2
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/live-reconcile.ts +13 -0
  118. package/src/config/load-degrade.ts +6 -1
  119. package/src/config/paths.ts +10 -3
  120. package/src/config/schema/config-schema.ts +17 -0
  121. package/src/config/schema/desktop-compatibility.ts +9 -0
  122. package/src/config/schema/leaf-validators.ts +1 -0
  123. package/src/generated/compatibility-version.json +451 -163
  124. package/src/generated/model-metadata.ts +1 -1
  125. package/src/integrations/catalog-refresh.ts +1 -1
  126. package/src/integrations/config-io.ts +20 -2
  127. package/src/integrations/journal.ts +15 -1
  128. package/src/integrations/mutation-plan.ts +1 -0
  129. package/src/integrations/registry.ts +7 -0
  130. package/src/integrations/store.ts +3 -0
  131. package/src/integrations/writer.ts +10 -1
  132. package/src/lib/advisor-activation.ts +16 -0
  133. package/src/lib/background-work.ts +65 -0
  134. package/src/lib/bounded-file-read.ts +58 -0
  135. package/src/lib/bun-path-runtime.d.mts +10 -1
  136. package/src/lib/bun-path-runtime.mjs +62 -3
  137. package/src/lib/desktop-proxy-route.ts +30 -0
  138. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  139. package/src/lib/errors.ts +7 -0
  140. package/src/lib/local-internal-call-capability.ts +55 -0
  141. package/src/lib/package-tree-integrity.ts +2 -2
  142. package/src/lib/socks5-fetch.ts +48 -71
  143. package/src/lib/socks5-handshake.ts +112 -0
  144. package/src/lib/translator-budget.ts +7 -1
  145. package/src/oauth/generic-account-failover.ts +29 -5
  146. package/src/oauth/index.ts +4 -2
  147. package/src/oauth/kiro-device-login.ts +2 -1
  148. package/src/oauth/kiro.ts +7 -1
  149. package/src/protocols/codecs/chat-ir.ts +467 -0
  150. package/src/protocols/codecs/chat.ts +10 -1
  151. package/src/protocols/codecs/messages-ir.ts +500 -0
  152. package/src/protocols/features.ts +10 -2
  153. package/src/protocols/path.ts +11 -1
  154. package/src/protocols/plan-snapshot.ts +39 -16
  155. package/src/protocols/plan.ts +9 -1
  156. package/src/protocols/settings.ts +8 -0
  157. package/src/providers/antigravity-account-roster.ts +102 -0
  158. package/src/providers/antigravity-models.ts +85 -7
  159. package/src/providers/model-rename-fields.ts +2 -0
  160. package/src/providers/model-rename-migration.ts +5 -3
  161. package/src/providers/registry/entries-core.ts +7 -6
  162. package/src/providers/registry/entries-extended.ts +19 -0
  163. package/src/responses/parser.ts +30 -19
  164. package/src/responses/state.ts +32 -29
  165. package/src/routing/history/build-file.ts +92 -0
  166. package/src/routing/history/indexer.ts +248 -216
  167. package/src/routing/history/ingest.ts +238 -0
  168. package/src/routing/history/rebuild-job.ts +111 -0
  169. package/src/routing/history/rebuild-protocol.ts +15 -0
  170. package/src/routing/history/rebuild-worker.ts +34 -0
  171. package/src/server/admission-model-scope.ts +4 -1
  172. package/src/server/antigravity-direct-eligibility.ts +208 -0
  173. package/src/server/auth-cors.ts +4 -0
  174. package/src/server/chat-completions.ts +75 -9
  175. package/src/server/claude-messages.ts +110 -20
  176. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  177. package/src/server/index/serve-options.ts +2 -2
  178. package/src/server/index/startup-warnings.ts +9 -0
  179. package/src/server/index/websocket-handler.ts +9 -0
  180. package/src/server/index.ts +13 -10
  181. package/src/server/inference/client-encoder-delivery.ts +6 -5
  182. package/src/server/login-shell-path.ts +146 -0
  183. package/src/server/management/advisor-routes.ts +191 -0
  184. package/src/server/management/agent-settings-routes.ts +22 -4
  185. package/src/server/management/companion-routes.ts +4 -0
  186. package/src/server/management/config-routes.ts +2 -2
  187. package/src/server/management/context.ts +4 -0
  188. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  189. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  190. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  191. package/src/server/management/model-routes.ts +2 -2
  192. package/src/server/management/protocol-settings-patch.ts +1 -0
  193. package/src/server/management/route-registry.ts +11 -0
  194. package/src/server/management/sibling-guard.ts +1 -0
  195. package/src/server/management-api.ts +6 -0
  196. package/src/server/messages-antigravity-direct.ts +195 -0
  197. package/src/server/messages-native.ts +5 -1
  198. package/src/server/request-log.ts +7 -0
  199. package/src/server/responses/adapter-continuation.ts +35 -18
  200. package/src/server/responses/adapter-delivery.ts +24 -2
  201. package/src/server/responses/adapter-dispatch.ts +29 -2
  202. package/src/server/responses/advisor-plan-slot.ts +27 -0
  203. package/src/server/responses/advisor-slot.ts +336 -0
  204. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  205. package/src/server/responses/core-combo-failure.ts +10 -1
  206. package/src/server/responses/core-combo.ts +21 -0
  207. package/src/server/responses/core-lifetime.ts +2 -0
  208. package/src/server/responses/core-normalize.ts +11 -0
  209. package/src/server/responses/core-options.ts +46 -0
  210. package/src/server/responses/core-replay.ts +7 -4
  211. package/src/server/responses/core.ts +1 -1
  212. package/src/server/responses/passthrough-delivery.ts +16 -10
  213. package/src/server/responses/passthrough-dispatch.ts +14 -3
  214. package/src/server/responses/request-prepare.ts +68 -5
  215. package/src/server/responses/request-transport.ts +16 -0
  216. package/src/server/responses/run-turn-execution.ts +70 -8
  217. package/src/server/responses/sidecar-execution.ts +78 -3
  218. package/src/server/responses/terminal-guard.ts +2 -1
  219. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  220. package/src/server/startup-action-control.ts +16 -2
  221. package/src/server/startup-health-cache.ts +1 -0
  222. package/src/service/desktop-startup.ts +14 -0
  223. package/src/service/health-sentinel-main.ts +23 -4
  224. package/src/service/health-sentinel-probe.ts +24 -7
  225. package/src/service/logging.ts +72 -0
  226. package/src/tray/windows-tray.ps1 +43 -24
  227. package/src/types/config.ts +63 -6
  228. package/src/types/provider.ts +7 -0
  229. package/src/types/request.ts +15 -0
  230. package/src/types/tools.ts +2 -0
  231. package/src/update/async-check.ts +1 -1
  232. package/src/update/badge.ts +2 -2
  233. package/src/update/index.ts +88 -15
  234. package/src/update/job-supervised-restart.ts +107 -0
  235. package/src/update/job.ts +32 -32
  236. package/src/update/notify.ts +1 -1
  237. package/src/update/refresh-scheduler.ts +2 -2
  238. package/src/update/supervised-recovery.d.mts +27 -0
  239. package/src/update/supervised-recovery.mjs +157 -0
  240. package/src/usage/cache-diagnostic.ts +2 -0
  241. package/src/usage/expected-prices.ts +12 -0
  242. package/src/web-search/gemini-executor.ts +2 -2
  243. package/assets/sponsors/packycode.png +0 -0
  244. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  245. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  246. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
package/src/cli/claude.ts CHANGED
@@ -32,7 +32,8 @@ import { DEFAULT_CATALOG_PATH } from "../codex/paths";
32
32
  import { readFileSync } from "node:fs";
33
33
  import { join } from "node:path";
34
34
  import { aliasForNative, aliasForRoute, legacyAliasForNative, legacyAliasForRoute } from "../claude/alias";
35
- import { desktop3pAlias } from "../claude/desktop-3p";
35
+ import { desktop3pAlias, isRealDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
36
+ import { desktopProfileWireAlias, legacyDesktopProfileWireAlias } from "../claude/desktop-profile";
36
37
  import { inspectDesktopFirstParty } from "../claude/desktop-first-party";
37
38
  import { isClaudeInterceptProxyUrl, type ClaudeInterceptSettingsState } from "../claude/intercept/settings";
38
39
  import { withoutSiblingMarker } from "../codex/sibling-start";
@@ -618,11 +619,47 @@ const MODEL_ENV_SLOT_NAMES = [
618
619
  "ANTHROPIC_SMALL_FAST_MODEL",
619
620
  ] as const;
620
621
 
621
- const DESKTOP_3P_ALIAS = /^claude-opus-4(?:-8)?-[a-z][0-9a-z]{2}$/;
622
+ const DESKTOP_3P_ALIAS = /^claude-(?:opus-4(?:-8)?-[a-z][0-9a-z]{2}|(?:opus-4-8|fable-5)-p[0-9a-z]{3})(?:--fast)?$/;
623
+
624
+ export function nativeClaudeModelIds(config: OcxConfig): string[] {
625
+ const assignments = config.claudeCode?.desktopProfile?.assignments ?? {};
626
+ const routes = Object.keys(assignments);
627
+ const realProfileIds = new Set(routes.filter(route => route.startsWith("anthropic/claude-"))
628
+ .map(route => route.slice("anthropic/".length)));
629
+ // A native CLI starts without the server registry. Persisted profile bindings still
630
+ // identify proxy selectors, even when an old provider declaration has the same spelling.
631
+ const proxyProfileIds = new Set(routes.filter(route => !route.startsWith("anthropic/claude-"))
632
+ .flatMap(route => {
633
+ const alias = assignments[route]!.alias;
634
+ const slash = route.indexOf("/");
635
+ return [alias, desktopProfileWireAlias(alias), legacyDesktopProfileWireAlias(alias),
636
+ desktop3pAlias(route.slice(0, slash), route.slice(slash + 1))];
637
+ }));
638
+ // The configured default is a genuine declaration too: the catalog seeds it for static
639
+ // providers, so it must be recognized here even without a server registry.
640
+ const anthropic = config.providers.anthropic;
641
+ const declaredIds = [
642
+ ...(anthropic?.defaultModel ? [anthropic.defaultModel] : []),
643
+ ...(anthropic?.models ?? []), ...(anthropic?.selectedModels ?? []), ...(anthropic?.retainModels ?? []),
644
+ ];
645
+ return [...new Set([
646
+ ...realProfileIds,
647
+ ...declaredIds
648
+ .filter(id => {
649
+ const base = id.endsWith("--fast") ? id.slice(0, -"--fast".length) : id;
650
+ return id.startsWith("claude-") && (realProfileIds.has(id)
651
+ || (!proxyProfileIds.has(id) && !proxyProfileIds.has(base)));
652
+ }),
653
+ ])];
654
+ }
622
655
 
623
- export function isProxyOnlyModelId(value: string, providerNames: readonly string[] = []): boolean {
656
+ export function isProxyOnlyModelId(value: string, providerNames: readonly string[] = [], realAnthropicIds: readonly string[] = []): boolean {
624
657
  const id = value.trim().replace(/\[1m\]$/, "");
625
658
  if (!id) return false;
659
+ if (isRealDesktop3pModelId(id)) return false;
660
+ const base = id.endsWith("--fast") ? id.slice(0, -"--fast".length) : id;
661
+ if (resolveDesktop3pAlias(id) || resolveDesktop3pAlias(base)) return true;
662
+ if (realAnthropicIds.includes(id)) return false;
626
663
  const aliasPrefixes = [CLAUDE_ALIAS_PREFIX_CURRENT, CLAUDE_ALIAS_PREFIX_CURRENT_V2, CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2];
627
664
  if (aliasPrefixes.some(prefix => id.startsWith(prefix)) || DESKTOP_3P_ALIAS.test(id)) {
628
665
  return true;
@@ -684,9 +721,10 @@ export function buildNativeClaudeEnv(
684
721
  // otherwise settings.env can replace the destination while retaining the credential.
685
722
  if (hasOwnedAdmission) delete env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST;
686
723
  const providerNames = Object.keys(config.providers);
724
+ const realAnthropicIds = nativeClaudeModelIds(config);
687
725
  for (const name of MODEL_ENV_SLOT_NAMES) {
688
726
  const value = env[name];
689
- if (!value || !isProxyOnlyModelId(value, providerNames)) continue;
727
+ if (!value || !isProxyOnlyModelId(value, providerNames, realAnthropicIds)) continue;
690
728
  delete env[name];
691
729
  // The label opencodex paired with this slot names a proxy route the native session cannot use.
692
730
  const labelName = `${name}_NAME`;
@@ -702,11 +740,12 @@ export function nativeModelOverride(
702
740
  configuredModel: string | undefined,
703
741
  args: readonly string[],
704
742
  providerNames: readonly string[] = [],
743
+ realAnthropicIds: readonly string[] = [],
705
744
  ): { flag?: string[]; warning?: string } {
706
- if (!pickedModel || !isProxyOnlyModelId(pickedModel, providerNames)) return {};
745
+ if (!pickedModel || !isProxyOnlyModelId(pickedModel, providerNames, realAnthropicIds)) return {};
707
746
  if (args.some(arg => arg === "--model" || arg.startsWith("--model="))) return {};
708
747
  const fallback = configuredModel?.trim();
709
- if (fallback && !isProxyOnlyModelId(fallback, providerNames)) {
748
+ if (fallback && !isProxyOnlyModelId(fallback, providerNames, realAnthropicIds)) {
710
749
  return {
711
750
  flag: ["--model", fallback],
712
751
  warning: `ℹ️ The saved model (${pickedModel}) requires the proxy. This native session will use ${fallback}.`,
@@ -867,6 +906,7 @@ async function launchNativeClaude(config: OcxConfig, args: string[], notice: str
867
906
  config.claudeCode?.model,
868
907
  args,
869
908
  providerNames,
909
+ nativeClaudeModelIds(config),
870
910
  );
871
911
  if (override.warning) console.error(override.warning);
872
912
  const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
@@ -484,7 +484,7 @@ const commandRunners: Record<string, CommandRunner> = {
484
484
  },
485
485
  config,
486
486
  port: live.port,
487
- }, ["mcode", "pi", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]));
487
+ }, ["mcode", "pi", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo", "qoder"]));
488
488
  } catch (error) {
489
489
  console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
490
490
  }
@@ -834,6 +834,10 @@ const commandRunners: Record<string, CommandRunner> = {
834
834
  const { handleComboCommand } = await import("./combo");
835
835
  return await handleComboCommand(deps.args.slice(1));
836
836
  },
837
+ advisor: async deps => {
838
+ const { handleAdvisorCommand } = await import("./advisor");
839
+ return await handleAdvisorCommand(deps.args.slice(1));
840
+ },
837
841
  companion: async deps => {
838
842
  const { handleCompanionCommand } = await import("./companion");
839
843
  return await handleCompanionCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
package/src/cli/doctor.ts CHANGED
@@ -1428,6 +1428,15 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1428
1428
  }
1429
1429
  }
1430
1430
 
1431
+ // #2999: which process refreshes the native main login in CODEX_HOME.
1432
+ console.log("\nNative main refresh");
1433
+ {
1434
+ const { loadConfig } = await import("../config");
1435
+ const { formatNativeMainRefreshDoctorLines, resolveNativeMainRefreshMode } = await import("../codex/native-main-refresh-mode");
1436
+ const resolution = await resolveNativeMainRefreshMode(loadConfig());
1437
+ for (const line of formatNativeMainRefreshDoctorLines(resolution)) console.log(line);
1438
+ }
1439
+
1431
1440
  // Mirrors `ocx status` through the same comparison rather than a second implementation:
1432
1441
  // two diagnostics disagreeing about whether an install is stale is worse than one (#2701).
1433
1442
  // No extra probe -- findLiveProxy already carried the version back.
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Cold start for `ocx ensure` when OpenCodex Desktop owns or runs the proxy (#6971).
3
+ *
4
+ * The Codex launcher shim runs `ocx ensure` before every Codex launch. Without this policy,
5
+ * ensure spawned a detached CLI proxy whenever nothing answered on the port. That is wrong in two
6
+ * Desktop situations: during the Desktop supervisor's restart backoff, and after Desktop quit
7
+ * while its durable ownership claim remains. In both, the CLI proxy took the port and Desktop came
8
+ * back as a guest without supervision. That is why the dashboard used to disable the shim under
9
+ * Desktop.
10
+ *
11
+ * Rules, in order:
12
+ * - A running Desktop app is the supervisor. Wait for its proxy; never start a competing one.
13
+ * - A durable Desktop claim with no running app: start Desktop (hidden, as a login launch would)
14
+ * where that is possible, then wait. Elsewhere, fail with guidance. A missing Desktop process
15
+ * never authorizes a CLI proxy by itself; handing ownership to the CLI stays an explicit
16
+ * `ocx service install` consent step.
17
+ * - Ownership or the process list that cannot be read is not evidence of absence: refuse.
18
+ * - No claim and no running app: the ordinary CLI cold start.
19
+ *
20
+ * Concurrent shim launches share one Desktop launch through a lease file held by a live process;
21
+ * every caller then waits on the same readiness probe.
22
+ */
23
+ import { execFileSync } from "node:child_process";
24
+ import { randomUUID } from "node:crypto";
25
+ import { existsSync, linkSync, readdirSync, readFileSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
26
+ import { homedir } from "node:os";
27
+ import { basename, dirname, join, win32 } from "node:path";
28
+ import { getConfigDir } from "../config/paths";
29
+ import { resolveServiceOwnership } from "../service/state";
30
+
31
+ export type DesktopProcessState = "running" | "absent" | "unknown";
32
+ export type DesktopOwnershipState = "desktop" | "other" | "unknown";
33
+
34
+ export type EnsureColdStartPlan =
35
+ | { kind: "spawn-cli" }
36
+ | { kind: "wait-desktop" }
37
+ | { kind: "launch-desktop" }
38
+ | { kind: "refuse"; reason: "desktop-unlaunchable" | "unverified" };
39
+
40
+ export function planEnsureColdStart(facts: {
41
+ ownership: DesktopOwnershipState;
42
+ desktopProcess: DesktopProcessState;
43
+ platform: NodeJS.Platform;
44
+ }): EnsureColdStartPlan {
45
+ if (facts.desktopProcess === "running") return { kind: "wait-desktop" };
46
+ if (facts.ownership === "unknown" || facts.desktopProcess === "unknown") return { kind: "refuse", reason: "unverified" };
47
+ if (facts.ownership === "other") return { kind: "spawn-cli" };
48
+ return facts.platform === "darwin" ? { kind: "launch-desktop" } : { kind: "refuse", reason: "desktop-unlaunchable" };
49
+ }
50
+
51
+ /** Desktop restart backoff tops out at 30 s; the budget covers it plus a cold sidecar start. */
52
+ export const DESKTOP_COLD_START_TIMEOUT_MS = 40_000;
53
+
54
+ export type DesktopColdStartResult<T> =
55
+ | { kind: "cli" }
56
+ | { kind: "live"; live: T }
57
+ | { kind: "failed"; message: string };
58
+
59
+ export interface DesktopColdStartDeps<T> {
60
+ platform: NodeJS.Platform;
61
+ ownership: () => DesktopOwnershipState;
62
+ desktopProcess: () => DesktopProcessState;
63
+ /** Start the Desktop app hidden. Returns false when the request could not be made. */
64
+ launchDesktop: () => boolean;
65
+ /** Returns a release function when this caller should launch, or null when another caller already did. */
66
+ acquireLaunchLease: () => (() => void) | null;
67
+ waitForProxy: (timeoutMs: number) => Promise<T | null>;
68
+ info?: (line: string) => void;
69
+ }
70
+
71
+ const QUIT_HINT = "To run the proxy from this CLI instead, quit OpenCodex and run `ocx service install` or `ocx start`.";
72
+
73
+ export async function runDesktopColdStart<T>(deps: DesktopColdStartDeps<T>): Promise<DesktopColdStartResult<T>> {
74
+ const plan = planEnsureColdStart({
75
+ ownership: deps.ownership(),
76
+ desktopProcess: deps.desktopProcess(),
77
+ platform: deps.platform,
78
+ });
79
+ if (plan.kind === "spawn-cli") return { kind: "cli" };
80
+ if (plan.kind === "refuse") {
81
+ return { kind: "failed", message: plan.reason === "unverified"
82
+ ? "Could not verify whether OpenCodex Desktop manages this proxy, so no CLI proxy was started. Run `ocx doctor`, or start the proxy explicitly with `ocx start`."
83
+ : `OpenCodex Desktop manages this proxy but is not running. Open OpenCodex to start it. ${QUIT_HINT}` };
84
+ }
85
+ if (plan.kind === "launch-desktop") {
86
+ const release = deps.acquireLaunchLease();
87
+ try {
88
+ if (release) {
89
+ deps.info?.("Starting OpenCodex Desktop, which manages this proxy...");
90
+ if (!deps.launchDesktop()) {
91
+ return { kind: "failed", message: `OpenCodex Desktop manages this proxy, but it could not be started. Open OpenCodex manually. ${QUIT_HINT}` };
92
+ }
93
+ }
94
+ const live = await deps.waitForProxy(DESKTOP_COLD_START_TIMEOUT_MS);
95
+ if (live) return { kind: "live", live };
96
+ return { kind: "failed", message: `OpenCodex Desktop was started, but its proxy did not answer in time. Check the OpenCodex menu. ${QUIT_HINT}` };
97
+ } finally { release?.(); }
98
+ }
99
+ deps.info?.("Waiting for OpenCodex Desktop to restart its proxy...");
100
+ const live = await deps.waitForProxy(DESKTOP_COLD_START_TIMEOUT_MS);
101
+ if (live) return { kind: "live", live };
102
+ return {
103
+ kind: "failed",
104
+ message: `OpenCodex Desktop is running but its proxy is not answering. Use Start Proxy in the OpenCodex menu. ${QUIT_HINT}`,
105
+ };
106
+ }
107
+
108
+ type Execute = (command: string, args: string[], timeoutMs: number) => string;
109
+
110
+ function execute(command: string, args: string[], timeoutMs: number): string {
111
+ return execFileSync(command, args, {
112
+ encoding: "utf8", timeout: timeoutMs, maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"], windowsHide: true,
113
+ });
114
+ }
115
+
116
+ /** A Desktop app executable: `opencodex-desktop`, inside an app bundle's Contents/MacOS on macOS. */
117
+ export function isDesktopExecutable(path: string, platform: NodeJS.Platform): boolean {
118
+ if (basename(path) !== "opencodex-desktop") return false;
119
+ return platform !== "darwin" || (basename(dirname(path)) === "MacOS" && basename(dirname(dirname(path))) === "Contents");
120
+ }
121
+
122
+ export interface DesktopProcessOptions {
123
+ platform?: NodeJS.Platform;
124
+ uid?: number;
125
+ env?: NodeJS.ProcessEnv;
126
+ run?: Execute;
127
+ /** Linux: executable paths of this user's processes, or \`unverified\`. */
128
+ linuxExecutables?: (uid: number) => string[] | "unverified";
129
+ /** Linux: whether procfs hides processes (hidepid=2/4). */
130
+ procfsHidesProcesses?: () => boolean;
131
+ /** Linux: whether this machine shows evidence of an OpenCodex Desktop install. */
132
+ desktopInstalled?: () => boolean;
133
+ }
134
+
135
+ /**
136
+ * Linux: hidepid=2 (invisible) or 4 (ptraceable) can hide a same-uid process whose credentials
137
+ * differ from the caller's (for example after newgrp), so an empty scan proves nothing there.
138
+ * An unreadable mount table is treated as filtering.
139
+ */
140
+ export function procfsHidesProcesses(mountinfo = "/proc/self/mountinfo"): boolean {
141
+ let text: string;
142
+ try { text = readFileSync(mountinfo, "utf8"); } catch { return true; }
143
+ return text.split("\n").some(line => {
144
+ const [mount, fs] = line.split(" - ");
145
+ if (mount?.split(" ")[4] !== "/proc" || !fs) return false;
146
+ const [type, , options = ""] = fs.split(" ");
147
+ return type === "proc" && options.split(",").some(option => /^hidepid=(2|4|invisible|ptraceable)$/.test(option));
148
+ });
149
+ }
150
+
151
+ function linuxDesktopInstalled(): boolean {
152
+ return existsSync(join(homedir(), ".opencodex-desktop")) || existsSync("/usr/bin/opencodex-desktop");
153
+ }
154
+
155
+ /** Linux truncates comm to 15 bytes. */
156
+ const LINUX_DESKTOP_COMM = "opencodex-desktop".slice(0, 15);
157
+
158
+ /**
159
+ * Linux: executables of this user's processes. A process that exited mid-scan or belongs to someone
160
+ * else is skipped. A non-dumpable process (ssh-agent, gpg-agent) hides its executable but not its
161
+ * comm, so it is ruled out by name; one whose comm could be Desktop makes the answer unverifiable.
162
+ * The scan must also see this process, or procfs is filtered and absence proves nothing.
163
+ */
164
+ export function scanLinuxExecutables(uid: number, selfPid: number = process.pid, proc = "/proc"): string[] | "unverified" {
165
+ const paths: string[] = [];
166
+ let sawSelf = false;
167
+ const code = (error: unknown) => (error as { code?: unknown } | null)?.code;
168
+ for (const entry of readdirSync(proc)) {
169
+ if (!/^\d+$/.test(entry)) continue;
170
+ if (Number(entry) === selfPid) sawSelf = true;
171
+ try {
172
+ if (statSync(`${proc}/${entry}`).uid !== uid) continue;
173
+ paths.push(readlinkSync(`${proc}/${entry}/exe`).replace(/ \(deleted\)$/, ""));
174
+ } catch (error) {
175
+ if (code(error) === "ENOENT" || code(error) === "ESRCH") continue;
176
+ try {
177
+ if (readFileSync(`${proc}/${entry}/comm`, "utf8").trim() !== LINUX_DESKTOP_COMM) continue;
178
+ } catch (commError) {
179
+ if (code(commError) === "ENOENT" || code(commError) === "ESRCH") continue;
180
+ }
181
+ return "unverified";
182
+ }
183
+ }
184
+ return sawSelf ? paths : "unverified";
185
+ }
186
+
187
+ /** Whether this user runs an OpenCodex Desktop app, judged by executable identity, never arguments. */
188
+ export function inspectDesktopProcess(options: DesktopProcessOptions = {}): DesktopProcessState {
189
+ const platform = options.platform ?? process.platform;
190
+ const run = options.run ?? execute;
191
+ try {
192
+ const uid = options.uid ?? process.getuid?.();
193
+ if (platform === "linux") {
194
+ if (uid === undefined) return "unknown";
195
+ const executables = (options.linuxExecutables ?? scanLinuxExecutables)(uid);
196
+ if (executables === "unverified") return "unknown";
197
+ if (executables.some(path => isDesktopExecutable(path, platform))) return "running";
198
+ // A filtered procfs cannot prove absence where Desktop may be installed. Without an install
199
+ // there is nothing to hide, so hardened hosts keep the plain CLI cold start.
200
+ return (options.procfsHidesProcesses ?? procfsHidesProcesses)() && (options.desktopInstalled ?? linuxDesktopInstalled)()
201
+ ? "unknown" : "absent";
202
+ }
203
+ if (platform === "darwin") {
204
+ if (uid === undefined) return "unknown";
205
+ // macOS reports the full executable path as comm.
206
+ const output = run("/bin/ps", ["-U", String(uid), "-o", "comm="], 3_000);
207
+ return output.split("\n").some(line => isDesktopExecutable(line.trim(), platform)) ? "running" : "absent";
208
+ }
209
+ if (platform === "win32") {
210
+ const env = options.env ?? process.env;
211
+ const root = env.SystemRoot ?? env.windir ?? "C:\\Windows";
212
+ const user = env.USERNAME?.trim();
213
+ const args = ["/FI", "IMAGENAME eq opencodex-desktop.exe", ...(user ? ["/FI", `USERNAME eq ${user}`] : []), "/FO", "CSV", "/NH"];
214
+ const output = run(win32.join(root, "System32", "tasklist.exe"), args, 3_000);
215
+ return /^"opencodex-desktop\.exe",/im.test(output) ? "running" : "absent";
216
+ }
217
+ return "absent";
218
+ } catch {
219
+ return "unknown";
220
+ }
221
+ }
222
+
223
+ /** Start the macOS app hidden, the same way its login item does. */
224
+ export function launchDesktopApp(options: { platform?: NodeJS.Platform; run?: Execute } = {}): boolean {
225
+ if ((options.platform ?? process.platform) !== "darwin") return false;
226
+ try {
227
+ (options.run ?? execute)("/usr/bin/open", ["-g", "-b", "com.opencodex.desktop", "--args", "--autostart"], 10_000);
228
+ return true;
229
+ } catch { return false; }
230
+ }
231
+
232
+ function processAlive(pid: number): boolean {
233
+ try { process.kill(pid, 0); return true; }
234
+ catch (error) { return (error as NodeJS.ErrnoException | null)?.code === "EPERM"; }
235
+ }
236
+
237
+ function readHolder(path: string): number | null {
238
+ const pid = Number(readFileSync(path, "utf8").trim());
239
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
240
+ }
241
+
242
+ const LEASE_FILE = /^desktop-launch\.(\d+)\.lease$/;
243
+
244
+ /**
245
+ * One Desktop launch per cold start. Leases are numbered generations, each published whole with
246
+ * link(2), which fails if the name exists, so exactly one contender can create generation N+1. A
247
+ * caller launches only after creating the next generation while the newest one belongs to a dead
248
+ * process (or none exists). A lease stays valid while its holder lives, so a slow launch never
249
+ * expires under its owner. Nothing ever moves or overwrites another holder's lease.
250
+ */
251
+ export function acquireDesktopLaunchLease(
252
+ configDir: string,
253
+ io: { pid?: number; alive?: (pid: number) => boolean } = {},
254
+ ): (() => void) | null {
255
+ const pid = io.pid ?? process.pid;
256
+ const alive = io.alive ?? processAlive;
257
+ const leasePath = (generation: number) => join(configDir, `desktop-launch.${generation}.lease`);
258
+ for (let attempt = 0; attempt < 4; attempt++) {
259
+ let generations: number[];
260
+ try {
261
+ generations = readdirSync(configDir).flatMap(name => {
262
+ const match = LEASE_FILE.exec(name);
263
+ return match ? [Number(match[1])] : [];
264
+ }).sort((a, b) => b - a);
265
+ } catch {
266
+ // An unreadable lease directory must not suppress the launch; it only loses deduplication.
267
+ return () => {};
268
+ }
269
+ const newest = generations[0];
270
+ if (newest !== undefined) {
271
+ try {
272
+ const holder = readHolder(leasePath(newest));
273
+ if (holder !== null && alive(holder)) return null;
274
+ } catch (error) {
275
+ if ((error as { code?: unknown })?.code === "ENOENT") continue;
276
+ return null;
277
+ }
278
+ }
279
+ const next = (newest ?? 0) + 1;
280
+ const staged = join(configDir, `desktop-launch.${randomUUID()}.tmp`);
281
+ try {
282
+ writeFileSync(staged, String(pid), { flag: "wx", mode: 0o600 });
283
+ linkSync(staged, leasePath(next));
284
+ } catch (error) {
285
+ if ((error as { code?: unknown })?.code === "EEXIST") continue;
286
+ return () => {};
287
+ } finally {
288
+ try { unlinkSync(staged); } catch { /* not created */ }
289
+ }
290
+ const withdraw = () => {
291
+ try { if (readHolder(leasePath(next)) === pid) unlinkSync(leasePath(next)); } catch { /* already gone */ }
292
+ };
293
+ // A contender with a stale directory read can publish a generation a winner already cleaned
294
+ // up. The highest generation is the one launcher; anyone below it stands down.
295
+ try {
296
+ if (readdirSync(configDir).some(name => Number(LEASE_FILE.exec(name)?.[1] ?? 0) > next)) {
297
+ withdraw();
298
+ return null;
299
+ }
300
+ } catch { /* keep the lease: the directory was readable a moment ago */ }
301
+ // Older generations belong to dead holders: the newest was checked before publishing above it.
302
+ for (const generation of generations) {
303
+ try { unlinkSync(leasePath(generation)); } catch { /* already gone */ }
304
+ }
305
+ // The newest generation is never deleted on release: removing it would let numbering restart
306
+ // below a stale contender. It stops counting once this process exits.
307
+ return () => {};
308
+ }
309
+ return null;
310
+ }
311
+
312
+ /** Production wiring; `waitForProxy` is ensure's identity-checked readiness probe. */
313
+ export function productionDesktopColdStartDeps<T>(waitForProxy: (timeoutMs: number) => Promise<T | null>): DesktopColdStartDeps<T> {
314
+ return {
315
+ platform: process.platform,
316
+ ownership: () => {
317
+ try {
318
+ const owner = resolveServiceOwnership();
319
+ if (owner.kind === "unknown") return "unknown";
320
+ return owner.kind === "owned" && owner.ownership.owner === "desktop" ? "desktop" : "other";
321
+ } catch { return "unknown"; }
322
+ },
323
+ desktopProcess: () => inspectDesktopProcess(),
324
+ launchDesktop: () => launchDesktopApp(),
325
+ acquireLaunchLease: () => acquireDesktopLaunchLease(getConfigDir()),
326
+ waitForProxy,
327
+ info: line => console.log(line),
328
+ };
329
+ }
@@ -169,7 +169,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
169
169
  const spec = EXPORT_CLIENTS[client];
170
170
  const root = await runtimeBaseUrl(deps);
171
171
  let built: { document: unknown; text: string };
172
- if (client === "raycast" || client === "droid") {
172
+ if (client === "raycast" || client === "droid" || client === "qoder") {
173
173
  // The dial address alone cannot distinguish a wildcard authenticated bind
174
174
  // from loopback. Let the live server resolve its admission/listener policy;
175
175
  // saved config can differ from the process serving this request.
package/src/cli/help.ts CHANGED
@@ -92,11 +92,12 @@ Usage:
92
92
  ocx api-key <sub> Alias of ocx access key
93
93
  ocx access <sub> External API keys and endpoint information
94
94
  ocx api <sub> Protocol paths: vocabulary, request-path preview, and policy
95
- ocx export --client <id> Print a client config wired to the running proxy (18 clients)
95
+ ocx export --client <id> Print a client config wired to the running proxy (19 clients)
96
96
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
97
97
  ocx grok <sub> Grok Build model selection and apply
98
98
  ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
99
99
  ocx config [sub] Validated configuration show/get/set/import/export
100
+ ocx advisor <status|on|off|set|consent> Advisor sidecar settings and context-sharing consent
100
101
  ocx companion <show|set|reset> Menu-bar and widget companion usage settings
101
102
  ocx lab <sub> Inspect Lab evidence and control local automation
102
103
  ocx chatgpt <sub> Experimental app-server shim: launch|restore|status (macOS)
package/src/cli/index.ts CHANGED
@@ -16,7 +16,6 @@ import { UpdateRestartRequired } from "./update-restart-candidate";
16
16
  import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
17
17
  import { findGuiDist } from "../server/gui-static";
18
18
  import { inspectGuiBundleFreshness, staleGuiBundleLines } from "../server/gui-freshness";
19
-
20
19
  // Best-effort recovery for runtime execution and spawned children if launched
21
20
  // from an unlinked/deleted working directory (runs after hoisted ESM module imports).
22
21
  try {
@@ -156,9 +155,9 @@ import {
156
155
  reconcileEnsureDesiredIntegrations,
157
156
  } from "./ensure-desired-integrations";
158
157
  import { ENSURE_READY_TIMEOUT_MS, ensureKeepWaiting, waitForLiveProxy } from "./ensure-readiness";
158
+ import { productionDesktopColdStartDeps, runDesktopColdStart } from "./ensure-desktop-cold-start";
159
159
  import { refreshOwnedCatalogIntegrations } from "../integrations/catalog-refresh";
160
160
  import { loadExportModels } from "../server/management/model-rows";
161
-
162
161
  import { removeOwnedConfigAfterDesktopCleanup } from "./uninstall-client-state";
163
162
  import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
164
163
  import { startArgv } from "../lib/self-launch-argv";
@@ -169,6 +168,7 @@ import { recoverStartStateUnderOwnershipLease } from "./start-owner-fence";
169
168
  import { handledSignalExitCode } from "../lib/handled-signal-exit";
170
169
  import { createLocalAttestationSecret } from "../lib/local-management-attestation";
171
170
  import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
171
+ import { startServiceLogging } from "../service/logging";
172
172
 
173
173
  /**
174
174
  * A failed shell-hook reconcile is not cosmetic: a stale hook keeps sourcing
@@ -208,6 +208,7 @@ async function refreshOwnedRaycastCatalog(
208
208
  }
209
209
  }
210
210
 
211
+ if (process.argv[2] === "start" && process.env.OCX_SERVICE) startServiceLogging(join(getConfigDir(), "service.log"));
211
212
  const updateRestartChild = admitUpdateRestartChild(process.argv.slice(2));
212
213
  initializeNodeLauncherContext();
213
214
  // The compiled executable is also the capture-only MCP server's launcher.
@@ -282,6 +283,7 @@ async function chooseListenPort(
282
283
  `Port ${preferred} is reserved for unauthenticatedLoopbackListener; choose a different proxy port.`,
283
284
  );
284
285
  }
286
+ await (await import("./auxiliary-listener-preflight")).assertAuxiliaryListenerPortsAvailable(config);
285
287
  // Soft start: brief prefer-retry then ephemeral hop.
286
288
  // Explicit `--port` (service wrappers / update restart): wait for the pinned port
287
289
  // to free without killing any listener (healthy ocx / foreign). Never hop.
@@ -816,9 +818,16 @@ async function handleEnsure(options: { existingIsSuccess?: boolean; forceStart?:
816
818
  console.log("Codex autostart is disabled.");
817
819
  return false;
818
820
  }
819
- const live = owner.live;
821
+ let live = owner.live;
822
+ if (!live) {
823
+ // OpenCodex Desktop owns or runs this proxy: wait for it or start it, never a competing CLI proxy (#6971).
824
+ const desktopStart = await runDesktopColdStart(productionDesktopColdStartDeps(timeoutMs => waitForProxy(timeoutMs)));
825
+ if (desktopStart.kind === "failed") { console.error(`❌ ${desktopStart.message}`); process.exitCode = 1; return false; }
826
+ // Also for restart: the proxy was absent and Desktop started it, so the live tail below applies.
827
+ if (desktopStart.kind === "live") live = desktopStart.live;
828
+ }
820
829
  if (live) {
821
- if (options.existingIsSuccess === false) {
830
+ if (options.existingIsSuccess === false && live === owner.live) {
822
831
  console.error("Proxy appeared while restart was confirming absence; no start was attempted.");
823
832
  return false;
824
833
  }
@@ -26,6 +26,7 @@ export const FILE_INTEGRATION_CLIENTS = [
26
26
  "commandcode",
27
27
  "kilo",
28
28
  "droid",
29
+ "qoder",
29
30
  ] as const;
30
31
 
31
32
  export const INTEGRATION_REFUSAL_REASONS: ReadonlySet<string> = new Set<IntegrationRefusalReason>([
@@ -65,6 +66,7 @@ const PLAN_SCHEMA_PATHS = new Set([
65
66
  "settings.providers.opencodex",
66
67
  "catalog.providers.opencodex",
67
68
  "customModels.*",
69
+ "modelConfigs.customModels.*",
68
70
  // ZCode reads its providers from a second file; a plan for it publishes that
69
71
  // file's templates, and a path missing here is rejected as an invalid preview.
70
72
  "config.providerConfigRules.providerRules.[providerId=opencodex]",
@@ -378,6 +378,18 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
378
378
  usage: "ocx model <subcommand>",
379
379
  summary: "Alias of ocx models.",
380
380
  },
381
+ {
382
+ name: "advisor",
383
+ usage: "ocx advisor <status|on|off|consent|set> ...",
384
+ summary: "Inspect and configure the advisor sidecar (expert consultation for routed workers).",
385
+ details: [
386
+ "ocx advisor and ocx advisor status read the resolved settings; use --json for machine-readable output.",
387
+ "ocx advisor on requires current context-sharing consent. Pass --ack-context-sharing to record consent v1 and enable. ocx advisor off disables the sidecar without granting consent.",
388
+ "ocx advisor consent records consent v1 after printing the disclosure. ocx advisor consent --revoke removes it and stops task-context transfer.",
389
+ "ocx advisor set updates --model, --effort, --policy and --timeout-ms; the model may be any routable model string (bare native, provider/model, or account-qualified). set does not grant consent.",
390
+ "policy manual consults only when the worker calls the synthetic advisor tool; policy preflight also attempts one automatic consultation per task with a stable conversation identity, once the task shows orientation evidence. Without a stable identity, each eligible request may trigger another consultation. Both require current consent before any task context is sent.",
391
+ ],
392
+ },
381
393
  {
382
394
  name: "companion",
383
395
  usage: "ocx companion <show|set|reset|usage|timeline> ...",
@@ -469,13 +481,13 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
469
481
  "protocols [--provider <name>] Contract version, API surfaces, protocol settings and feature vocabulary.",
470
482
  "explain --model <id> --inbound <responses|chat|messages> [--feature <key>]... Preview the request path; sends nothing upstream.",
471
483
  "policy Read the protocol policy; with --messages, --unrepresentable or --rollout <switch>=<on|off> it changes config.",
472
- "Every rollout switch defaults off. `ocx api policy` writes only when a setting flag is given.",
484
+ "Every rollout switch defaults off except antigravityDirect. `ocx api policy` writes only when a setting flag is given.",
473
485
  ],
474
486
  },
475
487
  {
476
488
  name: "export",
477
- usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|commandcode|prime|aside|raycast|omo|cline|kilo|droid> [--json] [--out <path>] [--force]",
478
- summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid) wired to the running proxy.",
489
+ usage: "ocx export --client <qoder|opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|commandcode|prime|aside|raycast|omo|cline|kilo|droid> [--json] [--out <path>] [--force]",
490
+ summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid, Qoder) wired to the running proxy.",
479
491
  details: [
480
492
  "--json prints the generated document as JSON on stdout; use --out for the client's native format.",
481
493
  "--out <path> writes the native config there and refuses to replace an existing file without --force.",
@@ -173,6 +173,9 @@ export async function handleDesktopAppRestart(
173
173
  + "Quit and relaunch the desktop app manually to refresh the model picker.",
174
174
  );
175
175
  return result;
176
+ case "relaunch_context_failed":
177
+ log.error("Codex desktop launch options could not be preserved safely, so the app was not stopped. Review its current launch options before retrying.");
178
+ return result;
176
179
  case "no_targets":
177
180
  log.log("Codex desktop app is not running; nothing to restart.");
178
181
  return result;
@@ -189,4 +192,3 @@ export async function handleDesktopAppRestart(
189
192
  return result;
190
193
  }
191
194
  }
192
-
@@ -202,6 +202,7 @@ export interface ExportContext {
202
202
  export type DroidReasoningDefaults = Record<string, string>;
203
203
 
204
204
  export type ExportClientId =
205
+ | "qoder"
205
206
  | "opencode"
206
207
  | "pi"
207
208
  | "omp"
@@ -9,8 +9,9 @@ import { shouldInjectApiAuthHeader } from "../../codex/inject";
9
9
 
10
10
 
11
11
  /**
12
- * Authoritative context window, or undefined. Never guesses: a missing, non-finite, or
13
- * non-positive value means the serializer omits every context-derived field.
12
+ * Validate the supplied catalog window (which may be a labelled vendor estimate).
13
+ * A missing, non-finite, or non-positive value omits every context-derived field;
14
+ * this serializer does not independently infer a window.
14
15
  */
15
16
  export function authoritativeContextWindow(contextWindow: number | undefined): number | undefined {
16
17
  if (typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0) {