@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
@@ -0,0 +1,154 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, unlinkSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { atomicWriteFileNoFollow } from "../config/atomic-write";
5
+ import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
6
+ import { assertNotRealClaudeConfigUnderTest } from "../lib/test-home-guard";
7
+ import { claudeConfigDir } from "./auth-detect";
8
+ import { claudeTierDisplayName, type ClaudeTierModels } from "./context-windows";
9
+
10
+ const tiers = ["opus", "sonnet", "haiku", "fable"] as const;
11
+
12
+ function readSettings(path: string): { raw: string; exists: boolean; doc: Record<string, unknown>; env: Record<string, unknown> } {
13
+ let raw = "";
14
+ let exists = false;
15
+ try {
16
+ const stat = lstatSync(path);
17
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) throw new Error("settings_unreadable");
18
+ raw = readFileSync(path, "utf8");
19
+ exists = true;
20
+ } catch (error) {
21
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw new Error("settings_unreadable");
22
+ }
23
+ let doc: Record<string, unknown>;
24
+ try {
25
+ const parsed: unknown = exists ? JSON.parse(raw) : {};
26
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
27
+ doc = parsed as Record<string, unknown>;
28
+ if (doc.env !== undefined && (!doc.env || typeof doc.env !== "object" || Array.isArray(doc.env))) throw new Error();
29
+ } catch { throw new Error("settings_unreadable"); }
30
+ return { raw, exists, doc, env: (doc.env ?? {}) as Record<string, unknown> };
31
+ }
32
+
33
+ function snapshotFingerprint(current: ReturnType<typeof readSettings>, models: ClaudeTierModels | undefined) {
34
+ const target = selectedEnv(models, current.env);
35
+ return createHash("sha256").update(JSON.stringify([current.exists, current.raw, target])).digest("hex");
36
+ }
37
+
38
+ /** Test seams for edits at publication boundaries; never configured through the API. */
39
+ export interface ModelDefaultWriteHooks {
40
+ beforeCapture?: () => void;
41
+ beforePublish?: () => void;
42
+ }
43
+
44
+ export class ModelDefaultsRecoveryError extends Error {
45
+ constructor(readonly backupPath: string, options?: ErrorOptions) {
46
+ super("settings_recovery_required", options);
47
+ }
48
+ }
49
+
50
+ /** Unset tiers stay untouched; only a label matching its current alias is managed. */
51
+ function selectedEnv(models: ClaudeTierModels | undefined, current: Record<string, unknown>): Record<string, string | null> {
52
+ const env: Record<string, string | null> = {};
53
+ for (const tier of tiers) {
54
+ const slot = `ANTHROPIC_DEFAULT_${tier.toUpperCase()}_MODEL`;
55
+ const name = `${slot}_NAME`;
56
+ const value = models?.[tier]?.trim();
57
+ if (!value) continue;
58
+ env[slot] = value;
59
+ const previous = typeof current[slot] === "string" ? claudeTierDisplayName(current[slot]) : undefined;
60
+ const managed = previous !== undefined && current[name] === previous;
61
+ // Custom (including empty/non-string) labels are user-owned. Exact pairing
62
+ // follows the launcher label convention; the name alone proves no ownership.
63
+ if (!Object.hasOwn(current, name) || managed) {
64
+ const label = claudeTierDisplayName(value);
65
+ if (label !== undefined) env[name] = label;
66
+ else if (managed) env[name] = null;
67
+ }
68
+ }
69
+ return env;
70
+ }
71
+
72
+ export function previewClaudeModelDefaults(models: ClaudeTierModels | undefined, configDir = claudeConfigDir()) {
73
+ const path = join(configDir, "settings.json");
74
+ const current = readSettings(path);
75
+ const target = selectedEnv(models, current.env);
76
+ const changes = Object.entries(target).filter(([key, value]) => current.env[key] !== value)
77
+ // The existing preview wire shape uses an empty replacement for label removal.
78
+ .map(([key, value]) => ({ key, before: typeof current.env[key] === "string" ? current.env[key] as string : null, after: value ?? "" }));
79
+ const fingerprint = snapshotFingerprint(current, models);
80
+ return { path, fingerprint, changes };
81
+ }
82
+
83
+ export function applyClaudeModelDefaults(
84
+ models: ClaudeTierModels | undefined,
85
+ fingerprint: string,
86
+ configDir = claudeConfigDir(),
87
+ backupDir = join(configDir, "model-default-backups"),
88
+ hooks: ModelDefaultWriteHooks = {},
89
+ ) {
90
+ const preview = previewClaudeModelDefaults(models, configDir);
91
+ if (preview.fingerprint !== fingerprint) throw new Error("settings_changed");
92
+ if (!preview.changes.length) return { changed: false, path: preview.path };
93
+ assertNotRealClaudeConfigUnderTest(configDir, preview.path);
94
+ const current = readSettings(preview.path);
95
+ const check = snapshotFingerprint(current, models);
96
+ if (check !== fingerprint) throw new Error("settings_changed");
97
+ mkdirSync(configDir, { recursive: true });
98
+ mkdirSync(backupDir, { recursive: true, mode: 0o700 });
99
+ const recoveryDir = mkdtempSync(join(backupDir, "apply-"));
100
+ if (process.platform === "win32") hardenSecretDir(recoveryDir, { required: true });
101
+ const backupPath = join(recoveryDir, "settings.json");
102
+ const staged = join(configDir, `.model-defaults-${randomUUID()}.tmp`);
103
+ let captured = false;
104
+ let published = false;
105
+ try {
106
+ const env = { ...current.env };
107
+ for (const [key, value] of Object.entries(selectedEnv(models, current.env))) {
108
+ if (value === null) delete env[key];
109
+ else env[key] = value;
110
+ }
111
+ current.doc.env = env;
112
+ atomicWriteFileNoFollow(staged, `${JSON.stringify(current.doc, null, 2)}\n`);
113
+ // Verify same-volume exclusive publication before moving the shared file.
114
+ const probe = join(recoveryDir, "link-probe");
115
+ linkSync(staged, probe);
116
+ unlinkSync(probe);
117
+ hooks.beforeCapture?.();
118
+ if (current.exists) {
119
+ // Move the actual destination, not a stale in-memory copy. A same-filesystem
120
+ // move captures late edits; cross-device failures leave the original alone.
121
+ try { renameSync(preview.path, backupPath); } catch (error) {
122
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new Error("settings_changed");
123
+ throw error;
124
+ }
125
+ captured = true;
126
+ const capturedEntry = lstatSync(backupPath);
127
+ if (!capturedEntry.isFile() || capturedEntry.isSymbolicLink()) throw new Error("settings_changed");
128
+ if (process.platform === "win32") hardenSecretPath(backupPath, { required: true });
129
+ if (snapshotFingerprint(readSettings(backupPath), models) !== fingerprint) throw new Error("settings_changed");
130
+ }
131
+ hooks.beforePublish?.();
132
+ // A hard-link publish is exclusive on POSIX and Windows: never replace a
133
+ // document created by another writer during the brief move-aside interval.
134
+ try { linkSync(staged, preview.path); } catch (error) {
135
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") throw new Error("settings_changed");
136
+ throw error;
137
+ }
138
+ published = true;
139
+ return { changed: true, path: preview.path, ...(captured ? { backupPath } : {}) };
140
+ } catch (error) {
141
+ if (captured && !published) {
142
+ try { linkSync(backupPath, preview.path); } catch (restoreError) {
143
+ // A newer destination wins. Retain the captured document privately;
144
+ // never delete the winner or overwrite it as an automatic rollback.
145
+ throw new ModelDefaultsRecoveryError(backupPath, { cause: restoreError });
146
+ }
147
+ }
148
+ throw error;
149
+ } finally {
150
+ // The private stage is also present at the destination after successful
151
+ // linking; cleanup failure must not turn a committed apply into failure.
152
+ try { unlinkSync(staged); } catch { /* private residue, no shared-file cleanup */ }
153
+ }
154
+ }
@@ -103,19 +103,19 @@ function modelInfo(id: string, displayName: string, ladder: readonly string[], i
103
103
 
104
104
  /**
105
105
  * Which id family the discovery list carries (devlog 050): Claude Code (CLI)
106
- * gets readable `claude-ocx-*` ids; Claude Desktop keeps the hashed
107
- * `claude-opus-4-8-<code>` family its 3P config was written with. Both families
106
+ * gets readable `ocx-claude-*` ids; Claude Desktop keeps its profile's
107
+ * `claude-fable-5-p<code>` ids (legacy no-profile callers use hash aliases). Both families
108
108
  * decode in resolveInboundModel regardless of the style served here.
109
109
  */
110
110
  export type AnthropicIdStyle = "desktop3p" | "readable";
111
111
 
112
- /** Build the full anthropic-flavor discovery list (ids are Desktop 3P aliases). */
112
+ /** Build discovery rows; a Desktop resolver can return undefined to withhold an unbound ID. */
113
113
  export function buildAnthropicModelInfos(
114
114
  nativeSlugs: readonly string[],
115
115
  routedModels: readonly CatalogModel[],
116
116
  auto: AutoContextMode = AUTO_CONTEXT_OFF,
117
117
  idStyle: AnthropicIdStyle = "desktop3p",
118
- aliasForRoute: (provider: string, modelId: string) => string = desktop3pAlias,
118
+ aliasForRoute: (provider: string, modelId: string) => string | undefined = desktop3pAlias,
119
119
  nativeContextCap?: NativeContextLimitsInput,
120
120
  fastMode?: boolean,
121
121
  // Presence is the feature gate: the caller passes undefined when `fastRows` is off, so a
@@ -144,7 +144,7 @@ export function buildAnthropicModelInfos(
144
144
  ? claudeCodeAlias(m.provider, listed)
145
145
  : aliasForRoute(m.provider, m.id);
146
146
  }),
147
- ]);
147
+ ].filter((id): id is string => typeof id === "string" && id.length > 0));
148
148
  // [1m] picker variant (devlog 260712 B1; widened in devlog/_plan/261009_claude_1m_default/020):
149
149
  // Claude Code accounts exactly 1M for ids carrying the marker (2.1.207 binary: /\[1m\]/i -> 1e6,
150
150
  // compaction preserved). A discovery client may lack CLAUDE_CODE_AUTO_COMPACT_WINDOW, so the
@@ -201,7 +201,7 @@ export function buildAnthropicModelInfos(
201
201
  };
202
202
  for (const slug of nativeSlugs) {
203
203
  const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
204
- if (seen.has(id)) continue;
204
+ if (!id || seen.has(id)) continue;
205
205
  seen.add(id);
206
206
  const nativeWindow = nativeOpenAiContextWindow(slug, nativeContextCap);
207
207
  const nativeMaxInput = nativeOpenAiMaxInputTokens(slug, nativeContextCap);
@@ -228,7 +228,7 @@ export function buildAnthropicModelInfos(
228
228
  const id = idStyle === "readable"
229
229
  ? claudeCodeAlias(m.provider, listedModelId)
230
230
  : aliasForRoute(m.provider, m.id);
231
- if (seen.has(id)) continue;
231
+ if (!id || seen.has(id)) continue;
232
232
  seen.add(id);
233
233
  const groupStart = out.length;
234
234
  const ladder = Array.isArray(m.reasoningEfforts) ? m.reasoningEfforts : [];
@@ -11,7 +11,7 @@
11
11
  * - errors: {type:"error", error:{type,message}}; may arrive mid-stream after HTTP 200.
12
12
  */
13
13
  import { createHash } from "node:crypto";
14
- import { httpStatusFromTerminalError } from "../lib/errors";
14
+ import { httpStatusFromTerminalError, UNDECLARED_TOOL_CALL_ERROR_CODE } from "../lib/errors";
15
15
  import { isTransientUpstreamStatus } from "../lib/upstream-retry";
16
16
  import {
17
17
  isTranslatorBudgetExceededError,
@@ -81,12 +81,16 @@ export function anthropicErrorType(status: number): string {
81
81
  * (devlog/_plan/261009_claude_1m_default/010).
82
82
  */
83
83
  const CLAUDE_PROMPT_TOO_LONG_RE = /prompt is too long|input is too long for requested model/i;
84
- const STATED_CONTEXT_COUNTS_RE = /maximum context length is (\d+) tokens[\s\S]*?(?:resulted in|you requested) (\d+) tokens/i;
84
+ const MAX_OVERFLOW_MESSAGE_CHARS = 64 * 1024;
85
+ const STATED_CONTEXT_LIMIT_RE = /maximum context length is (\d+) tokens/i;
86
+ const STATED_REQUEST_COUNT_RE = /(?:resulted in|you requested) (\d+) tokens/i;
85
87
 
86
88
  export function claudePromptTooLongMessage(message: string): string {
89
+ message = message.slice(0, MAX_OVERFLOW_MESSAGE_CHARS);
87
90
  if (CLAUDE_PROMPT_TOO_LONG_RE.test(message)) return message;
88
- const counts = STATED_CONTEXT_COUNTS_RE.exec(message);
89
- if (counts) return `prompt is too long: ${counts[2]} tokens > ${counts[1]} maximum`;
91
+ const limit = STATED_CONTEXT_LIMIT_RE.exec(message);
92
+ const requested = limit && STATED_REQUEST_COUNT_RE.exec(message.slice(limit.index + limit[0].length));
93
+ if (limit && requested) return `prompt is too long: ${requested[1]} tokens > ${limit[1]} maximum`;
90
94
  return `prompt is too long: ${message}`;
91
95
  }
92
96
 
@@ -804,11 +808,14 @@ export function responsesSseToAnthropicSse(
804
808
  // Unclassified status-absent response.failed (relaySseWithFailedTail synthetic
805
809
  // tail) still lands on a transient 5xx here — the mid-stream reset shape maps to
806
810
  // overloaded_error by design.
811
+ // The undeclared-tool refusal is deterministic and proxy-owned, not an upstream
812
+ // condition: keep its api_error type and code instead of a retryable overload.
813
+ const deterministic = code === UNDECLARED_TOOL_CALL_ERROR_CODE;
807
814
  fail(
808
815
  status,
809
816
  message,
810
- true,
811
- code === "context_length_exceeded" ? code : undefined,
817
+ !deterministic,
818
+ deterministic || code === "context_length_exceeded" ? code : undefined,
812
819
  );
813
820
  break;
814
821
  }
@@ -0,0 +1,151 @@
1
+ import { ADVISOR_CONTEXT_SHARING_CONSENT_VERSION, ADVISOR_CONTEXT_SHARING_DISCLOSURE } from "../advisor/disclosure";
2
+ import { CliUsageError, printData, rejectArgs, runCliAction, runtimeRequest, takeFlag, type RuntimeApiDeps } from "./runtime-api";
3
+
4
+ const USAGE = `Usage:
5
+ ocx advisor status [--json]
6
+ ocx advisor on [--ack-context-sharing] [--json]
7
+ ocx advisor off [--json]
8
+ ocx advisor consent [--revoke] [--json]
9
+ ocx advisor set [--model <model>] [--effort <effort>] [--policy <manual|preflight>] [--timeout-ms <ms>] [--json]`;
10
+
11
+ const VALUED_FLAGS = new Set(["--model", "--effort", "--policy", "--timeout-ms"]);
12
+
13
+ interface SetOptions {
14
+ model?: string;
15
+ effort?: string;
16
+ policy?: string;
17
+ timeoutMs?: string;
18
+ }
19
+
20
+ function parseSetArgs(args: string[]): SetOptions {
21
+ const options: SetOptions = {};
22
+ for (let index = 0; index < args.length; index += 1) {
23
+ const arg = args[index]!;
24
+ if (!VALUED_FLAGS.has(arg)) throw new CliUsageError(`unknown advisor set option ${arg}`, USAGE);
25
+ const value = args[++index];
26
+ if (value === undefined || value.startsWith("--")) throw new CliUsageError(`${arg} requires a value`, USAGE);
27
+ // An explicitly supplied empty value is meaningful for --model alone: the settings route
28
+ // treats "" as the supported "clear the model" state. Every other valued flag still needs one.
29
+ if (value === "" && arg !== "--model") throw new CliUsageError(`${arg} requires a value`, USAGE);
30
+ if (arg === "--model") options.model = value;
31
+ else if (arg === "--effort") options.effort = value;
32
+ else if (arg === "--policy") options.policy = value;
33
+ else options.timeoutMs = value;
34
+ }
35
+ if (Object.keys(options).length === 0) {
36
+ throw new CliUsageError("advisor set requires at least one of --model, --effort, --policy, --timeout-ms", USAGE);
37
+ }
38
+ return options;
39
+ }
40
+
41
+ async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
42
+ const args = [...argv];
43
+ const wantsJson = takeFlag(args, "--json");
44
+ rejectArgs(args, USAGE);
45
+ printData(await runtimeRequest("/api/advisor/settings", {}, deps), wantsJson);
46
+ }
47
+
48
+ function printDisclosure(): void {
49
+ console.error(ADVISOR_CONTEXT_SHARING_DISCLOSURE);
50
+ }
51
+
52
+ async function readConsent(deps: RuntimeApiDeps): Promise<unknown> {
53
+ const current = await runtimeRequest("/api/advisor/settings", {}, deps) as {
54
+ settings?: { contextSharingConsent?: unknown };
55
+ };
56
+ return current.settings?.contextSharingConsent;
57
+ }
58
+
59
+ async function setEnabled(enabled: boolean, argv: string[], deps: RuntimeApiDeps): Promise<void> {
60
+ const args = [...argv];
61
+ const wantsJson = takeFlag(args, "--json");
62
+ const acknowledge = takeFlag(args, "--ack-context-sharing");
63
+ rejectArgs(args, USAGE);
64
+ if (!enabled) {
65
+ if (acknowledge) {
66
+ throw new CliUsageError(
67
+ "--ack-context-sharing has no effect with `ocx advisor off`; it never records consent.",
68
+ USAGE,
69
+ );
70
+ }
71
+ printData(await runtimeRequest("/api/advisor/settings", {
72
+ method: "PUT",
73
+ body: JSON.stringify({ enabled: false }),
74
+ }, deps), wantsJson, ["Advisor disabled."]);
75
+ return;
76
+ }
77
+ const consent = await readConsent(deps);
78
+ if (acknowledge) {
79
+ printDisclosure();
80
+ printData(await runtimeRequest("/api/advisor/settings", {
81
+ method: "PUT",
82
+ body: JSON.stringify({
83
+ enabled: true,
84
+ contextSharingConsent: ADVISOR_CONTEXT_SHARING_CONSENT_VERSION,
85
+ }),
86
+ }, deps), wantsJson, ["Advisor enabled with context-sharing consent v1."]);
87
+ return;
88
+ }
89
+ if (consent !== ADVISOR_CONTEXT_SHARING_CONSENT_VERSION) {
90
+ printDisclosure();
91
+ throw new CliUsageError(
92
+ "Advisor context-sharing consent is required before consultations can send task content. "
93
+ + "Re-run `ocx advisor on --ack-context-sharing` after reading the disclosure, or run `ocx advisor consent`.",
94
+ USAGE,
95
+ );
96
+ }
97
+ printData(await runtimeRequest("/api/advisor/settings", {
98
+ method: "PUT",
99
+ body: JSON.stringify({ enabled: true }),
100
+ }, deps), wantsJson, ["Advisor enabled."]);
101
+ }
102
+
103
+ async function consent(argv: string[], deps: RuntimeApiDeps): Promise<void> {
104
+ const args = [...argv];
105
+ const wantsJson = takeFlag(args, "--json");
106
+ const revoke = takeFlag(args, "--revoke");
107
+ rejectArgs(args, USAGE);
108
+ if (!revoke) printDisclosure();
109
+ printData(await runtimeRequest("/api/advisor/settings", {
110
+ method: "PUT",
111
+ body: JSON.stringify({
112
+ contextSharingConsent: revoke ? null : ADVISOR_CONTEXT_SHARING_CONSENT_VERSION,
113
+ }),
114
+ }, deps), wantsJson, [revoke
115
+ ? "Advisor context-sharing consent removed. Consultations will not send task content."
116
+ : "Advisor context-sharing consent v1 recorded.",
117
+ ]);
118
+ }
119
+
120
+ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
121
+ const args = [...argv];
122
+ const wantsJson = takeFlag(args, "--json");
123
+ const options = parseSetArgs(args);
124
+ const patch: Record<string, unknown> = {};
125
+ if (options.model !== undefined) patch.model = options.model;
126
+ if (options.effort !== undefined) patch.effort = options.effort;
127
+ if (options.policy !== undefined) patch.policy = options.policy;
128
+ if (options.timeoutMs !== undefined) {
129
+ const parsed = Number(options.timeoutMs);
130
+ if (!Number.isFinite(parsed)) throw new CliUsageError("--timeout-ms must be a number", USAGE);
131
+ patch.timeoutMs = parsed;
132
+ }
133
+ printData(await runtimeRequest("/api/advisor/settings", {
134
+ method: "PUT",
135
+ body: JSON.stringify(patch),
136
+ }, deps), wantsJson, ["Advisor settings saved."]);
137
+ }
138
+
139
+ export async function handleAdvisorCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
140
+ return runCliAction(async () => {
141
+ const [sub = "status", ...rest] = argv;
142
+ if (sub === "status") await status(rest, deps);
143
+ else if (sub === "on") await setEnabled(true, rest, deps);
144
+ else if (sub === "off") await setEnabled(false, rest, deps);
145
+ else if (sub === "consent") await consent(rest, deps);
146
+ else if (sub === "set") await set(rest, deps);
147
+ else throw new CliUsageError(`unknown advisor command ${sub}`, USAGE);
148
+ });
149
+ }
150
+
151
+ export const ADVISOR_USAGE = USAGE;
@@ -0,0 +1,37 @@
1
+ import type { OcxConfig } from "../types";
2
+ import { AuxiliaryListenerBindError } from "../server/ports";
3
+ import { createServer } from "node:net";
4
+
5
+ /**
6
+ * Reject impossible auxiliary binds before startup creates private state. On Windows
7
+ * that publication requires several synchronous ACL subprocesses. This is only an
8
+ * early rejection: startServer still owns the actual bind and transactional rollback
9
+ * if another process takes the port between this probe and startup.
10
+ */
11
+ export async function assertAuxiliaryListenerPortsAvailable(config: OcxConfig): Promise<void> {
12
+ const loopback = config.unauthenticatedLoopbackListener;
13
+ const management = config.runtimeRole === "hub" ? config.hub?.managementIngress : undefined;
14
+ const targets = [
15
+ { listener: "unauthenticatedLoopbackListener", port: loopback?.enabled ? loopback.port : undefined },
16
+ { listener: "hub.managementIngress", port: management?.enabled ? management.port : undefined },
17
+ ] as const;
18
+ // Each probe releases its socket, so two listeners configured for one port would both
19
+ // pass and startup would still fail on the second bind. Reject the collision directly.
20
+ if (targets[0].port !== undefined && targets[0].port === targets[1].port) {
21
+ throw new AuxiliaryListenerBindError(
22
+ "hub.managementIngress", targets[1].port, "127.0.0.1",
23
+ new Error("port is also configured for unauthenticatedLoopbackListener"),
24
+ );
25
+ }
26
+ for (const { listener, port } of targets) {
27
+ // The port-less companion shares the public bind; it has no independent target.
28
+ if (port === undefined) continue;
29
+ await new Promise<void>((resolve, reject) => {
30
+ const probe = createServer(socket => socket.destroy());
31
+ const fail = (cause: unknown) => reject(new AuxiliaryListenerBindError(listener, port, "127.0.0.1", cause));
32
+ probe.once("error", fail);
33
+ probe.once("listening", () => probe.close(error => error ? fail(error) : resolve()));
34
+ try { probe.listen({ port, host: "127.0.0.1" }); } catch (error) { fail(error); }
35
+ });
36
+ }
37
+ }
@@ -25,6 +25,24 @@ export const AGENT_ROUTING_CAPABILITIES: readonly Capability[] = [
25
25
  "queued means submitted, not processed. unknown must not be replayed; no automatic retry, daemon start or thread resume.",
26
26
  "Exit 0: queued; 1: not sent; 3: unknown; 64: invalid usage. No remote/Claude transport or skill installation."],
27
27
  },
28
+ {
29
+ command: ["advisor"],
30
+ summary: "Inspect and configure the advisor sidecar (expert consultation for routed workers).",
31
+ routes: [
32
+ { method: "GET", path: "/api/advisor/settings" },
33
+ { method: "PUT", path: "/api/advisor/settings" },
34
+ ],
35
+ flags: [{ name: "--json", value: "boolean", summary: "Emit advisor settings as JSON." }],
36
+ mutates: true,
37
+ json: "payload",
38
+ details: [
39
+ "`status` (the default) reads the resolved settings; `on`/`off` toggle the sidecar; `consent` records or revokes context-sharing consent; `set` updates model, effort, policy, or timeout.",
40
+ "`on` does not grant consent. Without current consent it refuses and prints the disclosure. `on --ack-context-sharing` records consent v1 and enables. `consent --revoke` removes consent and stops task-context transfer.",
41
+ "The advisor model may be any routable model string: a bare native model, an explicit `provider/model`, or an account-qualified native model.",
42
+ "`policy: preflight` makes OpenCodex attempt one automatic consultation per task with a stable conversation identity once the task shows orientation evidence (an assistant tool call or a tool result after the latest user message). Without a stable identity, each eligible request may trigger another consultation. `policy: manual` consults only when the worker calls the synthetic `advisor` tool. Neither path sends task context without current context-sharing consent.",
43
+ ],
44
+ },
45
+
28
46
  {
29
47
  command: ["agent", "status"],
30
48
  usage: "ocx agent status [--json]",
@@ -786,7 +786,7 @@ export const CAPABILITIES: readonly Capability[] = [
786
786
  command: ["api","policy"],
787
787
  summary: "Read the protocol policy, or change the Messages surface, unrepresentable policy and rollout switches.",
788
788
  routes: [{"method":"GET","path":"/api/protocols"},{"method":"PATCH","path":"/api/protocols/settings"}],
789
- flags: [{"name":"--messages","value":"string","summary":"Open or close the Messages API: on or off. Off also turns the Claude integration off."},{"name":"--unrepresentable","value":"string","summary":"legacy keeps today's behavior; reject refuses a request its path cannot carry."},{"name":"--rollout","value":"string","summary":"One switch as name=on or name=off; repeatable. Every switch defaults off."},{"name":"--json","value":"boolean","summary":"Emit the resulting GET /api/protocols body."}],
789
+ flags: [{"name":"--messages","value":"string","summary":"Open or close the Messages API: on or off. Off also turns the Claude integration off."},{"name":"--unrepresentable","value":"string","summary":"legacy keeps today's behavior; reject refuses a request its path cannot carry."},{"name":"--rollout","value":"string","summary":"One switch as name=on or name=off; repeatable. Every switch defaults off except antigravityDirect."},{"name":"--json","value":"boolean","summary":"Emit the resulting GET /api/protocols body."}],
790
790
  mutates: true,
791
791
  json: "payload",
792
792
  details: ["A bare invocation reads and never writes.","A setting flag changes the operator's config; run it only when the operator asks for that change."],
@@ -20,7 +20,7 @@ const USAGE = `Usage (experimental, macOS only):
20
20
 
21
21
  function run(command: string, args: string[]) {
22
22
  const result = spawnSync(command, args, { encoding: "utf8", timeout: 5000 });
23
- return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
23
+ return { ok: result.status === 0, status: result.status, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
24
24
  }
25
25
 
26
26
  /**
@@ -42,11 +42,12 @@ function chatgptDesktopIssueInFile(): string | null {
42
42
  * desktop restart adapter does. "ChatGPT" is a display name another app can share, so quitting,
43
43
  * relaunching and the binary path all key on this verified bundle rather than on the name.
44
44
  */
45
- function discoverApp(): DesktopAppInstall | null {
45
+ function discoverApp(): DesktopAppInstall | null | undefined {
46
46
  try {
47
47
  return darwinDesktopAppAdapter.discover(darwinDefaultExec);
48
48
  } catch {
49
- return null;
49
+ // An inspection error is not evidence that the app is uninstalled.
50
+ return undefined;
50
51
  }
51
52
  }
52
53
 
@@ -55,14 +56,20 @@ function discoverApp(): DesktopAppInstall | null {
55
56
  * `-a` keeps ancestors in the match: when ocx runs inside a ChatGPT/Codex session the app is
56
57
  * one of this process's ancestors, and plain `pgrep -x` would report it as not running.
57
58
  */
58
- function appState(install: DesktopAppInstall, launcher: string): { running: boolean; shim: boolean } {
59
+ function appState(install: DesktopAppInstall, launcher: string): { running: boolean | null; shim: boolean | null } {
59
60
  const shell = join(install.root, "Contents", "MacOS", "ChatGPT");
60
61
  // Only this user's processes: another account's ChatGPT can neither be quit nor relaunched here.
61
62
  const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
62
63
  const pids = run("pgrep", [...(uid === undefined ? [] : ["-U", String(uid)]), "-a", "-x", "ChatGPT"]);
63
- if (!pids.ok) return { running: false, shim: false };
64
- for (const pid of pids.output.split(/\s+/).filter(value => /^\d+$/.test(value))) {
64
+ // pgrep exit 1 means no match; timeout, signal, or command errors are unknown.
65
+ if (!pids.ok) return pids.status === 1
66
+ ? { running: false, shim: false } : { running: null, shim: null };
67
+ const candidates = pids.output.split(/\s+/).filter(value => /^\d+$/.test(value));
68
+ if (candidates.length === 0) return { running: null, shim: null };
69
+ let unreadable = false;
70
+ for (const pid of candidates) {
65
71
  const command = run("ps", ["eww", "-o", "command=", "-p", pid]);
72
+ if (!command.ok || !command.output) unreadable = true;
66
73
  if (command.ok && (command.output === shell || command.output.startsWith(`${shell} `))) {
67
74
  const marker = `CODEX_CLI_PATH=${launcher}`;
68
75
  const start = command.output.indexOf(marker);
@@ -70,20 +77,24 @@ function appState(install: DesktopAppInstall, launcher: string): { running: bool
70
77
  && (start + marker.length === command.output.length || command.output[start + marker.length] === " ") };
71
78
  }
72
79
  }
73
- return { running: false, shim: false };
80
+ return unreadable ? { running: null, shim: null } : { running: false, shim: false };
74
81
  }
75
82
 
76
83
  /** Adapted from #5947: open ignores new launch settings until the previous app exits. */
77
84
  async function quitApp(install: DesktopAppInstall, launcher: string): Promise<boolean> {
78
- if (!appState(install, launcher).running) return true;
85
+ const initial = appState(install, launcher).running;
86
+ if (initial === false) return true;
87
+ if (initial === null) return false;
79
88
  for (let attempt = 0; attempt < 3; attempt++) {
80
89
  run("/usr/bin/osascript", ["-e", `quit app id "${install.id}"`]);
81
90
  for (let poll = 0; poll < 20; poll++) {
82
- if (!appState(install, launcher).running) return true;
91
+ const running = appState(install, launcher).running;
92
+ if (running === false) return true;
93
+ if (running === null) return false;
83
94
  await Bun.sleep(250);
84
95
  }
85
96
  }
86
- return !appState(install, launcher).running;
97
+ return appState(install, launcher).running === false;
87
98
  }
88
99
 
89
100
  export async function handleChatgptCommand(args: string[], platform: NodeJS.Platform = process.platform): Promise<number> {
@@ -105,16 +116,21 @@ export async function handleChatgptCommand(args: string[], platform: NodeJS.Plat
105
116
  const launcher = chatgptShimLauncherPath();
106
117
  const install = discoverApp();
107
118
  if (sub === "status") {
108
- const app = install ? appState(install, launcher) : { running: false, shim: false };
119
+ const app = install ? appState(install, launcher)
120
+ : install === undefined ? { running: null, shim: null } : { running: false, shim: false };
109
121
  const configIssue = config.chatgptDesktop?.appServerShim === true ? null : chatgptDesktopIssueInFile();
110
122
  const flag = config.chatgptDesktop?.appServerShim === true ? "on"
111
123
  : configIssue ? `off (config.json ${configIssue}; the whole chatgptDesktop block is ignored)` : "off";
112
124
  console.log(`app-server shim (experimental): ${flag}
113
125
  launcher: ${existsSync(launcher) ? "present" : "absent"}
114
- app: ${install ? (app.running ? "running" : "not running") : "not installed"}
115
- CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
126
+ app: ${install === undefined ? "unknown (app discovery failed)" : install ? (app.running === null ? "unknown (process inspection failed)" : app.running ? "running" : "not running") : "not installed"}
127
+ CODEX_CLI_PATH launcher: ${app.shim === null ? "unknown" : app.shim ? "yes" : "no"}`);
116
128
  return 0;
117
129
  }
130
+ if (install === undefined) {
131
+ console.error("ChatGPT app discovery failed; no launcher was changed and no app was opened. Retry after resolving discovery.");
132
+ return 1;
133
+ }
118
134
  if (!install) {
119
135
  if (sub === "restore") rmSync(launcher, { force: true });
120
136
  console.error("ChatGPT (com.openai.codex) was not found; install or open it once, then retry.");
@@ -143,9 +159,14 @@ CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
143
159
  console.error(`Refusing to ${sub === "launch" ? "launch the shim" : "restore ChatGPT"}: ${untrusted}.`);
144
160
  return 1;
145
161
  }
162
+ // Preflight before changing the launcher. quitApp rechecks at the action boundary.
163
+ if (appState(install, launcher).running === null) {
164
+ console.error("ChatGPT process inspection failed; no launcher was changed and no app was opened. Retry after resolving inspection.");
165
+ return 1;
166
+ }
146
167
  if (binary) writeChatgptShimLauncher(undefined, binary);
147
168
  if (!(await quitApp(install, launcher))) {
148
- console.error("ChatGPT did not quit; quit it manually and retry.");
169
+ console.error("ChatGPT exit could not be confirmed; no new app was opened. Check the running app and retry.");
149
170
  return 1;
150
171
  }
151
172
  // Remove an inherited override too: restore must launch without CODEX_CLI_PATH.