@bitkyc08/opencodex 2.81.0 → 2.82.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 (212) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  11. package/src/adapters/openai-responses/passthrough.ts +9 -4
  12. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  13. package/src/claude/agents-inject.ts +14 -3
  14. package/src/claude/context-windows.ts +19 -17
  15. package/src/claude/desktop-3p.ts +88 -12
  16. package/src/claude/desktop-profile.ts +2 -1
  17. package/src/claude/gateway-cache.ts +18 -1
  18. package/src/claude/inbound.ts +77 -3
  19. package/src/claude/intercept/cli-catalog.ts +3 -0
  20. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  21. package/src/claude/intercept/picker-models.ts +14 -6
  22. package/src/claude/intercept/settings.ts +3 -0
  23. package/src/claude/long-context.ts +66 -0
  24. package/src/claude/model-info.ts +19 -16
  25. package/src/claude/outbound.ts +94 -9
  26. package/src/claude/subagent-model.ts +40 -11
  27. package/src/cli/agent.ts +9 -5
  28. package/src/cli/aside-profile-recovery.ts +33 -0
  29. package/src/cli/capabilities-base.ts +5 -5
  30. package/src/cli/capabilities-integrations.ts +2 -2
  31. package/src/cli/cli-path-diagnostics.ts +132 -0
  32. package/src/cli/desktop-runtime-guidance.ts +65 -0
  33. package/src/cli/dispatch.ts +5 -0
  34. package/src/cli/doctor.ts +16 -3
  35. package/src/cli/index.ts +14 -9
  36. package/src/cli/integration-aside-sync.ts +2 -1
  37. package/src/cli/integrations.ts +16 -3
  38. package/src/cli/resolve.ts +25 -2
  39. package/src/cli/root.ts +4 -2
  40. package/src/cli/status.ts +64 -12
  41. package/src/cli/update-restart-child.ts +4 -2
  42. package/src/cli/update-restart-home.ts +28 -10
  43. package/src/cli/update-restart-service-record.ts +116 -0
  44. package/src/cli/update-restart-supervision.ts +96 -0
  45. package/src/cli/update-restart.ts +59 -13
  46. package/src/cli/v2.ts +27 -20
  47. package/src/cli/version-skew-notice.ts +77 -0
  48. package/src/clients/lazycodex.ts +1 -1
  49. package/src/clients/omo-role-models.ts +32 -7
  50. package/src/codex/auth-api/account-selection.ts +107 -0
  51. package/src/codex/auth-api/routes.ts +10 -49
  52. package/src/codex/auth-context.ts +20 -1
  53. package/src/codex/autostart-health.ts +12 -3
  54. package/src/codex/catalog/build-entries.ts +18 -0
  55. package/src/codex/catalog/bundled.ts +155 -26
  56. package/src/codex/catalog/combo-member.ts +3 -2
  57. package/src/codex/catalog/effort.ts +26 -5
  58. package/src/codex/catalog/gather-capture.ts +5 -0
  59. package/src/codex/catalog/model-hints.ts +15 -8
  60. package/src/codex/catalog/model-visibility.ts +3 -1
  61. package/src/codex/catalog/operator-tiers.ts +38 -0
  62. package/src/codex/catalog/provider-models.ts +43 -35
  63. package/src/codex/catalog/retained-sync.ts +7 -5
  64. package/src/codex/catalog/routed-gather.ts +15 -1
  65. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  66. package/src/codex/catalog-auto-refresh.ts +19 -0
  67. package/src/codex/catalog.ts +1 -1
  68. package/src/codex/codex-home-owner.ts +3 -0
  69. package/src/codex/config-write-lock.ts +333 -0
  70. package/src/codex/convergence.ts +2 -1
  71. package/src/codex/features.ts +257 -200
  72. package/src/codex/inject/config-write-section.ts +34 -0
  73. package/src/codex/inject/multi-agent-v2.ts +14 -6
  74. package/src/codex/inject/remove.ts +36 -11
  75. package/src/codex/inject/restore.ts +85 -20
  76. package/src/codex/inject-coordination.ts +15 -6
  77. package/src/codex/inject.ts +62 -20
  78. package/src/codex/journal.ts +62 -17
  79. package/src/codex/main-account-cache.ts +43 -0
  80. package/src/codex/paths.ts +2 -0
  81. package/src/codex/prepared-home.ts +18 -0
  82. package/src/codex/prompt-journal.ts +25 -12
  83. package/src/codex/prompt-layers.ts +49 -26
  84. package/src/codex/prompt-lock-claim.ts +134 -0
  85. package/src/codex/prompt-lock-evidence.ts +26 -0
  86. package/src/codex/prompt-lock-io.ts +16 -0
  87. package/src/codex/prompt-lock-owner.ts +130 -0
  88. package/src/codex/prompt-lock.ts +84 -53
  89. package/src/codex/routing/failure-window.ts +146 -0
  90. package/src/codex/routing/selection.ts +4 -1
  91. package/src/codex/routing.ts +13 -17
  92. package/src/codex/runtime.ts +106 -18
  93. package/src/codex/shim.ts +45 -19
  94. package/src/combos/jev-service-exchange.ts +281 -0
  95. package/src/combos/jev.ts +34 -220
  96. package/src/config/atomic-write.ts +1 -1
  97. package/src/config/codex-ws-reuse-setting.ts +10 -0
  98. package/src/config/live-reconcile.ts +17 -0
  99. package/src/config/load-degrade.ts +8 -0
  100. package/src/config/rebase-provenance.ts +11 -0
  101. package/src/config/schema/config-schema.ts +4 -0
  102. package/src/generated/compatibility-version.json +303 -171
  103. package/src/generated/model-metadata.ts +2 -2
  104. package/src/grok/inject.ts +50 -4
  105. package/src/grok/sync.ts +2 -1
  106. package/src/images/loop.ts +29 -11
  107. package/src/integrations/aside-profiles.ts +10 -3
  108. package/src/integrations/catalog-refresh.ts +18 -2
  109. package/src/integrations/writer.ts +9 -3
  110. package/src/lib/bun-path-runtime.d.mts +32 -0
  111. package/src/lib/bun-path-runtime.mjs +95 -0
  112. package/src/lib/bun-runtime-preflight.ts +140 -0
  113. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  114. package/src/lib/desktop-cli-handoff.mjs +92 -0
  115. package/src/lib/desktop-cli-record.d.mts +29 -0
  116. package/src/lib/desktop-cli-record.mjs +102 -0
  117. package/src/lib/request-execution-budget.ts +8 -0
  118. package/src/lib/synced-state-location.ts +46 -6
  119. package/src/lib/test-home-guard.ts +87 -1
  120. package/src/lib/translator-budget.ts +17 -4
  121. package/src/lib/windows-atomic-replace.ts +38 -16
  122. package/src/lib/windows-elevation.ts +6 -0
  123. package/src/lib/windows-secret-acl.ts +92 -25
  124. package/src/lib/winsw.ts +2 -3
  125. package/src/oauth/anthropic-account-refusal.ts +39 -1
  126. package/src/oauth/anthropic-routing.ts +1 -1
  127. package/src/oauth/anthropic-send-ownership.ts +3 -3
  128. package/src/oauth/chatgpt.ts +113 -41
  129. package/src/oauth/index.ts +6 -2
  130. package/src/oauth/store.ts +1 -1
  131. package/src/providers/azure-model-metadata.ts +115 -0
  132. package/src/providers/derive.ts +21 -0
  133. package/src/providers/quota/vendor-probes-key.ts +127 -17
  134. package/src/providers/quota.ts +1 -1
  135. package/src/responses/reasoning-envelope.ts +29 -1
  136. package/src/responses/reasoning-replay-cache.ts +17 -1
  137. package/src/responses/spill-store.ts +15 -2
  138. package/src/responses/state/metrics.ts +8 -0
  139. package/src/responses/state/spill-failure.ts +12 -0
  140. package/src/responses/state/spill-queue.ts +12 -5
  141. package/src/responses/state.ts +18 -10
  142. package/src/responses/task-input.ts +17 -0
  143. package/src/server/background-lifecycle.ts +27 -0
  144. package/src/server/chat-completions.ts +1 -0
  145. package/src/server/chat-native.ts +28 -12
  146. package/src/server/claude-messages.ts +43 -6
  147. package/src/server/inbound-body-admission.ts +1 -0
  148. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  149. package/src/server/index/serve-options.ts +10 -2
  150. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  151. package/src/server/index/startup-warnings.ts +19 -0
  152. package/src/server/index.ts +1 -0
  153. package/src/server/management/agent-settings-routes.ts +93 -32
  154. package/src/server/management/codex-agent-role-routes.ts +4 -2
  155. package/src/server/management/codex-prompt-routes.ts +1 -0
  156. package/src/server/management/codex-role-auto-assign.ts +13 -2
  157. package/src/server/management/config-routes.ts +59 -8
  158. package/src/server/management/context.ts +2 -2
  159. package/src/server/management/shared.ts +3 -3
  160. package/src/server/messages-native-oauth.ts +9 -8
  161. package/src/server/messages-native.ts +49 -13
  162. package/src/server/messages-response-headers.ts +14 -0
  163. package/src/server/request-log-conversation.ts +48 -5
  164. package/src/server/request-log.ts +31 -2
  165. package/src/server/responses/adapter-continuation.ts +2 -2
  166. package/src/server/responses/adapter-dispatch.ts +34 -13
  167. package/src/server/responses/codex-ws-exchange.ts +17 -2
  168. package/src/server/responses/codex-ws-pool.ts +100 -13
  169. package/src/server/responses/codex-ws-wire.ts +9 -0
  170. package/src/server/responses/combo-requested-effort.ts +3 -1
  171. package/src/server/responses/core-codex-account.ts +22 -1
  172. package/src/server/responses/core-combo-native.ts +20 -3
  173. package/src/server/responses/core-combo.ts +22 -14
  174. package/src/server/responses/core-lifetime.ts +3 -36
  175. package/src/server/responses/core-options.ts +5 -0
  176. package/src/server/responses/core-replay.ts +73 -0
  177. package/src/server/responses/core.ts +3 -3
  178. package/src/server/responses/fetch-helpers.ts +26 -3
  179. package/src/server/responses/passthrough-delivery.ts +24 -2
  180. package/src/server/responses/passthrough-dispatch.ts +19 -2
  181. package/src/server/responses/request-prepare.ts +4 -0
  182. package/src/server/responses/request-send-budget.ts +112 -49
  183. package/src/server/responses/response-effects.ts +1 -0
  184. package/src/server/responses/run-turn-execution.ts +2 -0
  185. package/src/server/responses/sidecar-execution.ts +25 -16
  186. package/src/server/responses/sidecar-send-budget.ts +58 -0
  187. package/src/server/responses/ws-upstream.ts +19 -6
  188. package/src/server/startup-health-cache.ts +2 -1
  189. package/src/server/workflow-refusal.ts +3 -0
  190. package/src/service/cli.ts +20 -4
  191. package/src/service/desktop-command-guard.ts +32 -0
  192. package/src/service/desktop-startup.ts +66 -40
  193. package/src/service/desktop-supervision.d.mts +26 -0
  194. package/src/service/desktop-supervision.mjs +140 -0
  195. package/src/service/managing-cli.ts +1 -0
  196. package/src/service/orchestration.ts +46 -20
  197. package/src/service/repair.ts +25 -9
  198. package/src/service/state.ts +4 -3
  199. package/src/service/systemd.ts +17 -8
  200. package/src/service/windows-ops.ts +61 -22
  201. package/src/storage/policy-job.ts +5 -1
  202. package/src/storage/policy-worker.ts +3 -1
  203. package/src/storage/policy.ts +9 -0
  204. package/src/types/config.ts +21 -0
  205. package/src/types/request.ts +5 -0
  206. package/src/types/wire.ts +1 -0
  207. package/src/update/index.ts +57 -9
  208. package/src/update/restart-ownership.ts +10 -3
  209. package/src/update/runtime-ownership.d.mts +6 -0
  210. package/src/update/runtime-ownership.mjs +23 -7
  211. package/src/web-search/loop.ts +5 -2
  212. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -0,0 +1,58 @@
1
+ import type { IncomingMeta, ProviderAdapter } from "../../adapters/base";
2
+ import type { HandleResponsesOptions } from "./core-options";
3
+ import type { ResponsesSendBudget } from "./request-send-budget";
4
+ import type { ProviderFetchOptions } from "./fetch-helpers";
5
+ import type { SingleUseDispatchPermit } from "../../lib/request-execution-budget";
6
+
7
+ /** Hosted inference consumes the same child-owned booking as ordinary dispatch. */
8
+ export function createSidecarSendBudget(
9
+ options: HandleResponsesOptions,
10
+ budget: Pick<ResponsesSendBudget, "adapterDispatchBudget" | "noteInitialDispatch" | "noteAdapterPhysicalSend" | "noteAdapterRecoveryWithheld" | "pendingHopPermit">,
11
+ inputTokens: () => number | undefined,
12
+ ) {
13
+ const initial = options.comboInitialSend;
14
+ let hopPermit: SingleUseDispatchPermit | undefined;
15
+ let producerActive = false;
16
+ /** Detach this owner’s hop before refunding its still-unused reservation. */
17
+ const releaseHop = (): void => {
18
+ const permit = hopPermit;
19
+ hopPermit = undefined;
20
+ if (budget.pendingHopPermit === permit) budget.pendingHopPermit = undefined;
21
+ permit?.release();
22
+ };
23
+ return {
24
+ /** Retain a Combo hop until dispatch; direct requests keep eager settlement. */
25
+ ownCredentialHop(permit?: SingleUseDispatchPermit): void {
26
+ if (!initial) { permit?.use(); return; } // Preserve direct callers' reporting contract.
27
+ releaseHop();
28
+ hopPermit = permit;
29
+ budget.pendingHopPermit = permit;
30
+ },
31
+ // Adapter-owned transports settle through the live dispatch view, not an HTTP receipt too.
32
+ incomingMeta: initial ? {
33
+ comboAttempt: options.comboAttempt === true,
34
+ sendBudget: budget.adapterDispatchBudget,
35
+ onPhysicalSend: send => budget.noteAdapterPhysicalSend(inputTokens(), send),
36
+ onRecoveryWithheld: budget.noteAdapterRecoveryWithheld,
37
+ } satisfies Partial<IncomingMeta> : {},
38
+ /** Attach HTTP receipts only when the adapter does not own physical dispatch. */
39
+ fetchOptions(adapter: ProviderAdapter): Pick<ProviderFetchOptions, "onPhysicalDispatch"> {
40
+ return initial && !adapter.fetchResponse && !adapter.runTurn
41
+ ? { onPhysicalDispatch: () => {
42
+ const prepaid = budget.pendingHopPermit;
43
+ budget.pendingHopPermit = undefined;
44
+ budget.noteInitialDispatch(prepaid);
45
+ } } : {};
46
+ },
47
+ /** Transfer cleanup to the asynchronous producer before it can outlive the response. */
48
+ takeProducerOwnership(): void {
49
+ if (initial) { initial.producerOwned = true; producerActive = true; }
50
+ },
51
+ /** Release unused bookings only after the owning producer has settled. */
52
+ release(): void {
53
+ initial?.permit.release(); producerActive = false; releaseHop();
54
+ },
55
+ /** An iteration may finish before runTurn; defer its refund while that producer is active. */
56
+ releaseUnsentHop(): void { if (!producerActive) releaseHop(); },
57
+ };
58
+ }
@@ -15,6 +15,7 @@ import type { NativeResponseControl } from "./native-response-control";
15
15
  // returned event frames as an SSE byte stream, so every downstream consumer
16
16
  // (passthrough relay, adapter parsers, usage sniffing) is unchanged.
17
17
 
18
+ import { codexWsReuseAcrossTurnsEnabled } from "../../config/codex-ws-reuse-setting";
18
19
  import { compareBunVersions } from "../../lib/bun-stream-caps";
19
20
  import { resolveProxyRoute, socks5ProxyFromEnv } from "../../lib/proxy-env";
20
21
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
@@ -26,7 +27,7 @@ import { codexWsCreateFrameExceedsLimit } from "./codex-ws-wire";
26
27
  import { isLoopbackUrl, rewriteWebSocketDial } from "../../plugins/upstream-hooks";
27
28
  export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES, MAX_CODEX_WS_QUEUE_BYTES,
28
29
  MAX_CODEX_WS_CREATE_FRAME_BYTES, CODEX_WS_CREATE_FRAME_LIMIT_BYTES, codexWsCreateFrameExceedsLimit,
29
- isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse } from "./codex-ws-wire";
30
+ isCodexWsQuotaObservedResponse, isCodexWsUpstreamResponse, isCodexWsPreludeProjection } from "./codex-ws-wire";
30
31
  export const MIN_BOUNDED_CODEX_WS_BUN_VERSION = "1.4.0";
31
32
 
32
33
  /**
@@ -99,8 +100,10 @@ export function planCodexWsDial(
99
100
  // and is authoritative; a pooled socket's upgrade copy is intentionally ignored. A rewriter
100
101
  // therefore cannot change them here, and the upgrade keeps the values the frame carries.
101
102
  const dialHeaders = { ...rewritten.headers };
103
+ const reuseAcrossTurns = codexWsReuseAcrossTurnsEnabled();
102
104
  for (const name of CODEX_WS_FRAME_HEADERS) {
103
- if (Object.hasOwn(headers, name)) dialHeaders[name] = headers[name]!;
105
+ if (reuseAcrossTurns) delete dialHeaders[name];
106
+ else if (Object.hasOwn(headers, name)) dialHeaders[name] = headers[name]!;
104
107
  else delete dialHeaders[name];
105
108
  }
106
109
  const dial = { ...rewritten, headers: dialHeaders };
@@ -148,7 +151,8 @@ export function shouldUseCodexWsUpstream(
148
151
  }
149
152
  }
150
153
 
151
- export function codexWsUpstreamFetch(
154
+ /** Select the bounded WS lane or HTTP fallback, forwarding receipts at physical dispatch. */
155
+ export async function codexWsUpstreamFetch(
152
156
  url: string,
153
157
  init: RequestInit,
154
158
  sseFallback: typeof globalThis.fetch,
@@ -157,6 +161,7 @@ export function codexWsUpstreamFetch(
157
161
  beforeDispatch?: (headers: Headers) => void,
158
162
  nativeControl?: NativeResponseControl,
159
163
  beforeContinuation?: () => Promise<void>,
164
+ onPhysicalDispatch?: () => void,
160
165
  ): Promise<Response> {
161
166
  const prepared = prepareCodexWsRequest(url, init);
162
167
  if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
@@ -217,8 +222,16 @@ export function codexWsUpstreamFetch(
217
222
  const dial = planCodexWsDial(wsUrl, headers, proxy);
218
223
  if (!dial) return sseFallback(url, init);
219
224
  const identity = control ? null : codexWsReuseIdentity(url, dial.headers, frameText, dial.proxy, dial.url);
220
- session = (identity ? codexWsPool.acquire(identity, dial.url, dial.headers, dial.proxy) : null)
221
- ?? new CodexWsSession(dial.url, dial.headers, false, undefined, dial.proxy);
225
+ const reuseAcrossTurns = identity !== null && codexWsReuseAcrossTurnsEnabled();
226
+ const pooled = identity
227
+ ? reuseAcrossTurns
228
+ ? await codexWsPool.acquireWaiting(identity, dial.url, dial.headers, dial.proxy, signal)
229
+ : codexWsPool.acquire(identity, dial.url, dial.headers, dial.proxy)
230
+ : null;
231
+ if (reuseAcrossTurns && signal?.aborted) {
232
+ return Promise.reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError"));
233
+ }
234
+ session = pooled ?? new CodexWsSession(dial.url, dial.headers, false, undefined, dial.proxy);
222
235
  if (!session.busy && !session.reserve()) {
223
236
  session.dispose();
224
237
  return sseFallback(url, init);
@@ -229,7 +242,7 @@ export function codexWsUpstreamFetch(
229
242
  return codexWsExchange({
230
243
  session, url, init, prepared, sseFallback, onQuota, beforeDispatch,
231
244
  nativeControl: control,
232
- beforeContinuation,
245
+ beforeContinuation, onPhysicalDispatch,
233
246
  bunVersion: typeof runtime === "string" ? runtime : runtime.version,
234
247
  });
235
248
  }
@@ -88,7 +88,8 @@ export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupH
88
88
  // Mirror deriveStartupHealth's choice: an already-registered service is refreshed in
89
89
  // place. Hardcoding installService here silently undid that for every stale-cache
90
90
  // read, which is the path the dashboard hits while a probe is revalidating.
91
- recommendedCommand: value.routingKind === "opencodex-local" && value.desktop?.owned
91
+ recommendedAction: value.desktop ? "Reopen OpenCodex and check Start at Login." : value.recommendedAction,
92
+ recommendedCommand: value.routingKind === "opencodex-local" && value.desktop !== undefined
92
93
  ? null
93
94
  : value.routingKind === "custom-local" || value.routingKind === "unknown"
94
95
  ? value.commands.restoreNative
@@ -41,6 +41,8 @@ export interface WorkflowRefusalLog {
41
41
  readonly requestId: string;
42
42
  readonly start: number;
43
43
  readonly logCtx: RequestLogContext;
44
+ /** Receiving HTTP route's notification after this refusal's final-log call. */
45
+ readonly onLogged?: () => void;
44
46
  }
45
47
 
46
48
  /**
@@ -84,6 +86,7 @@ export function workflowRefusalResponse(
84
86
  addFinalRequestLog(refusalLog.requestId, refusalLog.start, refusalLog.logCtx, 429, {
85
87
  closeReason: "terminal",
86
88
  });
89
+ refusalLog.onLogged?.();
87
90
  }
88
91
  const refusal = formatErrorResponse(
89
92
  429,
@@ -26,6 +26,7 @@ import type { WindowsSchedulerTaskProbe } from "./windows-scheduler";
26
26
  import { win32 } from "node:path";
27
27
  import { serviceDiagnosticsSummary } from "./diagnostics";
28
28
  import { runServiceClaim } from "./claim";
29
+ import { createSupervisionLatch, desktopServiceCommandRefusal } from "./desktop-command-guard";
29
30
 
30
31
  /**
31
32
  * `restart` is NO LONGER folded into `repair`.
@@ -214,6 +215,7 @@ export function removeServiceTokenAfterUninstall(
214
215
  /** Execute a service verb while preserving client-owned credentials during uninstall. */
215
216
  export async function serviceCommand(...args: (string | undefined)[]): Promise<void> {
216
217
  const filteredArgs = args.filter((a): a is string => Boolean(a));
218
+ const supervisionLatch = createSupervisionLatch();
217
219
  const execute = async (): Promise<void> => {
218
220
  // `claim` is not an install verb: it is deliberately outside planServiceCommand (whose
219
221
  // backend/installation checks do not apply to an ownership write) and outside
@@ -231,6 +233,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
231
233
  process.exit(1);
232
234
  }
233
235
  const { parsed, command } = plan;
236
+ const refusal = desktopServiceCommandRefusal(command, undefined, supervisionLatch);
237
+ if (refusal) {
238
+ console.error(refusal);
239
+ process.exitCode = 1;
240
+ return;
241
+ }
234
242
  if (command === "repair" || command === "restart") {
235
243
  const verb: ServiceRepairVerb = command === "restart" ? "restart" : "repair";
236
244
  assertServiceEnvironmentMatchesInstall();
@@ -249,7 +257,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
249
257
  // existing registration had been restarted, not repaired (#4914).
250
258
  let repairError: unknown;
251
259
  try {
252
- await repairService({ verb });
260
+ await repairService({ verb, supervisionLatch });
253
261
  } catch (error) {
254
262
  repairError = error;
255
263
  process.exitCode = 1;
@@ -295,12 +303,12 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
295
303
  throw new Error(`Task Scheduler state could not be verified before install: ${scheduler.detail}`);
296
304
  }
297
305
  if (scheduler.status === "absent") {
298
- await installFreshWindowsSchedulerSafely();
306
+ await installFreshWindowsSchedulerSafely({ supervisionLatch });
299
307
  } else {
300
- await installServiceSafely(backend, ops.install);
308
+ await installServiceSafely(backend, ops.install, { supervisionLatch });
301
309
  }
302
310
  } else {
303
- await installServiceSafely(backend, ops.install);
311
+ await installServiceSafely(backend, ops.install, { supervisionLatch });
304
312
  }
305
313
  } catch (error) {
306
314
  console.error(`❌ Service install cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -474,6 +482,14 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
474
482
  };
475
483
 
476
484
  const preliminary = parseServiceArgs(filteredArgs);
485
+ if (preliminary.invalid.length === 0) {
486
+ const refusal = desktopServiceCommandRefusal(preliminary.sub, undefined, supervisionLatch);
487
+ if (refusal) {
488
+ console.error(refusal);
489
+ process.exitCode = 1;
490
+ return;
491
+ }
492
+ }
477
493
  const windowsMutation = process.platform === "win32"
478
494
  && preliminary.invalid.length === 0
479
495
  && preliminary.sub !== "status";
@@ -0,0 +1,32 @@
1
+ import { createSupervisionLatch, inspectDesktopSupervision } from "./desktop-supervision.mjs";
2
+
3
+ export { createSupervisionLatch };
4
+ export type DesktopSupervision = ReturnType<typeof inspectDesktopSupervision>;
5
+ export type SupervisionInspector = (deps?: Parameters<typeof inspectDesktopSupervision>[0]) => DesktopSupervision;
6
+ export type SupervisionLatch = ReturnType<typeof createSupervisionLatch>;
7
+
8
+ export function desktopServiceRefusal(evidence: DesktopSupervision, blocked: boolean): string | null {
9
+ if (!blocked) return null;
10
+ const runtime = evidence.kind === "desktop" ? ` (pid ${evidence.runtimePid}, ${evidence.app})` : "";
11
+ return `OpenCodex Desktop supervises the running proxy${runtime}. `
12
+ + "Quit OpenCodex, then rerun 'ocx service install' to move startup management to this CLI.";
13
+ }
14
+
15
+ export function assertNoDesktopSupervision(
16
+ inspect: SupervisionInspector = inspectDesktopSupervision,
17
+ latch: SupervisionLatch = createSupervisionLatch(),
18
+ ): void {
19
+ const evidence = inspect();
20
+ const refusal = desktopServiceRefusal(evidence, latch.observe(evidence));
21
+ if (refusal) throw new Error(refusal);
22
+ }
23
+
24
+ export function desktopServiceCommandRefusal(
25
+ command: string,
26
+ inspect: SupervisionInspector = inspectDesktopSupervision,
27
+ latch: SupervisionLatch = createSupervisionLatch(),
28
+ ): string | null {
29
+ if (!["install", "repair", "start", "restart"].includes(command)) return null;
30
+ const evidence = inspect();
31
+ return desktopServiceRefusal(evidence, latch.observe(evidence));
32
+ }
@@ -1,9 +1,10 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
2
+ import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { readPid } from "../config/process-state";
6
6
  import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
7
+ import { inspectDesktopSupervision, processIdentity, procfs, type ProcReader } from "./desktop-supervision.mjs";
7
8
 
8
9
  export interface DesktopStartupDiagnostic {
9
10
  /** Durable desktop claim; failed supervision does not release ownership. */
@@ -11,11 +12,13 @@ export interface DesktopStartupDiagnostic {
11
12
  loginEnabled: boolean;
12
13
  running: boolean;
13
14
  viable: boolean;
15
+ /** Live parentage when no durable claim exists; never converts supervision into ownership. */
16
+ supervisor?: { supervisorPid: number; runtimePid: number; app: string };
14
17
  }
15
18
 
16
- /** A login item alone is insufficient: the same app must own and supervise this proxy. */
19
+ /** A login item alone is insufficient: the same app must supervise this proxy. */
17
20
  export function deriveDesktopStartup(facts: Omit<DesktopStartupDiagnostic, "viable">): DesktopStartupDiagnostic {
18
- return { ...facts, viable: facts.owned && facts.loginEnabled && facts.running };
21
+ return { ...facts, viable: (facts.owned || facts.supervisor !== undefined) && facts.loginEnabled && facts.running };
19
22
  }
20
23
 
21
24
  function run(command: string, args: string[]): string {
@@ -28,25 +31,12 @@ interface DesktopStartupDeps {
28
31
  uid?: number;
29
32
  ownership?: () => ServiceOwnershipResolution;
30
33
  readPid?: () => number | null;
34
+ readRuntimePortPid?: () => number | null;
31
35
  run?: typeof run;
32
36
  env?: NodeJS.ProcessEnv;
33
37
  proc?: ProcReader;
34
38
  }
35
39
 
36
- /** Linux process identity from procfs: executable target and parent pid. */
37
- interface ProcReader {
38
- exe(pid: number): string;
39
- parent(pid: number): number;
40
- }
41
- const procfs: ProcReader = {
42
- exe: pid => readlinkSync(`/proc/${pid}/exe`),
43
- // comm may contain spaces or parentheses, so fields are counted after the last ')'.
44
- parent: pid => {
45
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
46
- return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
47
- },
48
- };
49
-
50
40
  /** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
51
41
  export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
52
42
  const platform = deps.platform ?? process.platform;
@@ -56,9 +46,47 @@ export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopS
56
46
  ? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
57
47
  }
58
48
 
59
- function processIdentity(pid: number, execute: typeof run): { parent: number; executable: string } | null {
60
- const row = /^(\d+)\s+(.+)$/.exec(execute("/bin/ps", ["-p", String(pid), "-o", "ppid=,comm="]));
61
- return row ? { parent: Number(row[1]), executable: realpathSync(row[2]!) } : null;
49
+ function macLoginRegistration(deps: DesktopStartupDeps) {
50
+ const execute = deps.run ?? run;
51
+ const path = join(deps.home ?? homedir(), "Library", "LaunchAgents", "OpenCodex.plist");
52
+ const plist = JSON.parse(execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]));
53
+ const args = plist.ProgramArguments;
54
+ if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
55
+ || args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
56
+ || !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
57
+ || (plist.Program !== undefined && plist.Program !== args[0])) return null;
58
+ const app = realpathSync(args[0]);
59
+ const proxy = realpathSync(join(dirname(app), "ocx"));
60
+ accessSync(app, constants.X_OK);
61
+ accessSync(proxy, constants.X_OK);
62
+ const domain = `gui/${deps.uid ?? process.getuid!()}`;
63
+ const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
64
+ const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
65
+ const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
66
+ const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
67
+ const loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
68
+ && !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
69
+ && program !== undefined && realpathSync(program) === app
70
+ && loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
71
+ return { app, proxy, loginEnabled };
72
+ }
73
+
74
+ function diagnoseSupervisedStartup(deps: DesktopStartupDeps, ownerRevision: number, loginFor: (app: string) => boolean): DesktopStartupDiagnostic | undefined {
75
+ const evidence = inspectDesktopSupervision(deps);
76
+ if (evidence.kind !== "desktop") return undefined;
77
+ let loginEnabled = false;
78
+ try { loginEnabled = loginFor(evidence.app); }
79
+ catch { /* Login failure revokes restart viability, not the observed live supervisor. */ }
80
+ if ((deps.ownership ?? resolveServiceOwnership)().kind !== "none") return undefined;
81
+ // Login probes can outlive the runtime or its parent; only the same live chain gets credit.
82
+ const final = inspectDesktopSupervision({ ...deps, targetPid: evidence.runtimePid });
83
+ if (final.kind !== "desktop" || final.runtimePid !== evidence.runtimePid
84
+ || final.supervisorPid !== evidence.supervisorPid || final.app !== evidence.app) return undefined;
85
+ // A new claim or revision during the last process probe invalidates the unowned projection.
86
+ const finalOwner = (deps.ownership ?? resolveServiceOwnership)();
87
+ if (finalOwner.kind !== "none" || finalOwner.revision !== ownerRevision) return undefined;
88
+ const { supervisorPid, runtimePid, app } = evidence;
89
+ return deriveDesktopStartup({ owned: false, loginEnabled, running: true, supervisor: { supervisorPid, runtimePid, app } });
62
90
  }
63
91
 
64
92
  /** Read-only macOS desktop ownership, login registration and live parent/child checks. */
@@ -66,6 +94,10 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
66
94
  if ((deps.platform ?? process.platform) !== "darwin") return undefined;
67
95
  const ownership = deps.ownership ?? resolveServiceOwnership;
68
96
  const owner = ownership();
97
+ if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
98
+ const login = macLoginRegistration(deps);
99
+ return login !== null && login.app === app && login.loginEnabled;
100
+ });
69
101
  if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
70
102
  const facts = { owned: true, loginEnabled: false, running: false };
71
103
  const execute = deps.run ?? run;
@@ -74,26 +106,10 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
74
106
  const home = deps.home ?? homedir();
75
107
  const id = readFileSync(join(home, "Library", "Application Support", "com.opencodex.desktop", "install-id"), "utf8").trim();
76
108
  if (id !== owner.ownership.installId) return deriveDesktopStartup(facts);
77
- const path = join(home, "Library", "LaunchAgents", "OpenCodex.plist");
78
- const plist = JSON.parse(execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]));
79
- const args = plist.ProgramArguments;
80
- if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
81
- || args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
82
- || !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
83
- || (plist.Program !== undefined && plist.Program !== args[0])) return deriveDesktopStartup(facts);
84
- const app = realpathSync(args[0]);
85
- const proxy = realpathSync(join(dirname(app), "ocx"));
86
- accessSync(app, constants.X_OK);
87
- accessSync(proxy, constants.X_OK);
88
- const domain = `gui/${deps.uid ?? process.getuid!()}`;
89
- const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
90
- const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
91
- const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
92
- const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
93
- facts.loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
94
- && !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
95
- && program !== undefined && realpathSync(program) === app
96
- && loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
109
+ const login = macLoginRegistration(deps);
110
+ if (!login) return deriveDesktopStartup(facts);
111
+ const { app, proxy } = login;
112
+ facts.loginEnabled = login.loginEnabled;
97
113
  const pid = pidReader();
98
114
  if (pid !== null) {
99
115
  const child = processIdentity(pid, execute);
@@ -135,6 +151,16 @@ export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): Desk
135
151
  if ((deps.platform ?? process.platform) !== "linux") return undefined;
136
152
  const ownership = deps.ownership ?? resolveServiceOwnership;
137
153
  const owner = ownership();
154
+ if (owner.kind === "none") return diagnoseSupervisedStartup(deps, owner.revision, app => {
155
+ const home = deps.home ?? homedir();
156
+ const configured = (deps.env ?? process.env).XDG_CONFIG_HOME;
157
+ const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
158
+ // auto-launch's HOME entry must remain inside the session's XDG autostart search path.
159
+ if (resolve(config) !== resolve(home, ".config")) return false;
160
+ const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
161
+ const login = linuxLoginApp(entry);
162
+ return login !== null && realpathSync(login) === app;
163
+ });
138
164
  if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
139
165
  const facts = { owned: true, loginEnabled: false, running: false };
140
166
  const proc = deps.proc ?? procfs;
@@ -0,0 +1,26 @@
1
+ export type SupervisionEvidence =
2
+ | { kind: "desktop"; runtimePid: number; supervisorPid: number; app: string; proxy: string }
3
+ | { kind: "none" }
4
+ | { kind: "unknown"; reason: string; desktopSeen: boolean }
5
+ | { kind: "unsupported" };
6
+
7
+ export interface ProcReader {
8
+ exe(pid: number): string;
9
+ parent(pid: number): number;
10
+ }
11
+
12
+ export interface DesktopSupervisionDeps {
13
+ targetPid?: number;
14
+ platform?: NodeJS.Platform;
15
+ run?: (command: string, args: string[]) => string;
16
+ proc?: ProcReader;
17
+ readPid?: () => number | null;
18
+ readRuntimePortPid?: () => number | null;
19
+ }
20
+
21
+ export function desktopSupervisionPaths(): { pid: string; runtimePort: string };
22
+ export const procfs: ProcReader;
23
+ export function processIdentity(pid: number, execute?: DesktopSupervisionDeps["run"], onExecutable?: (path: string) => void):
24
+ { parent: number; executable: string } | null;
25
+ export function inspectDesktopSupervision(deps?: DesktopSupervisionDeps): SupervisionEvidence;
26
+ export function createSupervisionLatch(): { observe(evidence: SupervisionEvidence): boolean };
@@ -0,0 +1,140 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+
6
+ // Node >=18 also imports this module from bin/ocx.mjs; do not import TS or Bun APIs.
7
+ function run(command, args) {
8
+ return execFileSync(command, args, {
9
+ encoding: "utf8", timeout: 750, maxBuffer: 128 * 1024, stdio: ["ignore", "pipe", "pipe"],
10
+ }).trim();
11
+ }
12
+
13
+ // Mirror src/config/paths.ts getConfigDir/expandUserPath and process-state.ts paths.
14
+ // Keep this Node-safe copy in sync; the focused startup test pins both implementations.
15
+ export function desktopSupervisionPaths() {
16
+ const raw = process.env.OPENCODEX_HOME?.trim();
17
+ const expanded = raw === "~" ? homedir()
18
+ : raw?.startsWith("~/") || raw?.startsWith("~\\") ? join(homedir(), raw.slice(2)) : raw;
19
+ const home = expanded ? resolve(expanded) : join(homedir(), ".opencodex");
20
+ return { pid: join(home, "ocx.pid"), runtimePort: join(home, "runtime-port.json") };
21
+ }
22
+
23
+ function validPid(pid) {
24
+ return Number.isSafeInteger(pid) && pid > 0;
25
+ }
26
+
27
+ function readState(path, parse) {
28
+ try { return parse(readFileSync(path, "utf8")); }
29
+ catch (error) {
30
+ if (error?.code === "ENOENT") return null;
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ function readPidFile() {
36
+ return readState(desktopSupervisionPaths().pid, raw => {
37
+ const text = raw.trim();
38
+ if (!/^\d+$/.test(text) || !validPid(Number(text))) throw new Error("invalid pid");
39
+ return Number(text);
40
+ });
41
+ }
42
+
43
+ function readRuntimePortPid() {
44
+ return readState(desktopSupervisionPaths().runtimePort, raw => {
45
+ // Only project pid: no credential validation, serialization or returned port state.
46
+ const pid = JSON.parse(raw)?.pid;
47
+ if (!validPid(pid)) throw new Error("invalid runtime pid");
48
+ return pid;
49
+ });
50
+ }
51
+
52
+ /** Linux identity fields count from the end of comm, which may contain spaces or ')'. */
53
+ export const procfs = {
54
+ exe: pid => readlinkSync(`/proc/${pid}/exe`),
55
+ parent: pid => {
56
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
57
+ return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
58
+ },
59
+ };
60
+
61
+ export function processIdentity(pid, execute = run, onExecutable) {
62
+ const text = execute("/bin/ps", ["-p", String(pid), "-o", "ppid=,comm="]).trim();
63
+ const row = /^(\d+)\s+(.+)$/.exec(text);
64
+ if (!row) {
65
+ if (text) throw new Error("invalid process identity");
66
+ return null;
67
+ }
68
+ onExecutable?.(row[2]);
69
+ return { parent: Number(row[1]), executable: realpathSync(row[2]) };
70
+ }
71
+
72
+ /** Live supervision is evidence, never a durable ownership claim. */
73
+ export function inspectDesktopSupervision(deps = {}) {
74
+ const platform = deps.platform ?? process.platform;
75
+ if (platform !== "darwin" && platform !== "linux") return { kind: "unsupported" };
76
+ let desktopSeen = false;
77
+ const unknown = reason => ({ kind: "unknown", reason, desktopSeen });
78
+ const seen = executable => { if (basename(executable) === "opencodex-desktop") desktopSeen = true; };
79
+ const readPid = deps.readPid ?? readPidFile;
80
+ const readPortPid = deps.readRuntimePortPid ?? readRuntimePortPid;
81
+ const proc = deps.proc ?? procfs;
82
+ const execute = deps.run ?? run;
83
+ function identity(pid, parent = false) {
84
+ if (platform === "darwin") return processIdentity(pid, execute, parent ? seen : undefined);
85
+ const executable = proc.exe(pid);
86
+ if (parent) seen(executable);
87
+ const parentPid = proc.parent(pid);
88
+ if (!Number.isSafeInteger(parentPid) || parentPid < 0) throw new Error("invalid parent pid");
89
+ return { parent: parentPid, executable: realpathSync(executable) };
90
+ }
91
+ function capture() {
92
+ const pidFile = readPid();
93
+ const portPid = readPortPid();
94
+ const pids = [deps.targetPid, pidFile, portPid].filter(pid => pid !== undefined && pid !== null);
95
+ if (pids.some(pid => !validPid(pid))) return { error: "invalid-pid" };
96
+ if (pids.some(pid => pid !== pids[0])) return { error: "pid-mismatch" };
97
+ const runtimePid = pids[0] ?? null;
98
+ const child = runtimePid !== null ? identity(runtimePid) : null;
99
+ const parent = child && child.parent > 1 ? identity(child.parent, true) : null;
100
+ const snapshot = { pidFile, portPid, runtimePid, child, parent };
101
+ if (!runtimePid || !child || !parent || basename(parent.executable) !== "opencodex-desktop") {
102
+ return { ...snapshot, kind: "none" };
103
+ }
104
+ seen(parent.executable);
105
+ const app = parent.executable;
106
+ if (platform === "darwin" && (basename(dirname(app)) !== "MacOS"
107
+ || basename(dirname(dirname(app))) !== "Contents")) return { error: "app-mismatch" };
108
+ const proxy = realpathSync(join(dirname(app), "ocx"));
109
+ if (child.executable !== proxy || (platform === "linux"
110
+ && (dirname(proxy) !== dirname(app) || basename(proxy) !== "ocx"))) return { error: "proxy-mismatch" };
111
+ accessSync(app, constants.X_OK);
112
+ accessSync(proxy, constants.X_OK);
113
+ return { ...snapshot, kind: "desktop", app, proxy };
114
+ }
115
+ try {
116
+ const first = capture();
117
+ if (first.error) return unknown(first.error);
118
+ const final = capture();
119
+ if (final.error) return unknown(final.error);
120
+ if (JSON.stringify(first) !== JSON.stringify(final)) return unknown("snapshot-changed");
121
+ return final.kind === "desktop"
122
+ ? { kind: "desktop", runtimePid: final.runtimePid, supervisorPid: final.child.parent, app: final.app, proxy: final.proxy }
123
+ : { kind: "none" };
124
+ } catch {
125
+ // Error messages can contain paths or file contents; expose only the bounded reason.
126
+ return unknown("probe-failed");
127
+ }
128
+ }
129
+
130
+ /** One operation retains its Desktop veto through inconclusive later probes. */
131
+ export function createSupervisionLatch() {
132
+ let blocked = false;
133
+ return {
134
+ observe(evidence) {
135
+ if (evidence.kind === "none") blocked = false;
136
+ else if (evidence.kind === "desktop" || (evidence.kind === "unknown" && evidence.desktopSeen)) blocked = true;
137
+ return blocked;
138
+ },
139
+ };
140
+ }
@@ -81,6 +81,7 @@ function probeVersionOnce(
81
81
  windowsShim ? ["/c", shimCommand] : [...args, "--version"],
82
82
  {
83
83
  timeout: VERSION_PROBE_TIMEOUT_MS, encoding: "utf8", stdio: "pipe",
84
+ env: { ...deps.env, OCX_NO_DESKTOP_HANDOFF: "1" },
84
85
  windowsHide: true, ...(windowsShim ? { windowsVerbatimArguments: true } : {}),
85
86
  },
86
87
  ) as SpawnSyncReturns<string>;