@bitkyc08/opencodex 2.69.0 → 2.70.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 (147) hide show
  1. package/gui/dist/assets/App-_MrbTrBe.js +51 -0
  2. package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
  3. package/gui/dist/assets/index-DKrO2eF3.js +86 -0
  4. package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
  5. package/gui/dist/index.html +1 -1
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-model-contract.ts +119 -0
  8. package/src/adapters/anthropic.ts +14 -83
  9. package/src/adapters/base.ts +2 -0
  10. package/src/adapters/command-code-project-context.ts +542 -0
  11. package/src/adapters/command-code-tool-text.ts +99 -67
  12. package/src/adapters/command-code.ts +5 -1
  13. package/src/adapters/cursor/catalog.ts +11 -0
  14. package/src/adapters/cursor/effort-map.ts +3 -0
  15. package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
  16. package/src/adapters/devin/cloud-direct/chat.ts +78 -48
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
  18. package/src/adapters/devin/context-overflow.ts +75 -0
  19. package/src/adapters/devin/live-models.ts +174 -13
  20. package/src/adapters/devin/reasoning-signature.ts +100 -0
  21. package/src/adapters/devin.ts +310 -117
  22. package/src/adapters/kiro/payload.ts +10 -10
  23. package/src/adapters/kiro-images.ts +58 -7
  24. package/src/adapters/openai-chat/passthrough.ts +3 -1
  25. package/src/adapters/openai-chat.ts +2 -1
  26. package/src/adapters/openai-responses/passthrough.ts +2 -1
  27. package/src/bridge/response-json.ts +15 -1
  28. package/src/bridge/sse.ts +3 -1
  29. package/src/claude/intercept/model-bindings.ts +1 -0
  30. package/src/cli/account-extended.ts +31 -1
  31. package/src/cli/account.ts +3 -0
  32. package/src/cli/capabilities.ts +13 -0
  33. package/src/cli/client-start-fence.ts +36 -0
  34. package/src/cli/dispatch.ts +3 -2
  35. package/src/cli/index.ts +98 -117
  36. package/src/cli/provider-runtime.ts +23 -2
  37. package/src/cli/provider.ts +3 -0
  38. package/src/cli/resolve.ts +18 -0
  39. package/src/cli/start-owner-fence.ts +30 -0
  40. package/src/cli/stop-approval.ts +13 -1
  41. package/src/cli/stop-restore.ts +73 -0
  42. package/src/cli/tray-proxy.ts +221 -18
  43. package/src/cli/version-skew.ts +25 -23
  44. package/src/client/runtime.ts +8 -4
  45. package/src/codex/auth-api/account-list.ts +11 -5
  46. package/src/codex/auth-api/login-flow.ts +5 -3
  47. package/src/codex/auth-api/main-account-probe.ts +186 -110
  48. package/src/codex/auth-api/pool-mode-gate.ts +61 -7
  49. package/src/codex/auth-api/pool-quota-probe.ts +142 -90
  50. package/src/codex/auth-api/reset-credit-service.ts +1 -1
  51. package/src/codex/catalog/gather-capture.ts +1 -0
  52. package/src/codex/catalog/model-hints.ts +3 -3
  53. package/src/codex/catalog/provider-models.ts +2 -0
  54. package/src/codex/low-quota-events.ts +28 -0
  55. package/src/codex/low-quota-observer.ts +23 -0
  56. package/src/codex/low-quota-protection.ts +272 -0
  57. package/src/codex/quota-query-backoff.ts +148 -0
  58. package/src/codex/quota.ts +7 -2
  59. package/src/config/diagnostics.ts +10 -1
  60. package/src/config/load-degrade.ts +17 -0
  61. package/src/config/mutation-lock.ts +3 -4
  62. package/src/config/process-state.ts +1 -1
  63. package/src/config/provider-validation.ts +22 -0
  64. package/src/config/schema/blocked-model-redirects.ts +17 -0
  65. package/src/config/schema/config-schema.ts +4 -1
  66. package/src/config/schema/leaf-validators.ts +33 -2
  67. package/src/config/serving-runtimes.ts +412 -0
  68. package/src/generated/compatibility-version.json +211 -115
  69. package/src/generated/model-metadata.ts +3 -3
  70. package/src/images/loop.ts +3 -0
  71. package/src/lib/errors.ts +2 -1
  72. package/src/lib/handled-signal-exit.ts +23 -0
  73. package/src/lib/request-failure-model.ts +1 -0
  74. package/src/lib/self-launch-argv.ts +9 -0
  75. package/src/lib/shadow-call.ts +5 -2
  76. package/src/lib/strict-semver.ts +26 -0
  77. package/src/lib/transient-service-path.ts +16 -0
  78. package/src/lib/windows-service-wrappers.ts +10 -6
  79. package/src/lib/winsw.ts +3 -1
  80. package/src/oauth/anthropic-model-routes.ts +105 -0
  81. package/src/oauth/anthropic-routing.ts +85 -48
  82. package/src/oauth/devin.ts +135 -5
  83. package/src/oauth/index.ts +20 -3
  84. package/src/oauth/kiro-terminal-failover.ts +23 -9
  85. package/src/oauth/pool-settings-capability.ts +10 -2
  86. package/src/oauth/store.ts +1 -1
  87. package/src/protocols/encoders/adapter-events.ts +3 -1
  88. package/src/providers/github-copilot-context.ts +27 -0
  89. package/src/providers/kiro-models.ts +4 -0
  90. package/src/providers/model-rename-fields.ts +3 -0
  91. package/src/providers/quota/account-cache.ts +8 -3
  92. package/src/providers/quota/devin.ts +286 -0
  93. package/src/providers/quota/vendor-probes-oauth.ts +6 -2
  94. package/src/providers/quota.ts +1 -1
  95. package/src/providers/registry/entries-core.ts +1 -1
  96. package/src/providers/registry/model-seeds.ts +5 -2
  97. package/src/responses/compaction-images.ts +27 -0
  98. package/src/router.ts +104 -16
  99. package/src/routing/analytics.ts +1 -0
  100. package/src/server/auth-cors.ts +9 -1
  101. package/src/server/background-lifecycle.ts +21 -10
  102. package/src/server/chat-completions.ts +2 -1
  103. package/src/server/chat-native.ts +1 -0
  104. package/src/server/index.ts +4 -8
  105. package/src/server/inference/client-encoder-delivery.ts +3 -0
  106. package/src/server/management/context.ts +3 -0
  107. package/src/server/management/low-quota-routes.ts +14 -0
  108. package/src/server/management/oauth-account-routes.ts +21 -1
  109. package/src/server/management/provider-overwrite-carry.ts +79 -6
  110. package/src/server/management/provider-routes.ts +41 -28
  111. package/src/server/management/route-registry.ts +1 -0
  112. package/src/server/management-api.ts +7 -0
  113. package/src/server/responses/adapter-continuation.ts +3 -0
  114. package/src/server/responses/adapter-delivery.ts +3 -0
  115. package/src/server/responses/adapter-dispatch.ts +20 -4
  116. package/src/server/responses/antigravity-validation-refusal.ts +20 -0
  117. package/src/server/responses/core-auth.ts +1 -0
  118. package/src/server/responses/core-codex-account.ts +1 -0
  119. package/src/server/responses/core-normalize.ts +4 -0
  120. package/src/server/responses/passthrough-dispatch.ts +8 -1
  121. package/src/server/responses/request-sidecar-auth.ts +2 -0
  122. package/src/server/responses/request-transport.ts +27 -10
  123. package/src/server/responses/response-effects.ts +4 -1
  124. package/src/server/responses/run-turn-execution.ts +126 -42
  125. package/src/server/responses/sidecar-execution.ts +4 -1
  126. package/src/service/diagnostics.ts +3 -1
  127. package/src/service/guarded-manager-target.ts +39 -12
  128. package/src/service/launchd.ts +14 -6
  129. package/src/service/repair.ts +6 -1
  130. package/src/service/service-child-ownership.ts +168 -0
  131. package/src/service/state.ts +22 -4
  132. package/src/service/systemd.ts +2 -2
  133. package/src/service/windows-guarded-manager.ts +530 -0
  134. package/src/service/windows-taskxml.ts +2 -2
  135. package/src/service.ts +2 -0
  136. package/src/types/config.ts +26 -0
  137. package/src/types/provider.ts +12 -0
  138. package/src/types/request.ts +2 -0
  139. package/src/types.ts +1 -0
  140. package/src/usage/expected-prices.ts +13 -0
  141. package/src/usage/telemetry-contract.ts +1 -0
  142. package/src/vision/anthropic-describe.ts +2 -1
  143. package/src/web-search/anthropic-executor.ts +2 -1
  144. package/src/web-search/loop.ts +1 -0
  145. package/gui/dist/assets/App-DDbgOTh6.js +0 -51
  146. package/gui/dist/assets/index-CuaBJkG4.js +0 -86
  147. package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
@@ -13,9 +13,11 @@ import type { ProviderAdapter, AdapterRequest } from "../../adapters/base";
13
13
  import { releaseProviderRequestSlot, waitForProviderRequestSlot, type ProviderRequestSlot } from "../../providers/request-pacing";
14
14
  import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig, OcxUsage } from "../../types";
15
15
  import type { AnthropicAccountSelectionReason } from "../../oauth/anthropic-routing";
16
+ import { resolveAnthropicModelRoute, routeCandidates, type AnthropicRouteDecision } from "../../oauth/anthropic-model-routes";
16
17
  import {
17
18
  isAnthropicAccountPoolEnabled,
18
19
  getAnthropicPoolAccessSnapshot,
20
+ getEligibleAnthropicAccounts,
19
21
  commitAnthropicSelectionRouting,
20
22
  formatAnthropicProviderForLog,
21
23
  anthropicSessionKeyFromParts,
@@ -118,11 +120,14 @@ export async function prepareResponsesTransport(
118
120
  || route.providerName === "kiro"
119
121
  || route.providerName === "google-antigravity"
120
122
  || route.providerName === "orcarouter-oauth"
123
+ // runTurn transport: the replay runs on the first-event preflight in run-turn-execution.
124
+ || route.providerName === "devin"
121
125
  ) && route.provider.authMode === "oauth";
122
126
  let sentOAuthSnapshot: OAuthAccessSnapshot | undefined;
123
127
  let replayOAuthCredentialSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined;
124
128
  let anthropicPoolAccountId: string | null = null;
125
129
  let anthropicPoolFailovers = 0;
130
+ let anthropicRouteDecision: AnthropicRouteDecision | null = null;
126
131
  // Generic OAuth rotation (#2568) for providers with no pool of their own. Bound to the account
127
132
  // the request actually used, so a concurrent rotation cannot cool an innocent replacement.
128
133
  let genericFailoverAccountId: string | null = null;
@@ -166,6 +171,7 @@ export async function prepareResponsesTransport(
166
171
  ? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
167
172
  : await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
168
173
  }
174
+ if (anthropicRouteDecision && !routeCandidates(getEligibleAnthropicAccounts(), anthropicRouteDecision).includes(candidate.accountId)) return null;
169
175
  const committed = await commitOAuthAccountSelection(route.providerName, candidate.accountId, {
170
176
  expectedSelection: oauthSelection,
171
177
  expectedCredentialGeneration: candidate.generation,
@@ -174,7 +180,7 @@ export async function prepareResponsesTransport(
174
180
  if (committed) {
175
181
  if (route.providerName === "anthropic" && !commitAnthropicSelectionRouting(
176
182
  candidate.accountId, oauthSelection, committed,
177
- { config, sessionKey: anthropicSessionKey, reason: anthropicReason, expectedCredentialGeneration: candidate.generation },
183
+ { config, sessionKey: anthropicSessionKey, reason: anthropicReason, expectedCredentialGeneration: candidate.generation, routeDecision: anthropicRouteDecision },
178
184
  )) return null;
179
185
  oauthSelection = committed;
180
186
  servingOAuthSnapshot = candidate;
@@ -185,8 +191,11 @@ export async function prepareResponsesTransport(
185
191
  // Resolve that choice, not the rejected candidate, before trying admission again.
186
192
  oauthSelection = captureOAuthAccountSelection(route.providerName);
187
193
  if (!oauthSelection) return null;
194
+ const revisedAnthropicId = route.providerName === "anthropic" && anthropicRouteDecision
195
+ ? resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision).accountId : null;
196
+ if (route.providerName === "anthropic" && anthropicRouteDecision && !revisedAnthropicId) return null;
188
197
  candidate = route.providerName === "anthropic"
189
- ? await getAnthropicPoolAccessSnapshot(oauthSelection.accountId)
198
+ ? await getAnthropicPoolAccessSnapshot(revisedAnthropicId ?? oauthSelection.accountId)
190
199
  : await getValidAccessSnapshotForAccount(route.providerName, oauthSelection.accountId, { requireUsableAccount: true });
191
200
  if (route.provider.googleMode === "cloud-code-assist" && !candidate.projectId) return null;
192
201
  }
@@ -194,9 +203,10 @@ export async function prepareResponsesTransport(
194
203
  };
195
204
  const refreshResolvedOAuthSelection = async (sent: OAuthAccessSnapshot): Promise<OAuthAccessSnapshot> => {
196
205
  const current = captureOAuthAccountSelection(route.providerName);
197
- const unchanged = current?.accountId === oauthSelection?.accountId
198
- && current?.revision === oauthSelection?.revision;
199
- const candidate = unchanged ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
206
+ // Keyed on the account, not the selection revision: a selection that moved away and back
207
+ // (A -> B -> A) before the 401 landed still serves the rejected credential, and skipping
208
+ // the refresh would replay it and spend the one recovery attempt.
209
+ const candidate = current?.accountId === sent.accountId ? await forceRefreshOAuthAccessSnapshot(sent) : sent;
200
210
  const admitted = await commitResolvedOAuthSelection(candidate);
201
211
  if (!admitted) throw new Error("OAuth selection changed during credential recovery");
202
212
  if (kiroLoadEnabled && options.accountLoad?.lease?.accountId !== admitted.accountId) {
@@ -508,7 +518,7 @@ export async function prepareResponsesTransport(
508
518
  bindRouteReasoningReplayScope({ parsed: requestParsed, providerName: route.providerName, provider: route.provider,
509
519
  adapterName: nextAdapter.name, oauthCredentialSnapshot: replayOAuthCredentialSnapshot });
510
520
  const rebuilt = await nextAdapter.buildRequest(requestParsed, {
511
- headers: requestState.selectedForwardHeaders, translatorBudget,
521
+ headers: requestState.selectedForwardHeaders, providerName: route.providerName, translatorBudget,
512
522
  ...(imageTierBias > 0 ? { imageTierBias } : {}),
513
523
  });
514
524
  const bodySize = checkOutboundBodySize(rebuilt.body, config.maxUpstreamBodyBytes);
@@ -546,22 +556,28 @@ export async function prepareResponsesTransport(
546
556
  if (route.provider.authMode === "oauth") {
547
557
  try {
548
558
  if (route.providerName === "anthropic" && isAnthropicAccountPoolEnabled(config)) {
549
- const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config);
559
+ const routeResult = resolveAnthropicModelRoute(config, route.modelId);
560
+ if (routeResult.error) return formatErrorResponse(400, "invalid_request_error", `Invalid Anthropic model routes: ${routeResult.error}`);
561
+ anthropicRouteDecision = routeResult.decision;
562
+ const selection = resolveAnthropicAccountForSession(anthropicSessionKey, config, Date.now(), anthropicRouteDecision);
550
563
  if (!selection.accountId) {
564
+ // Route names may resemble account IDs; log only the matched rule position.
565
+ if (anthropicRouteDecision) console.warn(`[anthropic-pool] route:#${anthropicRouteDecision.position} ${selection.reason}; answering locally`);
551
566
  if (selection.reason === "all-cooled") {
552
- const retryAfterSec = getAnthropicPoolRetryAfterSeconds();
567
+ const retryAfterSec = getAnthropicPoolRetryAfterSeconds(Date.now(), anthropicRouteDecision);
553
568
  return formatErrorResponse(
554
569
  429,
555
570
  "rate_limit_error",
556
- "All Anthropic OAuth accounts are temporarily rate-limited",
571
+ anthropicRouteDecision ? "Anthropic OAuth accounts for this model route are temporarily rate-limited" : "All Anthropic OAuth accounts are temporarily rate-limited",
557
572
  retryAfterSec !== null ? { retryAfter: String(retryAfterSec) } : undefined,
558
573
  );
559
574
  }
560
- return formatErrorResponse(401, "authentication_error", "No eligible Anthropic OAuth account available");
575
+ return formatErrorResponse(401, "authentication_error", anthropicRouteDecision ? "No eligible Anthropic OAuth account for this model route" : "No eligible Anthropic OAuth account available");
561
576
  }
562
577
  const admitted = await commitResolvedOAuthSelection(await getAnthropicPoolAccessSnapshot(selection.accountId), true, selection.reason);
563
578
  if (!admitted) return formatErrorResponse(409, "conflict_error", "OAuth account selection changed; retry the request");
564
579
  anthropicPoolAccountId = admitted.accountId;
580
+ if (anthropicRouteDecision) console.info(`[anthropic-pool] route:#${anthropicRouteDecision.position} ${selection.reason}`);
565
581
  route.provider = { ...route.provider, apiKey: admitted.accessToken };
566
582
  logCtx.provider = formatAnthropicProviderForLog("anthropic", admitted.accountId, config);
567
583
  } else {
@@ -837,6 +853,7 @@ export async function prepareResponsesTransport(
837
853
  set replayOAuthCredentialSnapshot(value: Pick<OAuthAccessSnapshot, "accountId" | "generation"> | undefined) {
838
854
  replayOAuthCredentialSnapshot = value;
839
855
  },
856
+ anthropicRouteDecision,
840
857
  get anthropicPoolAccountId(): string | null {
841
858
  return anthropicPoolAccountId;
842
859
  },
@@ -100,7 +100,10 @@ export function createResponsesEffects(
100
100
  if (parsed._rawBody && typeof parsed._rawBody === "object") {
101
101
  delete (parsed._rawBody as Record<string, unknown>).text;
102
102
  }
103
- parsed.context.messages.push({ role: "user", content: COMPACT_PROMPT, timestamp: Date.now() });
103
+ parsed.context.messages = [
104
+ ...parsed.context.messages,
105
+ { role: "user", content: COMPACT_PROMPT, timestamp: Date.now() },
106
+ ];
104
107
  }
105
108
 
106
109
  let routedNamespaceToolAliases: RoutedNamespaceToolAliases = new Map();
@@ -27,6 +27,8 @@ import {
27
27
  rotateGenericOAuthAccountOn429,
28
28
  failoverAccountSnapshot,
29
29
  } from "../../oauth/generic-account-failover";
30
+ import { OAuthAccountPausedError, publicOAuthAuthenticationErrorMessage, type OAuthAccessSnapshot } from "../../oauth/index";
31
+ import { tryAlternateAfterTerminalRefresh } from "../../oauth/kiro-terminal-failover";
30
32
  import { resolveWireProtocolOverride } from "../adapter-resolve";
31
33
  import { formatErrorResponse, bridgeToResponsesSSE, buildResponseJSON } from "../../bridge";
32
34
  import { redactSecretString } from "../../lib/redact";
@@ -93,6 +95,9 @@ export async function executeResponsesRunTurn(
93
95
  | "genericFailovers"
94
96
  | "genericFailoverLimit"
95
97
  | "applyFailoverSnapshot"
98
+ | "refreshResolvedOAuthSelection"
99
+ | "isOAuth401ReplayProvider"
100
+ | "sentOAuthSnapshot"
96
101
  | "resolveSelectionAdapter"
97
102
  | "adapter"
98
103
  | "noteRoutedAttemptSend"
@@ -122,6 +127,7 @@ export async function executeResponsesRunTurn(
122
127
  adapterBindings,
123
128
  refreshRunTurnAdapter,
124
129
  applyFailoverSnapshot,
130
+ refreshResolvedOAuthSelection,
125
131
  resolveSelectionAdapter,
126
132
  } = transportState;
127
133
  const {
@@ -348,6 +354,99 @@ export async function executeResponsesRunTurn(
348
354
  yield* await preflightRunTurnFailover(stream, iterParsed);
349
355
  })();
350
356
  };
357
+ // Rebind the turn to an admitted account. The failed attempt emitted no client-visible bytes,
358
+ // so replay is safe, but a Cursor conversation/checkpoint is credential-scoped: carrying its
359
+ // account identity into the next account would not be. The rotated adapter derives its own.
360
+ const adoptRunTurnAccount = (admittedSnapshot: Pick<OAuthAccessSnapshot, "accountId" | "generation">): boolean => {
361
+ parsed._cursorIdentityScope = undefined;
362
+ parsed._cursorConversationId = undefined;
363
+ if (parsed._providerContinuation?.cursor) {
364
+ const { cursor: _discardedCursor, ...otherProviderState } = parsed._providerContinuation;
365
+ parsed._providerContinuation = otherProviderState;
366
+ }
367
+ const rotatedProvider = resolveWireProtocolOverride(
368
+ route.providerName,
369
+ route.modelId,
370
+ route.provider,
371
+ inboundWire,
372
+ route.staticPolicy,
373
+ );
374
+ const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
375
+ if (!rotatedAdapter.runTurn) return false;
376
+ transportState.runTurnAdapter = rotatedAdapter;
377
+ bindRouteReasoningReplayScope({
378
+ parsed,
379
+ providerName: route.providerName,
380
+ provider: rotatedProvider,
381
+ adapterName: rotatedAdapter.name,
382
+ oauthCredentialSnapshot: {
383
+ accountId: admittedSnapshot.accountId,
384
+ generation: admittedSnapshot.generation,
385
+ },
386
+ codexAuthContext: admissionState.authCtx,
387
+ forwardHeaders: requestState.selectedForwardHeaders,
388
+ });
389
+ sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
390
+ recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name);
391
+ return true;
392
+ };
393
+ // The runTurn half of the upstream-401 replay adapter-dispatch runs for HTTP transports:
394
+ // force-refresh the credential that was sent, once per request. A refresh that cannot
395
+ // succeed marks the account needsReauth inside the OAuth owner, so the account stops being
396
+ // selected; the turn then moves to a surviving stored account, or, with none, the client
397
+ // gets the login instruction instead of an opaque upstream 401.
398
+ let oauth401ReplayAttempted = false;
399
+ let pausedAuthRecovery = false;
400
+ const recoverRunTurnAdapterOnPreflight401 = async (
401
+ error: Extract<AdapterEvent, { type: "error" }>,
402
+ ): Promise<boolean> => {
403
+ const sent = transportState.sentOAuthSnapshot;
404
+ if (error.status !== 401 || !transportState.isOAuth401ReplayProvider || !sent || oauth401ReplayAttempted) return false;
405
+ oauth401ReplayAttempted = true;
406
+ const hop = reserveCredentialHop("auth-recovery", `${route.providerName}|${route.modelId}|runturn-oauth-401`);
407
+ if (!hop.allowed) return false;
408
+ try {
409
+ let admitted: OAuthAccessSnapshot | null;
410
+ try {
411
+ admitted = await applyFailoverSnapshot(await refreshResolvedOAuthSelection(sent));
412
+ } catch (err) {
413
+ if (err instanceof OAuthAccountPausedError) {
414
+ pausedAuthRecovery = true;
415
+ Object.assign(error, { status: 403, errorType: "permission_error", message: publicOAuthAuthenticationErrorMessage(err) });
416
+ hop.permit?.release();
417
+ return false;
418
+ }
419
+ // Not only OAuthLoginRequiredError: a concurrent request that already flagged this
420
+ // account and moved the selection makes this refresh fail as "selection changed". The
421
+ // helper still requires the sent generation to be flagged needsReauth, so it is safe.
422
+ const alternate = transportState.genericFailovers < transportState.genericFailoverLimit
423
+ ? await tryAlternateAfterTerminalRefresh(config, route.providerName, sent.accountId, sent.generation)
424
+ : null;
425
+ admitted = alternate ? await applyFailoverSnapshot(alternate) : null;
426
+ if (admitted) transportState.genericFailovers += 1;
427
+ else Object.assign(error, { errorType: "authentication_error", message: publicOAuthAuthenticationErrorMessage(err) });
428
+ }
429
+ if (!admitted || !adoptRunTurnAccount(admitted)) {
430
+ hop.permit?.release();
431
+ return false;
432
+ }
433
+ sendBudgetState.pendingHopPermit = hop.permit;
434
+ return true;
435
+ } catch (err) {
436
+ // Applying the alternate can still fail, e.g. when a newer manual selection points back
437
+ // at the flagged account; the client then needs the login instruction, not the raw 401.
438
+ Object.assign(error, { errorType: "authentication_error", message: publicOAuthAuthenticationErrorMessage(err) });
439
+ hop.permit?.release();
440
+ return false;
441
+ }
442
+ };
443
+ const recoverRunTurnAdapterOnPreflightError = async (
444
+ error: Extract<AdapterEvent, { type: "error" }>,
445
+ ): Promise<AttemptRecoveryKind | undefined> => {
446
+ if (await recoverRunTurnAdapterOnPreflight401(error)) return "oauth-401";
447
+ if (await rotateRunTurnAdapterOnPreflight429(error)) return "oauth-account-429";
448
+ return undefined;
449
+ };
351
450
  const rotateRunTurnAdapterOnPreflight429 = async (
352
451
  error: Extract<AdapterEvent, { type: "error" }>,
353
452
  ): Promise<boolean> => {
@@ -400,42 +499,10 @@ export async function executeResponsesRunTurn(
400
499
  hop.permit?.release();
401
500
  return false;
402
501
  }
403
- // A Cursor conversation/checkpoint is credential-scoped. The failed attempt emitted no
404
- // client-visible bytes, so replay is safe, but carrying its account identity into the next
405
- // account would not be. Let the rotated adapter derive a fresh identity and conversation.
406
- parsed._cursorIdentityScope = undefined;
407
- parsed._cursorConversationId = undefined;
408
- if (parsed._providerContinuation?.cursor) {
409
- const { cursor: _discardedCursor, ...otherProviderState } = parsed._providerContinuation;
410
- parsed._providerContinuation = otherProviderState;
411
- }
412
- const rotatedProvider = resolveWireProtocolOverride(
413
- route.providerName,
414
- route.modelId,
415
- route.provider,
416
- inboundWire,
417
- route.staticPolicy,
418
- );
419
- const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
420
- if (!rotatedAdapter.runTurn) {
502
+ if (!adoptRunTurnAccount(admittedSnapshot)) {
421
503
  hop.permit?.release();
422
504
  return false;
423
505
  }
424
- transportState.runTurnAdapter = rotatedAdapter;
425
- bindRouteReasoningReplayScope({
426
- parsed,
427
- providerName: route.providerName,
428
- provider: rotatedProvider,
429
- adapterName: rotatedAdapter.name,
430
- oauthCredentialSnapshot: {
431
- accountId: admittedSnapshot.accountId,
432
- generation: admittedSnapshot.generation,
433
- },
434
- codexAuthContext: admissionState.authCtx,
435
- forwardHeaders: requestState.selectedForwardHeaders,
436
- });
437
- sealRequestAttemptIdentity(logCtx.activeAttempt, logCtx.provider, rotatedAdapter.name, logCtx.accountLogLabel);
438
- recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, rotatedAdapter.name);
439
506
  // The caller replays the turn on this rotation, and a runTurn adapter dispatches through
440
507
  // its own reservation ladder -- Cursor reserves once per physical send. Confirming here
441
508
  // would leave that ladder to charge the same replay a second time (#4709), so hand the
@@ -463,13 +530,14 @@ export async function executeResponsesRunTurn(
463
530
  yield event;
464
531
  continue;
465
532
  }
466
- if (!firstMeaningfulSeen && !replayUnsafe && event.type === "error"
467
- && await rotateRunTurnAdapterOnPreflight429(event)) {
533
+ const recovery = !firstMeaningfulSeen && !replayUnsafe && event.type === "error"
534
+ ? await recoverRunTurnAdapterOnPreflightError(event) : undefined;
535
+ if (recovery) {
468
536
  const retryQueue = createAdapterEventQueue({
469
537
  onBacklogExceeded: () => runTurnAbort.abort(),
470
538
  });
471
539
  const pendingPermit = sendBudgetState.pendingHopPermit;
472
- const retryAttempt = runTurnAttempt(retryQueue, "oauth-account-429", false, replayParsed);
540
+ const retryAttempt = runTurnAttempt(retryQueue, recovery, false, replayParsed);
473
541
  if (pendingPermit) {
474
542
  const releaseIfUnclaimed = () => {
475
543
  if (sendBudgetState.pendingHopPermit !== pendingPermit) return;
@@ -522,15 +590,13 @@ export async function executeResponsesRunTurn(
522
590
  return streamAfterPreflight(preflight.stream, replayParsed, preflight.replayUnsafe);
523
591
  }
524
592
  if (preflight.ready) return streamAfterPreflight(preflight.stream, replayParsed, preflight.replayUnsafe);
525
- if (preflight.replayUnsafe
526
- || !preflight.error
527
- || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
528
- return preflight.stream;
529
- }
593
+ const recovery = preflight.replayUnsafe || !preflight.error
594
+ ? undefined : await recoverRunTurnAdapterOnPreflightError(preflight.error);
595
+ if (!recovery) return preflight.stream;
530
596
  const retryQueue = createAdapterEventQueue({
531
597
  onBacklogExceeded: () => runTurnAbort.abort(),
532
598
  });
533
- latestRetryAttempt = runTurnAttempt(retryQueue, "oauth-account-429", false, replayParsed);
599
+ latestRetryAttempt = runTurnAttempt(retryQueue, recovery, false, replayParsed);
534
600
  void latestRetryAttempt;
535
601
  source = retryQueue.stream();
536
602
  }
@@ -599,6 +665,14 @@ export async function executeResponsesRunTurn(
599
665
  // Preflight holds only heartbeats and the first meaningful event. A first-event 429 can be
600
666
  // replayed transparently; after any output reaches the bridge, a later error stays terminal.
601
667
  eventSource = await preflightRunTurnFailover(eventSource, wsFirstParsed, preflightDeadlineAt);
668
+ if (pausedAuthRecovery) {
669
+ cancelResponseCompletion();
670
+ runTurnAbort.abort();
671
+ queue.close();
672
+ cleanupRunTurnAbort();
673
+ releaseSearchProbeLease();
674
+ return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(new OAuthAccountPausedError()));
675
+ }
602
676
  }
603
677
  if (grokDevinPreflight) {
604
678
  const preflight = await preflightAdapterEvents(eventSource, undefined, {
@@ -672,6 +746,7 @@ export async function executeResponsesRunTurn(
672
746
  ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
673
747
  stallTimeoutSec,
674
748
  hideThinkingSummary: parsed.options.hideThinkingSummary,
749
+ hideRawReasoning: parsed.options.hideRawReasoning,
675
750
  declaredToolNames,
676
751
  bareCustomToolNames,
677
752
  enforceDeclaredToolNames,
@@ -729,6 +804,14 @@ export async function executeResponsesRunTurn(
729
804
  for await (const event of await preflightRunTurnFailover(
730
805
  (async function* () { yield* firstAttemptEvents; })(),
731
806
  )) runTurnEvents.push(event);
807
+ if (pausedAuthRecovery) {
808
+ cancelResponseCompletion();
809
+ runTurnAbort.abort();
810
+ queue.close();
811
+ cleanupRunTurnAbort();
812
+ releaseSearchProbeLease();
813
+ return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(new OAuthAccountPausedError()));
814
+ }
732
815
  }
733
816
  if (grokDevinPreflight) {
734
817
  const preflight = await preflightAdapterEvents(
@@ -811,6 +894,7 @@ export async function executeResponsesRunTurn(
811
894
  translatorBudget,
812
895
  replayCacheScope: parsed._reasoningReplayScope,
813
896
  hideThinkingSummary: parsed.options.hideThinkingSummary,
897
+ hideRawReasoning: parsed.options.hideRawReasoning,
814
898
  toolNsMap,
815
899
  declaredToolNames,
816
900
  enforceDeclaredToolNames,
@@ -67,6 +67,7 @@ export async function executeResponsesSidecars(
67
67
  | "genericFailoverLimit"
68
68
  | "replayOAuthCredentialSnapshot"
69
69
  | "applyFailoverSnapshot"
70
+ | "anthropicRouteDecision"
70
71
  | "anthropicPoolAccountId"
71
72
  | "anthropicPoolFailovers"
72
73
  | "anthropicSessionKey"
@@ -275,6 +276,7 @@ export async function executeResponsesSidecars(
275
276
  anthropicSessionKey,
276
277
  Date.now(),
277
278
  responseHeaders,
279
+ transportState.anthropicRouteDecision,
278
280
  );
279
281
  if (!nextAccountId) {
280
282
  hop.permit?.release();
@@ -385,7 +387,7 @@ export async function executeResponsesSidecars(
385
387
  );
386
388
  const imgResponse = await runWithImageBridge({
387
389
  parsed, adapter: transportState.adapter,
388
- incomingMeta: { headers: requestState.selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget },
390
+ incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget },
389
391
  ...(imgPlan ? { plan: imgPlan } : {}),
390
392
  ...(vidPlan ? { videoPlan: vidPlan } : {}),
391
393
  forwardHeaders: requestState.selectedForwardHeaders,
@@ -477,6 +479,7 @@ export async function executeResponsesSidecars(
477
479
  }),
478
480
  incomingMeta: {
479
481
  headers: requestState.selectedForwardHeaders,
482
+ providerName: route.providerName,
480
483
  abortSignal: options.abortSignal,
481
484
  translatorBudget,
482
485
  providerFetch: routedProviderFetch,
@@ -5,7 +5,7 @@ import { sh } from "./guards";
5
5
  import { installedServiceListenPort, confirmServiceServing } from "./health";
6
6
  import { expectedLaunchdCommand, launchdJobMatchesPlist, probeLaunchdLoadState } from "./launchd";
7
7
  import type { LaunchdLoadProbe } from "./launchd";
8
- import { plistPath, windowsServiceScriptPath, windowsLauncherVbsPath, windowsTaskXmlPath, readServiceInstallState } from "./state";
8
+ import { plistPath, windowsServiceScriptPath, windowsLauncherVbsPath, windowsTaskXmlPath, readServiceInstallState, serviceLauncherPathDiagnostic } from "./state";
9
9
  import type { ServiceBackend } from "./state";
10
10
  import { unitPath, isSystemd } from "./systemd";
11
11
  import { statusWindowsXml } from "./windows-ops";
@@ -23,6 +23,8 @@ import { LABEL, TASK, serviceLogPath } from "./state";
23
23
  */
24
24
  export function bakedServicePathsDiagnostic(platform: NodeJS.Platform = process.platform): string | null {
25
25
  const state = readServiceInstallState();
26
+ const temporaryLauncher = serviceLauncherPathDiagnostic(state, platform);
27
+ if (temporaryLauncher) return temporaryLauncher;
26
28
  // A launcher install runs the launcher, not the baked pair, so the pair's existence says
27
29
  // nothing about whether the service can start. Judging the recorded launcher is both
28
30
  // necessary (a deleted launcher IS stale) and sufficient (a replaced version directory
@@ -6,12 +6,30 @@ import { statusWinswRaw } from "../lib/winsw";
6
6
  import { systemdProperty } from "../service-manager-probe";
7
7
  import { expectedLaunchdCommand, launchdEvictionTargets, probeLaunchdLoadState, runLaunchctl } from "./launchd";
8
8
  import { probeSystemdUnitInactive, unitPath } from "./systemd";
9
+ import {
10
+ inspectWindowsGuardedManager,
11
+ observeWindowsGuardedManagerStopped,
12
+ type WindowsGuardedManagerDeps,
13
+ } from "./windows-guarded-manager";
9
14
  import { probeWindowsSchedulerTask } from "./windows-scheduler";
10
15
  import { TASK } from "./state";
11
16
 
12
17
  export type GuardedManagerTarget =
13
18
  | { kind: "absent" }
14
- | { kind: "bound"; pid: number; managerPid: number; backend: "launchd" | "systemd" }
19
+ | {
20
+ kind: "bound";
21
+ pid: number;
22
+ managerPid: number;
23
+ backend: "launchd" | "systemd" | "scheduler" | "winsw";
24
+ /**
25
+ * Whether stopping this manager can leave the approved process alive.
26
+ * launchd/systemd end their job's whole process tree; Windows Task
27
+ * Scheduler's /end does not reliably cascade to the wrapper's child (#764),
28
+ * so a bound scheduler or WinSW manager still needs the approved PID
29
+ * signalled separately after the manager is stopped.
30
+ */
31
+ childNeedsSeparateStop: boolean;
32
+ }
15
33
  | { kind: "unknown"; reason: string };
16
34
 
17
35
  export type GuardedManagerStopped = "inactive" | "active" | "unknown";
@@ -59,6 +77,7 @@ export interface GuardedManagerDeps {
59
77
  verifyPid?: (pid: number) => number | null;
60
78
  scheduler?: typeof probeWindowsSchedulerTask;
61
79
  winsw?: typeof statusWinswRaw;
80
+ win?: WindowsGuardedManagerDeps;
62
81
  }
63
82
 
64
83
  /** Only one current manager with a verified parent chain may be stopped by name. */
@@ -92,7 +111,7 @@ export function inspectGuardedManagerTarget(
92
111
  } catch { return { kind: "unknown", reason: "launchd state could not be verified" }; }
93
112
  if (managerPid === null) return { kind: "absent" };
94
113
  return managerOwnsApprovedPid(managerPid, approvedPid, parentOf)
95
- ? { kind: "bound", pid: approvedPid, managerPid, backend: "launchd" }
114
+ ? { kind: "bound", pid: approvedPid, managerPid, backend: "launchd", childNeedsSeparateStop: false }
96
115
  : { kind: "unknown", reason: "launchd job does not own the approved process" };
97
116
  }
98
117
  if (platform === "linux") {
@@ -111,16 +130,19 @@ export function inspectGuardedManagerTarget(
111
130
  return { kind: "unknown", reason: "systemd unit state is not current and active" };
112
131
  }
113
132
  return managerOwnsApprovedPid(pid, approvedPid, parentOf)
114
- ? { kind: "bound", pid: approvedPid, managerPid: pid, backend: "systemd" }
133
+ ? { kind: "bound", pid: approvedPid, managerPid: pid, backend: "systemd", childNeedsSeparateStop: false }
115
134
  : { kind: "unknown", reason: "systemd unit does not own the approved process" };
116
135
  } catch { return { kind: "unknown", reason: "systemd state could not be verified" }; }
117
136
  }
118
137
  if (platform === "win32") {
119
138
  const scheduler = (deps.scheduler ?? probeWindowsSchedulerTask)();
120
139
  const native = (deps.winsw ?? statusWinswRaw)();
121
- return scheduler.status === "absent" && native === "nonexistent"
122
- ? { kind: "absent" }
123
- : { kind: "unknown", reason: "Windows manager child PID cannot be proven" };
140
+ // A simulated win32 (deps.platform injected) must not run real schtasks or
141
+ // process-list queries at the host; the module degrades every unset probe to
142
+ // its least-proving answer then, which is exactly the old fail-closed shape.
143
+ return inspectWindowsGuardedManager(
144
+ approvedPid, scheduler, native, deps.win ?? {}, deps.platform === undefined,
145
+ );
124
146
  }
125
147
  return { kind: "unknown", reason: "unsupported service manager platform" };
126
148
  }
@@ -128,7 +150,7 @@ export function inspectGuardedManagerTarget(
128
150
  /** A guarded success needs manager absence after settlement and once more at publication. */
129
151
  export async function observeGuardedManagerStopped(
130
152
  manager: Exclude<GuardedManagerTarget, { kind: "unknown" }>,
131
- deps: Pick<GuardedManagerDeps, "platform" | "scheduler" | "winsw"> & {
153
+ deps: Pick<GuardedManagerDeps, "platform" | "scheduler" | "winsw" | "win"> & {
132
154
  launchd?: typeof probeLaunchdLoadState;
133
155
  systemd?: typeof probeSystemdUnitInactive;
134
156
  } = {},
@@ -140,11 +162,16 @@ export async function observeGuardedManagerStopped(
140
162
  return state === "not-loaded" ? "inactive" : state === "unknown" ? "unknown" : "active";
141
163
  }
142
164
  if (platform === "linux") return (deps.systemd ?? probeSystemdUnitInactive)();
143
- if (platform === "win32" && manager.kind === "absent") {
144
- const scheduler = (deps.scheduler ?? probeWindowsSchedulerTask)();
145
- const native = (deps.winsw ?? statusWinswRaw)();
146
- if (scheduler.status === "absent" && native === "nonexistent") return "inactive";
147
- return scheduler.status === "unknown" || native === "unknown" ? "unknown" : "active";
165
+ if (platform === "win32") {
166
+ // Registration presence is not the question: schtasks /end leaves the task
167
+ // registered forever, so the Windows check is RUNNING state + surviving
168
+ // wrapper processes + WinSW status, never mere presence.
169
+ return observeWindowsGuardedManagerStopped({
170
+ scheduler: deps.scheduler,
171
+ winsw: deps.winsw,
172
+ ...(deps.win ?? {}),
173
+ ...(manager.kind === "bound" ? { formerManagerPid: manager.managerPid } : {}),
174
+ }, deps.platform === undefined);
148
175
  }
149
176
  } catch { return "unknown"; }
150
177
  return "unknown";
@@ -12,7 +12,7 @@ import { serviceApiTokenFilePath } from "../lib/service-secrets";
12
12
  import { recordOwnedConfigPath } from "../lib/config-ownership";
13
13
  import { writeServiceApiTokenFile, assertLiveServiceManagerAllowed } from "./guards";
14
14
  import { resolveServiceListenPort, buildServiceShellCommand, buildServiceLauncherShellCommand, installedServiceListenPort, resolvedProxyEnv } from "./health";
15
- import { SERVICE_MANAGED_ENV, LABEL, cliEntry, logPath, serviceStatePath, currentCodexSqliteHomeAbsolute, type ServiceInstallState, writeServiceInstallState, readServiceInstallState } from "./state";
15
+ import { SERVICE_MANAGED_ENV, LABEL, cliEntry, filterTransientServicePath, logPath, serviceStatePath, currentCodexSqliteHomeAbsolute, type ServiceInstallState, writeServiceInstallState, readServiceInstallState } from "./state";
16
16
  import { writeServiceDefinitionFile } from "./windows-ops";
17
17
  import { readTextOrNull } from "./windows-taskxml";
18
18
 
@@ -39,7 +39,7 @@ export function buildPlist(
39
39
  const runtime = deps.runtime ?? durableBunRuntime();
40
40
  const { bun, bunRuntimeSource, cli } = cliEntry(runtime);
41
41
  const log = logPath();
42
- const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
42
+ const path = filterTransientServicePath(process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", ":", "darwin");
43
43
  const codexHome = process.env.CODEX_HOME?.trim();
44
44
  const codexSqliteHome = currentCodexSqliteHomeAbsolute();
45
45
  const opencodexHome = process.env.OPENCODEX_HOME?.trim();
@@ -72,7 +72,7 @@ export function buildPlist(
72
72
  <string>${plistString(command)}</string>
73
73
  </array>
74
74
  <key>RunAtLoad</key><true/>
75
- <key>KeepAlive</key><true/>
75
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
76
76
  <key>EnvironmentVariables</key>
77
77
  <dict>
78
78
  ${envLines}
@@ -90,6 +90,11 @@ ${envLines}
90
90
  */
91
91
  const PLIST_PATH_ENTRY = /^(\s*<key>PATH<\/key><string>)([^\n]*)(<\/string>)$/m;
92
92
 
93
+ function filterPlistPathVariable(plist: string): string {
94
+ return plist.replace(PLIST_PATH_ENTRY, (_match, open: string, value: string, close: string) =>
95
+ `${open}${filterTransientServicePath(value, ":", "darwin")}${close}`);
96
+ }
97
+
93
98
  /**
94
99
  * The rendered plist with the PREVIOUS definition's `PATH` put back — or null when `PATH`
95
100
  * is not the only difference.
@@ -114,14 +119,17 @@ const PLIST_PATH_ENTRY = /^(\s*<key>PATH<\/key><string>)([^\n]*)(<\/string>)$/m;
114
119
  * re-install still updates it.
115
120
  */
116
121
  export function reusePreviousPlistPathVariable(previous: string, rendered: string): string | null {
122
+ if (previous === rendered) return null;
117
123
  const prev = PLIST_PATH_ENTRY.exec(previous);
118
124
  const next = PLIST_PATH_ENTRY.exec(rendered);
119
- if (!prev || !next || prev[2] === next[2]) return null;
125
+ if (!prev || !next) return null;
126
+ const normalizedPrevious = filterPlistPathVariable(previous);
127
+ const previousPath = filterTransientServicePath(prev[2] ?? "", ":", "darwin");
120
128
  // A function replacer, not a `$1` template: a PATH entry containing `$&` or `$1` would
121
129
  // otherwise be re-expanded into the file.
122
130
  const adopted = rendered.replace(PLIST_PATH_ENTRY, (_match, open: string, _value: string, close: string) =>
123
- `${open}${prev[2] ?? ""}${close}`);
124
- return adopted === previous ? adopted : null;
131
+ `${open}${previousPath}${close}`);
132
+ return adopted === normalizedPrevious ? normalizedPrevious : null;
125
133
  }
126
134
 
127
135
  /**
@@ -5,7 +5,7 @@ import type { ServiceDiagnostic } from "./diagnostics";
5
5
  import { assertServiceEnvironmentMatchesInstall, assertServiceAuthEnvironment } from "./guards";
6
6
  import { installLaunchd, restartLaunchdJob } from "./launchd";
7
7
  import type { LaunchdInstallOutcome } from "./launchd";
8
- import { TASK, writeServiceInstallState, serviceSourceDir, resolveServiceOwnership } from "./state";
8
+ import { TASK, writeServiceInstallState, serviceSourceDir, resolveServiceOwnership, serviceLauncherPathDiagnostic } from "./state";
9
9
  import type { ServiceOwnership, ServiceOwnershipResolution } from "./state";
10
10
  import { installSystemd } from "./systemd";
11
11
  import { writeWindowsSchedulerAssets, reregisterWindowsSchedulerTask, windowsSchedulerRegistrationMatchesSnapshot, restoreWindowsSchedulerTaskIfAbsent, startWindows, stopWindows, statusWindowsXml } from "./windows-ops";
@@ -62,6 +62,8 @@ export interface RepairServiceDeps {
62
62
  schedulerLauncher?: string;
63
63
  /** Test seam — defaults to process.platform so Linux CI cannot hit real installSystemd. */
64
64
  platform?: NodeJS.Platform;
65
+ /** Test seam for a recorded temporary launcher. */
66
+ launcherPathDiagnostic?: () => string | null;
65
67
  }
66
68
 
67
69
  /**
@@ -175,6 +177,9 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
175
177
  (deps.assertEnv ?? assertServiceEnvironmentMatchesInstall)();
176
178
  (deps.assertAuth ?? assertServiceAuthEnvironment)();
177
179
 
180
+ const temporaryLauncher = (deps.launcherPathDiagnostic ?? (() => serviceLauncherPathDiagnostic(undefined, platform, true)))();
181
+ if (temporaryLauncher) console.warn(`⚠️ ${temporaryLauncher}`);
182
+
178
183
  if (platform === "win32") {
179
184
  if (diag.backend === "native") {
180
185
  await (deps.repairNative ?? (() => installWinswService(defaultWinswEntry(serviceSourceDir))))();