@bitkyc08/opencodex 2.68.0 → 2.69.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 (175) hide show
  1. package/bin/ocx.mjs +25 -17
  2. package/gui/dist/assets/App-DDbgOTh6.js +51 -0
  3. package/gui/dist/assets/{Tray-Bn8WhKBH.js → Tray-uqKRqfvw.js} +1 -1
  4. package/gui/dist/assets/index-BF9QnKO_.css +1 -0
  5. package/gui/dist/assets/index-CuaBJkG4.js +86 -0
  6. package/gui/dist/assets/{quota-summary-DL-bNuNL.js → quota-summary-COppQSVq.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/factory-droid.svg +8 -0
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-output-schema.ts +109 -11
  11. package/src/adapters/coding-agent/protocol.ts +106 -11
  12. package/src/adapters/coding-agent/turn.ts +23 -18
  13. package/src/adapters/google-errors.ts +16 -1
  14. package/src/adapters/google-http.ts +14 -0
  15. package/src/adapters/openai-chat/passthrough.ts +2 -0
  16. package/src/adapters/openai-chat/summary-budget.ts +72 -0
  17. package/src/adapters/openai-chat/tool-call-id-remint.ts +11 -2
  18. package/src/adapters/openai-chat.ts +6 -9
  19. package/src/bridge/response-json.ts +3 -1
  20. package/src/bridge/sse.ts +3 -1
  21. package/src/claude/desktop-picker.ts +6 -2
  22. package/src/claude/desktop-system-proxy.ts +155 -0
  23. package/src/claude/inbound-model-options.ts +10 -2
  24. package/src/claude/inbound.ts +1 -1
  25. package/src/claude/intercept/picker-ca-cleanup.ts +31 -0
  26. package/src/claude/intercept/picker-ca.ts +283 -83
  27. package/src/claude/intercept/picker-models.ts +4 -1
  28. package/src/claude/intercept/picker-trust.ts +15 -2
  29. package/src/claude/intercept/runtime.ts +99 -32
  30. package/src/cli/account-api.ts +5 -2
  31. package/src/cli/account-extended.ts +47 -11
  32. package/src/cli/account-target.ts +12 -7
  33. package/src/cli/account.ts +47 -3
  34. package/src/cli/capabilities.ts +33 -7
  35. package/src/cli/claude-agent-startup-sync.ts +2 -0
  36. package/src/cli/claude-desktop.ts +37 -13
  37. package/src/cli/cross-home-owner.ts +138 -0
  38. package/src/cli/dispatch.ts +1 -1
  39. package/src/cli/doctor.ts +18 -8
  40. package/src/cli/ensure-desired-integrations.ts +5 -0
  41. package/src/cli/export-command.ts +6 -6
  42. package/src/cli/help.ts +1 -1
  43. package/src/cli/index.ts +15 -18
  44. package/src/cli/integrations.ts +8 -1
  45. package/src/cli/models-runtime-subcommands.ts +1 -0
  46. package/src/cli/models-runtime.ts +106 -4
  47. package/src/cli/models.ts +6 -1
  48. package/src/cli/opencode.ts +2 -83
  49. package/src/cli/registry.ts +3 -2
  50. package/src/cli/status.ts +86 -7
  51. package/src/client/connect.ts +24 -10
  52. package/src/client/link-ingress.ts +3 -1
  53. package/src/client/link-join.ts +76 -9
  54. package/src/client/link-relay-transport.ts +184 -0
  55. package/src/client/link-relay.ts +16 -4
  56. package/src/client/machine-listener.ts +2 -0
  57. package/src/clients/config-export/constants.ts +8 -0
  58. package/src/clients/config-export/contracts.ts +13 -1
  59. package/src/clients/config-export/droid.ts +71 -0
  60. package/src/clients/config-export/kilo.ts +116 -0
  61. package/src/clients/config-export.ts +33 -2
  62. package/src/codex/auth-api/routes.ts +1 -1
  63. package/src/codex/catalog/access-programs.ts +14 -2
  64. package/src/codex/catalog/provider-models.ts +1 -1
  65. package/src/codex/catalog-auto-refresh.ts +122 -0
  66. package/src/codex/config-drift-heal.ts +80 -0
  67. package/src/codex/desktop-switches.ts +7 -5
  68. package/src/codex/inject/bounded-config-reader.ts +69 -0
  69. package/src/codex/inject/config-toml.ts +31 -3
  70. package/src/codex/inject/routing-target.ts +6 -1
  71. package/src/codex/inject.ts +4 -0
  72. package/src/codex/model-entitlements.ts +32 -5
  73. package/src/codex/native-profile-startup.ts +17 -5
  74. package/src/codex/project-config-warnings.ts +72 -21
  75. package/src/codex/quota-auto-refresh-state.ts +8 -2
  76. package/src/codex/quota-auto-refresh.ts +80 -9
  77. package/src/codex/sync.ts +10 -0
  78. package/src/combos/failover.ts +4 -1
  79. package/src/combos/jev.ts +15 -4
  80. package/src/combos/types.ts +16 -0
  81. package/src/config/diagnostics.ts +19 -1
  82. package/src/config/load-degrade.ts +29 -0
  83. package/src/config/macos-system-proxy.ts +114 -0
  84. package/src/config/proxy-env.ts +67 -20
  85. package/src/config/schema/config-schema.ts +15 -0
  86. package/src/config/schema/leaf-validators.ts +23 -0
  87. package/src/config.ts +2 -0
  88. package/src/generated/compatibility-version.json +220 -144
  89. package/src/integrations/catalog-refresh.ts +1 -1
  90. package/src/integrations/config-io.ts +12 -3
  91. package/src/integrations/droid-settings.ts +65 -0
  92. package/src/integrations/kilo-candidates.ts +30 -0
  93. package/src/integrations/mutation-plan.ts +62 -7
  94. package/src/integrations/native/ownership-preflight.ts +46 -12
  95. package/src/integrations/registry.ts +136 -7
  96. package/src/integrations/state.ts +52 -4
  97. package/src/integrations/target.ts +3 -0
  98. package/src/integrations/writer.ts +55 -14
  99. package/src/lib/account-selection-events.ts +14 -0
  100. package/src/lib/jsonc.ts +110 -0
  101. package/src/lib/standalone.ts +9 -1
  102. package/src/lib/windows-system-proxy.ts +101 -1
  103. package/src/link/relay-auth.ts +29 -0
  104. package/src/link/ssh-argv.ts +6 -3
  105. package/src/link/ssh-runner.ts +5 -5
  106. package/src/oauth/generic-account-failover.ts +52 -8
  107. package/src/oauth/index.ts +37 -14
  108. package/src/oauth/store.ts +96 -11
  109. package/src/oauth/token-guardian.ts +7 -3
  110. package/src/oauth/types.ts +2 -0
  111. package/src/plugins/loader.ts +5 -3
  112. package/src/providers/fastwire.ts +3 -0
  113. package/src/providers/key-failover.ts +13 -5
  114. package/src/providers/kiro-model-catalog.ts +4 -3
  115. package/src/providers/model-rename-fields.ts +1 -0
  116. package/src/providers/openai-tiers-destination.ts +9 -0
  117. package/src/providers/quota/account-cache.ts +17 -3
  118. package/src/providers/quota/vendor-probes-oauth.ts +2 -0
  119. package/src/providers/quota.ts +2 -2
  120. package/src/responses/code-mode-helper-compat.ts +14 -1
  121. package/src/responses/custom-tool-compat.ts +1 -1
  122. package/src/server/auth-cors.ts +1 -0
  123. package/src/server/images.ts +70 -20
  124. package/src/server/index/link-listener.ts +20 -3
  125. package/src/server/index/link-relay-sessions.ts +138 -0
  126. package/src/server/index/optional-listeners.ts +11 -1
  127. package/src/server/index/serve-options.ts +12 -0
  128. package/src/server/inference/client-encoder-delivery.ts +1 -0
  129. package/src/server/local-management-read-client.ts +21 -2
  130. package/src/server/management/agent-settings-routes.ts +19 -5
  131. package/src/server/management/combo-routes.ts +5 -3
  132. package/src/server/management/config-routes.ts +32 -6
  133. package/src/server/management/model-routes.ts +232 -7
  134. package/src/server/management/model-rows.ts +199 -1
  135. package/src/server/management/oauth-account-routes.ts +63 -3
  136. package/src/server/management/route-registry.ts +2 -0
  137. package/src/server/port-reclaim.ts +170 -32
  138. package/src/server/responses/adapter-delivery.ts +6 -4
  139. package/src/server/responses/adapter-dispatch.ts +72 -2
  140. package/src/server/responses/core-combo.ts +6 -1
  141. package/src/server/responses/core-normalize.ts +19 -4
  142. package/src/server/responses/core-options.ts +2 -0
  143. package/src/server/responses/memory-models.ts +169 -0
  144. package/src/server/responses/passthrough-delivery.ts +4 -0
  145. package/src/server/responses/passthrough-dispatch.ts +10 -0
  146. package/src/server/responses/request-prepare.ts +86 -4
  147. package/src/server/responses/request-transport.ts +4 -0
  148. package/src/server/responses/run-turn-execution.ts +5 -2
  149. package/src/server/responses/shadow-target-availability.ts +17 -4
  150. package/src/server/responses/sidecar-execution.ts +18 -2
  151. package/src/server/responses/skills-snapshot.ts +239 -0
  152. package/src/server/responses-undeclared-tool-guard.ts +43 -6
  153. package/src/server/search.ts +1 -1
  154. package/src/service/guards.ts +4 -5
  155. package/src/service/state.ts +39 -2
  156. package/src/service.ts +2 -2
  157. package/src/types/config.ts +46 -4
  158. package/src/types/provider.ts +9 -0
  159. package/src/types/request.ts +6 -0
  160. package/src/types/tools.ts +38 -8
  161. package/src/types.ts +3 -0
  162. package/src/update/async-check.ts +5 -1
  163. package/src/update/index.ts +30 -12
  164. package/src/update/job.ts +3 -1
  165. package/src/update/pnpm-read-policy.d.mts +4 -0
  166. package/src/update/pnpm-read-policy.mjs +44 -0
  167. package/src/update/worker-launch.ts +152 -11
  168. package/src/usage/cost.ts +6 -3
  169. package/src/usage/log.ts +4 -0
  170. package/src/web-search/backends.ts +2 -2
  171. package/src/web-search/devin-executor.ts +1 -1
  172. package/src/web-search/sidecar-providers.ts +5 -5
  173. package/gui/dist/assets/App-mpsibsFj.js +0 -51
  174. package/gui/dist/assets/index-BDUBS8PW.css +0 -1
  175. package/gui/dist/assets/index-kUE8tuAK.js +0 -86
@@ -42,6 +42,7 @@ import { kiroEvidenceIdentity } from "../providers/kiro-account-state-disk";
42
42
  import { kiroAccountSupportsModel } from "../providers/kiro-model-catalog";
43
43
  import { ACCOUNT_QUOTA_TTL_MS } from "../providers/quota-wire";
44
44
  import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
45
+ import { subscribeOAuthAccountPauseChanges } from "../lib/account-selection-events";
45
46
  import type { OcxConfig, OcxProviderConfig } from "../types";
46
47
 
47
48
  /** Cap same-request rotations so a short Retry-After cannot spin. Mirrors the Anthropic bound. */
@@ -83,7 +84,7 @@ const EXCLUDED_PROVIDERS = new Set(["openai", "anthropic"]);
83
84
 
84
85
  interface AccountHealth {
85
86
  cooldownUntil: number;
86
- cooldownSource: "retry-after" | "default" | "kiro-suspension";
87
+ cooldownSource: "retry-after" | "default" | "kiro-suspension" | "auth";
87
88
  identity?: string;
88
89
  }
89
90
 
@@ -98,6 +99,8 @@ const health = new Map<string, AccountHealth>();
98
99
  /** Provider -> recent eligible-account count. TTL-bounded; never holds credential material. */
99
100
  const presence = new Map<string, PresenceEntry>();
100
101
 
102
+ subscribeOAuthAccountPauseChanges(provider => presence.delete(provider));
103
+
101
104
  const healthKey = (provider: string, accountId: string, family?: QuotaModelFamily) =>
102
105
  family ? `${provider}\u0000${accountId}\u0000${family}` : `${provider}\u0000${accountId}`;
103
106
 
@@ -114,6 +117,13 @@ export function quarantineKiroSuspendedAccount(accountId: string, generation?: s
114
117
  function isCooled(provider: string, accountId: string, now: number, family?: QuotaModelFamily): boolean {
115
118
  const entry = health.get(healthKey(provider, accountId, family));
116
119
  if (!entry) return false;
120
+ if (provider === "google-antigravity" && entry.cooldownSource === "auth") {
121
+ const live = getAccountSet(provider)?.accounts.find(row => row.id === accountId);
122
+ if (!live || entry.identity !== credentialGeneration(live.credential)) {
123
+ health.delete(healthKey(provider, accountId, family));
124
+ return false;
125
+ }
126
+ }
117
127
  if (provider === "kiro" && entry.cooldownSource === "kiro-suspension") {
118
128
  const live = getAccountSet("kiro")?.accounts.find(row => row.id === accountId);
119
129
  if (!live || entry.identity !== kiroEvidenceIdentity(live)) {
@@ -128,12 +138,13 @@ function isCooled(provider: string, accountId: string, now: number, family?: Quo
128
138
  return true;
129
139
  }
130
140
 
131
- export type KiroSkipReason = "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
141
+ export type KiroSkipReason = "paused" | "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
132
142
 
133
143
  /** Eligibility for automatic alternatives; an active singleton can still send. */
134
144
  export function kiroAutoSelection(
135
145
  account: ProviderAccount, now = Date.now(),
136
146
  ): { autoSelectable: boolean; skipReason?: KiroSkipReason } {
147
+ if (account.paused === true) return { autoSelectable: false, skipReason: "paused" };
137
148
  if (account.needsReauth === true) return { autoSelectable: false, skipReason: "needs_reauth" };
138
149
  const cooled = isCooled("kiro", account.id, now);
139
150
  if (cooled && health.get(healthKey("kiro", account.id))?.cooldownSource === "kiro-suspension")
@@ -163,8 +174,10 @@ function eligibleAccountCount(providerName: string, now: number): number {
163
174
  const set = getAccountSet(providerName);
164
175
  // Kiro terminal refresh marks the just-refused account needsReauth before the alternate
165
176
  // selector runs. Both stored logins still express consent to recover through the survivor.
166
- const eligible = set ? (providerName === "kiro" ? set.accounts.length
167
- : set.accounts.filter(account => account.needsReauth !== true).length) : 0;
177
+ const eligible = set
178
+ ? set.accounts.filter(account => account.paused !== true
179
+ && (providerName === "kiro" || account.needsReauth !== true)).length
180
+ : 0;
168
181
  presence.set(providerName, { eligible, readAt: now });
169
182
  return eligible;
170
183
  }
@@ -260,8 +273,9 @@ function eligibleIdsIn(
260
273
  ): string[] {
261
274
  if (!set) return [];
262
275
  return set.accounts
263
- .filter(account => providerName === "kiro" ? kiroAutoSelection(account, now).autoSelectable
264
- : account.needsReauth !== true && !isCooled(providerName, account.id, now, family))
276
+ .filter(account => account.paused !== true
277
+ && (providerName === "kiro" ? kiroAutoSelection(account, now).autoSelectable
278
+ : account.needsReauth !== true && !isCooled(providerName, account.id, now, family)))
265
279
  .map(account => account.id);
266
280
  }
267
281
 
@@ -285,6 +299,34 @@ export function hasEligibleGenericOAuthFailoverTarget(
285
299
  return eligibleIdsIn(set, providerName, now, family).some(id => id !== failedAccountId);
286
300
  }
287
301
 
302
+ /** Select a live Antigravity sibling using activation captured before the refused send. */
303
+ export function rotateAntigravityAccountOnAuthRefusal(
304
+ poolActivated: boolean,
305
+ failedAccountId: string,
306
+ sentGeneration: string,
307
+ requestedModelId: string | null | undefined,
308
+ now = Date.now(),
309
+ ): string | null {
310
+ if (!poolActivated) return null;
311
+ const set = getAccountSet("google-antigravity");
312
+ if (!set || set.accounts.length < 2) return null;
313
+ const family = classifyModelFamilyForQuota("google-antigravity", requestedModelId);
314
+ const failed = set.accounts.find(row => row.id === failedAccountId);
315
+ if (failed && credentialGeneration(failed.credential) === sentGeneration) {
316
+ health.set(healthKey("google-antigravity", failedAccountId, family), {
317
+ cooldownUntil: now + DEFAULT_COOLDOWN_MS,
318
+ cooldownSource: "auth",
319
+ identity: sentGeneration,
320
+ });
321
+ sweepExpiredOnWrite(now);
322
+ }
323
+ const eligible = new Set(eligibleIdsIn(set, "google-antigravity", now, family));
324
+ const order = set.accounts.map(row => row.id);
325
+ const start = order.indexOf(failedAccountId);
326
+ const ring = start >= 0 ? [...order.slice(start + 1), ...order.slice(0, start)] : order;
327
+ return ring.find(id => id !== failedAccountId && eligible.has(id)) ?? null;
328
+ }
329
+
288
330
  /** Generic pool strategies the kernel can actually run. `quota` IS the pre-kernel path. */
289
331
  type ActiveGenericStrategy = "round-robin" | "fill-first" | "least-loaded";
290
332
 
@@ -579,7 +621,9 @@ export function preferredInitialAccount(
579
621
  if (!selected) return null;
580
622
  const active = selected.activeAccountId;
581
623
  const accountRows = new Map(selected.accounts.map(account => [account.id, account]));
582
- const order = selected.accounts.filter(account => account.needsReauth !== true).map(account => account.id);
624
+ const order = selected.accounts
625
+ .filter(account => account.paused !== true && account.needsReauth !== true)
626
+ .map(account => account.id);
583
627
  if (order.length < 2) return null;
584
628
 
585
629
  const modelEligible = preferKiroModelSupport(providerName,
@@ -629,7 +673,7 @@ export function preferredInitialAccount(
629
673
  }
630
674
 
631
675
  const activeRow = selected.accounts.find(account => account.id === active);
632
- if (activeRow && activeRow.needsReauth !== true
676
+ if (activeRow && activeRow.paused !== true && activeRow.needsReauth !== true
633
677
  && !isCooled(providerName, activeRow.id, now, classifyModelFamilyForQuota(providerName, requestedModelId))
634
678
  && !isAccountQuotaExhausted(providerName, activeRow.id, requestedModelId, activeRow)) return null;
635
679
 
@@ -108,6 +108,7 @@ export interface ObservedOAuthAccessSnapshot extends OAuthAccessSnapshot {
108
108
 
109
109
  export type OAuthActiveTokenObservation =
110
110
  | { readonly kind: "available"; readonly snapshot: ObservedOAuthAccessSnapshot }
111
+ | { readonly kind: "paused" }
111
112
  | { readonly kind: "missing" }
112
113
  | { readonly kind: "malformed" }
113
114
  | { readonly kind: "needs-reauth" }
@@ -418,6 +419,14 @@ export class OAuthLoginRequiredError extends Error {
418
419
  }
419
420
  }
420
421
 
422
+ /** An operator-paused account is temporarily unavailable, not an invalid login. */
423
+ export class OAuthAccountPausedError extends Error {
424
+ constructor() {
425
+ super("OAuth account is paused. Resume it in account settings and retry.");
426
+ this.name = "OAuthAccountPausedError";
427
+ }
428
+ }
429
+
421
430
  export class OAuthProviderPublicationError extends Error {
422
431
  constructor() {
423
432
  super("OAuth credential was saved, but the provider entry was not written. Resolve the account namespace collision, then retry login.");
@@ -455,6 +464,7 @@ export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
455
464
  }
456
465
  if (
457
466
  (error instanceof OAuthLoginRequiredError && isOAuthProvider(error.provider))
467
+ || error instanceof OAuthAccountPausedError
458
468
  || error instanceof OAuthProviderPublicationError
459
469
  // Reauth identity outcomes carry fixed, account-free remediation text. Dropping them to the
460
470
  // generic message hides WHICH failure the user must fix (sign in with the selected account).
@@ -466,7 +476,7 @@ export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
466
476
  return "OAuth authentication failed. Check the OpenCodex account status and retry.";
467
477
  }
468
478
 
469
- function accessSnapshot(provider: string, accountId: string, cred: OAuthCredentials): OAuthAccessSnapshot {
479
+ function accessSnapshot(provider: string, accountId: string, cred: OAuthCredentials, oauthProvider = provider): OAuthAccessSnapshot {
470
480
  // Derived, not read back: a stored `authType` is trusted when present, but a credential imported
471
481
  // before the field existed still routes correctly because the client pair implies SSO OIDC.
472
482
  const kiroAuthType = cred.kiro?.authType
@@ -484,9 +494,11 @@ function accessSnapshot(provider: string, accountId: string, cred: OAuthCredenti
484
494
  // Validated here, not at the call site: an unvalidated origin from a legacy or crafted
485
495
  // credential must never travel with a bearer, and dropping it makes the transport fall back to
486
496
  // the canonical host rather than to whatever the previous account was using.
487
- const accountApiBaseUrl = provider === "github-copilot"
497
+ // The host rides on the OAuth definition the snapshot was resolved through, not the routed
498
+ // slot name: a custom provider reusing the Devin definition keeps its stored tenant URL.
499
+ const accountApiBaseUrl = oauthProvider === "github-copilot"
488
500
  ? validateCopilotApiBaseUrl(cred.apiBaseUrl)
489
- : provider === "devin" || provider === "devin-cli"
501
+ : oauthProvider === "devin" || oauthProvider === "devin-cli"
490
502
  ? validateDevinApiBaseUrl(cred.apiBaseUrl)
491
503
  : undefined;
492
504
  return {
@@ -529,6 +541,7 @@ export function observeActiveOAuthAccessToken(
529
541
  const accountSet = authStore.store[provider];
530
542
  const account = accountSet?.accounts.find(candidate => candidate.id === accountSet.activeAccountId);
531
543
  if (!account) return { kind: "missing" };
544
+ if (account.paused === true) return { kind: "paused" };
532
545
  if (account.needsReauth) return { kind: "needs-reauth" };
533
546
  if (account.credential.expires <= now) return { kind: "expired" };
534
547
  if (account.credential.expires <= now + REFRESH_SKEW_MS) return { kind: "near-expiry" };
@@ -550,18 +563,20 @@ async function resolveAccessSnapshotForAccount(
550
563
  accountId: string,
551
564
  rejectedGeneration?: string,
552
565
  requireUsableAccount = false,
566
+ oauthProvider = provider,
553
567
  ): Promise<OAuthAccessSnapshot> {
554
- const def = OAUTH_PROVIDERS[provider];
555
- if (!def) throw new UnsupportedOAuthProviderError(provider);
568
+ const def = OAUTH_PROVIDERS[oauthProvider];
569
+ if (!def) throw new UnsupportedOAuthProviderError(oauthProvider);
556
570
  // One store read answers both questions. A caller that opts in gets the account REJECTED
557
571
  // when it needs reauthentication, which a bare credential read cannot detect: a revoked
558
572
  // account keeps a readable credential, so resolution would otherwise succeed and the
559
573
  // request would dispatch on an account already known to need a fresh login.
560
574
  const row = getAccountCredentialWithStatus(provider, accountId);
561
575
  if (!row) throw new OAuthLoginRequiredError(provider);
576
+ if (row.paused === true) throw new OAuthAccountPausedError();
562
577
  if (requireUsableAccount && row.needsReauth) throw new OAuthLoginRequiredError(provider);
563
578
  const cred = row.credential;
564
- const current = accessSnapshot(provider, accountId, cred);
579
+ const current = accessSnapshot(provider, accountId, cred, oauthProvider);
565
580
  if (rejectedGeneration !== undefined && current.generation !== rejectedGeneration) return current;
566
581
  if (rejectedGeneration === undefined && cred.expires > Date.now() + REFRESH_SKEW_MS) return current;
567
582
 
@@ -590,12 +605,14 @@ async function resolveAccessSnapshotForAccount(
590
605
  };
591
606
  const refresh = (async (): Promise<OAuthAccessSnapshot> => {
592
607
  const accessToken = await refreshAndPersistAccessToken(provider, accountId, def, cred, abort.signal, flight, replacedStaleFlight);
593
- const persisted = getAccountCredential(provider, accountId);
594
- if (!persisted) throw new OAuthLoginRequiredError(provider);
608
+ const persistedRow = getAccountCredentialWithStatus(provider, accountId);
609
+ if (!persistedRow) throw new OAuthLoginRequiredError(provider);
610
+ if (persistedRow.paused === true) throw new OAuthAccountPausedError();
611
+ const persisted = persistedRow.credential;
595
612
  if (persisted.access !== accessToken) {
596
613
  throw new Error(`OAuth refresh persisted an unexpected access token for ${provider}`);
597
614
  }
598
- return accessSnapshot(provider, accountId, persisted);
615
+ return accessSnapshot(provider, accountId, persisted, oauthProvider);
599
616
  })().catch(error => {
600
617
  if (abort.signal.reason instanceof OAuthTokenRefreshStaleError) throw abort.signal.reason;
601
618
  throw error;
@@ -607,10 +624,13 @@ async function resolveAccessSnapshotForAccount(
607
624
  return refresh;
608
625
  }
609
626
 
610
- export async function getValidAccessTokenSnapshot(provider: string): Promise<OAuthAccessSnapshot> {
627
+ export async function getValidAccessTokenSnapshot(
628
+ provider: string,
629
+ options: { oauthProvider?: string } = {},
630
+ ): Promise<OAuthAccessSnapshot> {
611
631
  const set = getAccountSet(provider);
612
632
  if (!set) throw new OAuthLoginRequiredError(provider);
613
- return resolveAccessSnapshotForAccount(provider, set.activeAccountId);
633
+ return resolveAccessSnapshotForAccount(provider, set.activeAccountId, undefined, false, options.oauthProvider);
614
634
  }
615
635
 
616
636
  /** Providers whose upstream-401 replay path may force a snapshot refresh. */
@@ -836,7 +856,7 @@ function merged(fresh: OAuthCredentials, previous: OAuthCredentials): OAuthCrede
836
856
  ...(fresh.kiro === undefined && previous.kiro ? { kiro: previous.kiro } : {}),
837
857
  };
838
858
  }
839
- export async function refreshXaiAccountWithLock(provider:string,accountId:string,def:OAuthProviderDef,callerCredential:OAuthCredentials,deps:XaiRefreshDeps={}):Promise<string>{const writerGeneration=captureConfigGeneration();const now=deps.now??Date.now;const guard=await(deps.intentLock??createOAuthRefreshIntentLock(provider,accountId)).acquire();try{const stored=getAccountCredential(provider,accountId);if(!stored)throw new OAuthLoginRequiredError(provider);const active=getAccountSet(provider)?.activeAccountId===accountId,candidate=authoritative(stored,active,now);if(credentialGeneration(candidate)!==credentialGeneration(callerCredential)&&candidate.expires>now()+REFRESH_SKEW_MS){if(credentialGeneration(candidate)!==credentialGeneration(stored)){const o=await mergeAccountCredential(provider,accountId,candidate,{expectedGeneration:credentialGeneration(stored),afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}}return candidate.access;}if(cached(provider,accountId,candidate,now))throw new OAuthLoginRequiredError(provider);const generation=credentialGeneration(candidate);try{const fresh=merged(await def.refresh(candidate.refresh,deps.signal),candidate);const o=await mergeAccountCredential(provider,accountId,fresh,{expectedGeneration:generation,afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));if(candidate.source==="local-cli")console.warn(XAI_LOCAL_CLI_DETACH_WARNING);return fresh.access;}catch(error){if(error instanceof OAuthMutationBusyError){permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));throw error;}if(!terminal(error))throw error;const failedAt=now();permanentRefreshFailures.set(verdictKey(provider,accountId,candidate),failedAt+XAI_PERMANENT_FAILURE_TTL_MS);sweepExpiredOnWrite(failedAt);await markAccountNeedsReauthIfGeneration(provider,accountId,generation,writerGeneration);throw new OAuthLoginRequiredError(provider);}}finally{guard.release();}}
859
+ export async function refreshXaiAccountWithLock(provider:string,accountId:string,def:OAuthProviderDef,callerCredential:OAuthCredentials,deps:XaiRefreshDeps={}):Promise<string>{const writerGeneration=captureConfigGeneration();const now=deps.now??Date.now;const guard=await(deps.intentLock??createOAuthRefreshIntentLock(provider,accountId)).acquire();try{const row=getAccountCredentialWithStatus(provider,accountId);if(!row)throw new OAuthLoginRequiredError(provider);if(row.paused)throw new OAuthAccountPausedError();const stored=row.credential;const active=getAccountSet(provider)?.activeAccountId===accountId,candidate=authoritative(stored,active,now);if(credentialGeneration(candidate)!==credentialGeneration(callerCredential)&&candidate.expires>now()+REFRESH_SKEW_MS){if(credentialGeneration(candidate)!==credentialGeneration(stored)){const o=await mergeAccountCredential(provider,accountId,candidate,{expectedGeneration:credentialGeneration(stored),afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}}return candidate.access;}if(cached(provider,accountId,candidate,now))throw new OAuthLoginRequiredError(provider);const generation=credentialGeneration(candidate);try{const fresh=merged(await def.refresh(candidate.refresh,deps.signal),candidate);const o=await mergeAccountCredential(provider,accountId,fresh,{expectedGeneration:generation,afterPrePersistRead:deps.afterPrePersistRead});if(o.superseded){if(o.stored.expires>now()+REFRESH_SKEW_MS)return o.stored.access;throw new OAuthLoginRequiredError(provider);}permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));if(candidate.source==="local-cli")console.warn(XAI_LOCAL_CLI_DETACH_WARNING);return fresh.access;}catch(error){if(error instanceof OAuthMutationBusyError){permanentRefreshFailures.delete(verdictKey(provider,accountId,candidate));throw error;}if(!terminal(error))throw error;const failedAt=now();permanentRefreshFailures.set(verdictKey(provider,accountId,candidate),failedAt+XAI_PERMANENT_FAILURE_TTL_MS);sweepExpiredOnWrite(failedAt);await markAccountNeedsReauthIfGeneration(provider,accountId,generation,writerGeneration);throw new OAuthLoginRequiredError(provider);}}finally{guard.release();}}
840
860
 
841
861
  function newerClaudeCredential(stored: OAuthCredentials, now: number): OAuthCredentials | undefined {
842
862
  if (stored.source !== "local-cli") return undefined;
@@ -1018,8 +1038,11 @@ export async function refreshGenericAccountWithLock(
1018
1038
  logOAuthEvent("OAuth refresh started", { provider, accountId });
1019
1039
  const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
1020
1040
  try {
1021
- const stored = getAccountCredential(provider, accountId);
1022
- if (!stored) throw new OAuthLoginRequiredError(provider);
1041
+ // Re-read under the lock: a pause committed while this caller waited must stop the refresh.
1042
+ const row = getAccountCredentialWithStatus(provider, accountId);
1043
+ if (!row) throw new OAuthLoginRequiredError(provider);
1044
+ if (row.paused) throw new OAuthAccountPausedError();
1045
+ const stored = row.credential;
1023
1046
  if (
1024
1047
  credentialGeneration(stored) !== credentialGeneration(callerCredential)
1025
1048
  && stored.expires > Date.now() + REFRESH_SKEW_MS
@@ -27,7 +27,7 @@ import { atomicWriteFileNoFollowUnclaimed } from "../config/atomic-write";
27
27
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
28
28
  import { recordOwnedConfigPath } from "../lib/config-ownership";
29
29
  import { MAX_PENDING_OAUTH_MUTATIONS } from "../lib/translator-budget";
30
- import { publishAccountSelection } from "../lib/account-selection-events";
30
+ import { publishAccountSelection, publishOAuthAccountPauseChange } from "../lib/account-selection-events";
31
31
  import {
32
32
  captureConfigGeneration,
33
33
  type GenerationContext,
@@ -625,6 +625,7 @@ function normalizeAccount(value: unknown): ProviderAccount | null {
625
625
  const account: ProviderAccount = { id: candidate.id, credential };
626
626
  if (typeof candidate.alias === "string" && candidate.alias.trim()) account.alias = candidate.alias.trim();
627
627
  if (candidate.needsReauth === true) account.needsReauth = true;
628
+ if (candidate.paused === true) account.paused = true;
628
629
  if (typeof candidate.addedAt === "number") account.addedAt = candidate.addedAt;
629
630
  if (typeof candidate.loginId === "string"
630
631
  && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(candidate.loginId)) {
@@ -874,7 +875,7 @@ export async function saveCredentialWithReceipt(
874
875
  if (existing) {
875
876
  existing.credential = safe;
876
877
  delete existing.needsReauth;
877
- set.activeAccountId = existing.id;
878
+ if (existing.paused !== true) set.activeAccountId = existing.id;
878
879
  accountId = existing.id;
879
880
  } else {
880
881
  // Legacy migration: a pre-identity row (no accountId/email) for this provider is the
@@ -912,6 +913,15 @@ export async function saveCredentialWithReceipt(
912
913
  accountId = id;
913
914
  }
914
915
  }
916
+ const updatedSet = store[provider];
917
+ const savedAccount = updatedSet?.accounts.find(account => account.id === accountId);
918
+ const activeAccount = updatedSet?.accounts.find(account => account.id === updatedSet.activeAccountId);
919
+ if (savedAccount?.paused === true
920
+ && (!activeAccount || activeAccount.paused === true || activeAccount.needsReauth === true)) {
921
+ const fallback = updatedSet.accounts.find(account => account.id !== accountId
922
+ && account.paused !== true && account.needsReauth !== true);
923
+ if (fallback) updatedSet.activeAccountId = fallback.id;
924
+ }
915
925
  // Every explicit login, including an in-place legacy slot upgrade, starts new evidence.
916
926
  store[provider]!.accounts.find(account => account.id === accountId)!.loginId = randomUUID();
917
927
  return {
@@ -1047,7 +1057,7 @@ export async function upsertCredentialByIdentity(
1047
1057
  }
1048
1058
 
1049
1059
  /**
1050
- * Remove the ACTIVE account; remaining accounts promote the first one.
1060
+ * Remove the ACTIVE account; promote the first usable survivor when available.
1051
1061
  *
1052
1062
  * Returns what actually happened, which a caller cannot otherwise know. A read-then-remove
1053
1063
  * preflight is not equivalent: `mutateStore` serializes mutations, so between a caller's
@@ -1064,7 +1074,8 @@ export async function removeCredential(provider: string): Promise<"removed" | "n
1064
1074
  delete store[provider];
1065
1075
  return "removed" as const;
1066
1076
  }
1067
- set.activeAccountId = set.accounts[0]!.id;
1077
+ set.activeAccountId = set.accounts.find(account => account.paused !== true && account.needsReauth !== true)?.id
1078
+ ?? set.accounts[0]!.id;
1068
1079
  return "removed" as const;
1069
1080
  }, [provider], { scrubLegacyBackup: result => result === "removed" ? [provider] : [] });
1070
1081
  }
@@ -1106,10 +1117,14 @@ export function getAccountCredential(provider: string, accountId: string): OAuth
1106
1117
  export function getAccountCredentialWithStatus(
1107
1118
  provider: string,
1108
1119
  accountId: string,
1109
- ): { credential: OAuthCredentials; needsReauth: boolean } | null {
1120
+ ): { credential: OAuthCredentials; needsReauth: boolean; paused: boolean } | null {
1110
1121
  const account = loadAuthStore()[provider]?.accounts.find(a => a.id === accountId);
1111
1122
  if (!account?.credential) return null;
1112
- return { credential: account.credential, needsReauth: account.needsReauth === true };
1123
+ return {
1124
+ credential: account.credential,
1125
+ needsReauth: account.needsReauth === true,
1126
+ paused: account.paused === true,
1127
+ };
1113
1128
  }
1114
1129
 
1115
1130
  /** Persist a refreshed credential for a SPECIFIC account without touching activeAccountId. */
@@ -1122,11 +1137,19 @@ export async function saveAccountCredential(
1122
1137
  const safe = normalizeCredential(cred);
1123
1138
  if (!safe) return;
1124
1139
  await mutateStore(store => {
1125
- const account = store[provider]?.accounts.find(a => a.id === accountId);
1126
- if (!account) return;
1140
+ const set = store[provider];
1141
+ const account = set?.accounts.find(a => a.id === accountId);
1142
+ if (!set || !account) return;
1127
1143
  account.credential = safe;
1128
1144
  if (opts.rotateLoginId) account.loginId = randomUUID();
1129
1145
  delete account.needsReauth;
1146
+ // A pause that found no usable fallback leaves the active id on a paused row. Once this
1147
+ // reauthentication makes an unpaused account usable, hand the selection to it.
1148
+ const active = set.accounts.find(a => a.id === set.activeAccountId);
1149
+ if (active?.paused === true && account.paused !== true && account.id !== active.id) {
1150
+ set.activeAccountId = account.id;
1151
+ set.selectionRevision = randomUUID();
1152
+ }
1130
1153
  }, [provider, accountId, safe], { assertBeforePersist: opts.assertBeforePersist });
1131
1154
  }
1132
1155
 
@@ -1162,7 +1185,7 @@ export async function commitOAuthAccountSelection(
1162
1185
  const valid = (set: ProviderAccountSet): boolean => {
1163
1186
  if (expected && (set.activeAccountId !== expected.accountId || set.selectionRevision !== expected.revision)) return false;
1164
1187
  const account = set.accounts.find(account => account.id === accountId);
1165
- if (!account || (requireUsableAccount && account.needsReauth === true)) return false;
1188
+ if (!account || account.paused === true || (requireUsableAccount && account.needsReauth === true)) return false;
1166
1189
  return expectedCredentialGeneration === undefined || credentialGeneration(account.credential) === expectedCredentialGeneration;
1167
1190
  };
1168
1191
  if (expected?.accountId === accountId) {
@@ -1196,7 +1219,64 @@ export async function setAccountAlias(provider: string, accountId: string, alias
1196
1219
  }, [provider, accountId, alias]);
1197
1220
  }
1198
1221
 
1199
- /** Remove one account by id; active removal promotes the first remaining account. */
1222
+ export type SetAccountPausedResult =
1223
+ | { status: "updated"; activeAccountId: string; activeAccountChanged: boolean }
1224
+ | { status: "unchanged"; activeAccountId: string; activeAccountChanged: boolean }
1225
+ | { status: "not-found" };
1226
+
1227
+ /** Persist an operator pause and move an active account to the next usable unpaused slot when available. */
1228
+ export async function setAccountPaused(
1229
+ provider: string,
1230
+ accountId: string,
1231
+ paused: boolean,
1232
+ ): Promise<SetAccountPausedResult> {
1233
+ const result = await mutateStore(store => {
1234
+ const set = store[provider];
1235
+ const account = set?.accounts.find(candidate => candidate.id === accountId);
1236
+ if (!set || !account) return { status: "not-found" } as const;
1237
+ if ((account.paused === true) === paused) {
1238
+ return { status: "unchanged", activeAccountId: set.activeAccountId, activeAccountChanged: false } as const;
1239
+ }
1240
+
1241
+ if (paused) account.paused = true;
1242
+ else delete account.paused;
1243
+ // Pause is part of selection eligibility even when the operator changes a non-active
1244
+ // account. Bump the provider selection revision so observers receive the existing
1245
+ // post-persistence roster invalidation and stale automatic proposals cannot commit.
1246
+ set.selectionRevision = randomUUID();
1247
+
1248
+ let activeAccountChanged = false;
1249
+ if (paused && set.activeAccountId === accountId) {
1250
+ const start = set.accounts.findIndex(candidate => candidate.id === accountId);
1251
+ const ring = [...set.accounts.slice(start + 1), ...set.accounts.slice(0, start)];
1252
+ const fallback = ring.find(candidate => candidate.paused !== true && candidate.needsReauth !== true);
1253
+ if (fallback) {
1254
+ set.activeAccountId = fallback.id;
1255
+ set.selectionRevision = randomUUID();
1256
+ activeAccountChanged = true;
1257
+ }
1258
+ } else if (!paused) {
1259
+ // If all accounts had been paused, the active id still points at a paused
1260
+ // slot. Resuming the first usable account must restore a usable selection.
1261
+ const active = set.accounts.find(candidate => candidate.id === set.activeAccountId);
1262
+ if ((!active || active.paused === true || active.needsReauth === true) && account.needsReauth !== true) {
1263
+ set.activeAccountId = accountId;
1264
+ set.selectionRevision = randomUUID();
1265
+ activeAccountChanged = true;
1266
+ }
1267
+ }
1268
+
1269
+ return {
1270
+ status: "updated",
1271
+ activeAccountId: set.activeAccountId,
1272
+ activeAccountChanged,
1273
+ } as const;
1274
+ }, [provider, accountId, paused]);
1275
+ if (result.status === "updated") publishOAuthAccountPauseChange(provider);
1276
+ return result;
1277
+ }
1278
+
1279
+ /** Remove one account by id; active removal promotes the first usable survivor when available. */
1200
1280
  export async function removeAccount(provider: string, accountId: string): Promise<boolean> {
1201
1281
  const removed = await mutateStore(store => {
1202
1282
  const set = store[provider];
@@ -1208,7 +1288,11 @@ export async function removeAccount(provider: string, accountId: string): Promis
1208
1288
  delete store[provider];
1209
1289
  return true;
1210
1290
  }
1211
- if (set.activeAccountId === accountId) set.activeAccountId = set.accounts[0]!.id;
1291
+ if (set.activeAccountId === accountId) {
1292
+ const next = set.accounts.find(account => account.paused !== true && account.needsReauth !== true)
1293
+ ?? set.accounts[0]!;
1294
+ set.activeAccountId = next.id;
1295
+ }
1212
1296
  return true;
1213
1297
  }, [provider, accountId], { scrubLegacyBackup: removed => removed ? [provider] : [] });
1214
1298
  return removed;
@@ -1238,6 +1322,7 @@ export async function replaceProviderAccountSet(
1238
1322
  credential: { ...account.credential, ...(account.credential.kiro ? { kiro: { ...account.credential.kiro } } : {}) },
1239
1323
  ...(account.alias ? { alias: account.alias } : {}),
1240
1324
  ...(account.needsReauth ? { needsReauth: true } : {}),
1325
+ ...(account.paused ? { paused: true } : {}),
1241
1326
  ...(account.addedAt !== undefined ? { addedAt: account.addedAt } : {}),
1242
1327
  ...(account.loginId ? { loginId: account.loginId } : {}),
1243
1328
  })),
@@ -14,7 +14,7 @@
14
14
  import { loadConfig } from "../config";
15
15
  import type { OcxConfig, OcxTokenGuardianConfig } from "../types";
16
16
  import { listAccounts } from "./store";
17
- import { getValidAccessTokenForAccount, listOAuthProviders, OAuthLoginRequiredError, resolveRefreshPolicy } from "./index";
17
+ import { getValidAccessTokenForAccount, listOAuthProviders, OAuthAccountPausedError, OAuthLoginRequiredError, resolveRefreshPolicy } from "./index";
18
18
  import {
19
19
  getValidCodexToken,
20
20
  listCodexAccountIds,
@@ -141,11 +141,12 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
141
141
  const tasks: Array<() => Promise<void>> = [];
142
142
 
143
143
  // A) OAuth providers — every account in each provider's set (multiauth keep-alive),
144
- // skipping accounts already marked needsReauth (terminal; only a re-login fixes them).
144
+ // skipping accounts already marked needsReauth (terminal; only a re-login fixes them) or
145
+ // paused by the operator (manual exclusion from all automatic account use).
145
146
  for (const provider of listOAuthProviders()) {
146
147
  if (resolveRefreshPolicy(provider, config) !== "proactive") continue;
147
148
  for (const account of listAccounts(provider)) {
148
- if (account.needsReauth) continue;
149
+ if (account.needsReauth || account.paused) continue;
149
150
  if (account.credential.expires > nowMs + horizonMs) continue;
150
151
  const key = `oauth:${provider}:${account.id}`;
151
152
  if (inBackoff(key, nowMs)) { result.skippedBackoff.push(key); continue; }
@@ -155,6 +156,9 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
155
156
  backoff.delete(key);
156
157
  result.refreshed.push(key);
157
158
  } catch (err) {
159
+ // The account may have been paused after this task was queued. It is no longer an
160
+ // automatic refresh failure and must not acquire retry backoff.
161
+ if (err instanceof OAuthAccountPausedError) return;
158
162
  // Terminal grant failures surface as OAuthLoginRequiredError (account marked
159
163
  // needsReauth by the resolver) — back off at the ceiling; transient errors backoff exponentially.
160
164
  const permanent = err instanceof OAuthLoginRequiredError;
@@ -88,6 +88,8 @@ export interface ProviderAccount {
88
88
  credential: OAuthCredentials;
89
89
  /** Terminal refresh failure (invalid_grant / reused / revoked) — re-login required. */
90
90
  needsReauth?: boolean;
91
+ /** Operator exclusion from generic OAuth account selection until explicitly resumed. */
92
+ paused?: boolean;
91
93
  addedAt?: number;
92
94
  }
93
95
 
@@ -81,10 +81,12 @@ export function macAclListingTrustError(listing: string, currentUser = userInfo(
81
81
  if (!entry) { unparseable = true; continue; }
82
82
  if (entry[2] === "deny") continue;
83
83
  const principal = entry[1]!;
84
- // The file owner, this process's user, and root already control the path without an ACE.
85
- // Require the `user:` prefix: a bare or group principal might include other users.
84
+ // The file owner and this process's user already control the path without an ACE. Require the
85
+ // `user:` prefix: a bare or group principal might include other users. macOS `ls` prints a
86
+ // resolved directory-record NAME here, so neither `0` nor `root` proves that the ACE is UID 0.
87
+ // On a genuinely root-owned path, `root` is still admitted by the owner comparison.
86
88
  if (principal.startsWith("user:")
87
- && [owner, currentUser, "root", "0"].includes(principal.slice(5))) continue;
89
+ && [owner, currentUser].includes(principal.slice(5))) continue;
88
90
  const rights = entry[3]!.split(",");
89
91
  if (rights.includes("only_inherit")) continue;
90
92
  if (rights.some(right => !MAC_ACL_BENIGN_TOKENS.has(right))) return "has an access control list";
@@ -340,6 +340,9 @@ export function createAdapterTierMetadata(
340
340
  const loggedWireValue = wireValue === null ? null : sanitizeLogMetadataString(wireValue);
341
341
  const outcome: AttemptTierOutcome = {
342
342
  wireKind,
343
+ ...(context.responseTierAuthoritative !== undefined
344
+ ? { responseTierAuthoritative: context.responseTierAuthoritative }
345
+ : {}),
343
346
  ...(wireValue === null
344
347
  ? { wireValue: null }
345
348
  : loggedWireValue ? { wireValue: loggedWireValue } : {}),
@@ -12,7 +12,7 @@ import { commitProviderApiKeySelection } from "./api-key-selection";
12
12
  import type { ProviderApiKeySelection } from "../types/provider";
13
13
  import { routedProviderConfig } from "../router";
14
14
  import { getProviderRegistryEntry } from "./registry";
15
- import { normalizedBaseUrl } from "./quota/vendor-probes-key";
15
+ import { isCanonicalCommandCodeBaseUrl, normalizedBaseUrl } from "./quota/vendor-probes-key";
16
16
  import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, ResetReplayPolicy, TransientRetryPolicy } from "../types";
17
17
  import { OPENCODE_GO_SESSION_HEADER } from "./opencode-go-transport";
18
18
  import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
@@ -566,7 +566,8 @@ export function selectProactiveApiKeyTransport(
566
566
  * `enabled: false` to opt out). When the knob is absent, the OpenCode Go destination
567
567
  * (subscription traffic such as Muse Spark) falls back to a patient same-key policy so a
568
568
  * burst 429 waits and replays instead of surfacing to the client and aborting a long
569
- * session; every other provider without the knob keeps today's fail-fast behavior.
569
+ * session; key-auth Command Code at its canonical endpoints gets the same fallback (#5180), and
570
+ * every other provider without the knob keeps today's fail-fast behavior.
570
571
  * OAuth/forward/local credentials are never replayed on the same token. The returned
571
572
  * policy is fully defaulted so callers never re-check fields.
572
573
  */
@@ -590,10 +591,17 @@ export function rateLimitRetryPolicyFor(
590
591
  respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
591
592
  };
592
593
  }
593
- // No explicit knob: patient fallback for the OpenCode Go destination only.
594
+ // No explicit knob: patient fallback for OpenCode Go and canonical Command Code only.
594
595
  if (provider.authMode !== undefined && provider.authMode !== "key") return null;
595
- if (!isOpenCodeGoDestination(provider)) return null;
596
- return { ...OPENCODE_GO_RATE_LIMIT_RETRY };
596
+ if (isOpenCodeGoDestination(provider)) return { ...OPENCODE_GO_RATE_LIMIT_RETRY };
597
+ // Command Code's Provider API rate-limits long muse-spark turns the same way (#5180): a single
598
+ // key cannot fail over, and the Codex client does not retry a 429, so the turn aborts. The key
599
+ // endpoint gets the same patient same-key policy. A row repointed at a custom relay is not the
600
+ // canonical endpoint and keeps fail-fast unless it sets `retryOn429`.
601
+ if (typeof provider.baseUrl === "string" && isCanonicalCommandCodeBaseUrl(provider.baseUrl.trim())) {
602
+ return { ...OPENCODE_GO_RATE_LIMIT_RETRY };
603
+ }
604
+ return null;
597
605
  }
598
606
 
599
607
  /**
@@ -27,7 +27,8 @@ export function kiroModelDiscoveryEnabled(): boolean {
27
27
  }
28
28
 
29
29
  function liveAccount(accountId: string): ProviderAccount | undefined {
30
- return getAccountSet("kiro")?.accounts.find(account => account.id === accountId && account.needsReauth !== true);
30
+ return getAccountSet("kiro")?.accounts.find(account => account.id === accountId
31
+ && account.needsReauth !== true && account.paused !== true);
31
32
  }
32
33
 
33
34
  function currentIdentity(accountId: string): string | undefined {
@@ -74,7 +75,7 @@ export function refreshKiroAccountModelsDetached(
74
75
  providerConfig: OcxProviderConfig,
75
76
  dependencies: ProviderOutboundDependencies = {},
76
77
  ): void {
77
- if (!kiroModelDiscoveryEnabled() || account.needsReauth === true) return;
78
+ if (!kiroModelDiscoveryEnabled() || account.needsReauth === true || account.paused === true) return;
78
79
  const identity = kiroEvidenceIdentity(account);
79
80
  if (currentIdentity(account.id) !== identity) return;
80
81
  const old = validRow(account);
@@ -138,7 +139,7 @@ export function kiroAccountSupportsModel(accountId: string, model: string): bool
138
139
  export function kiroObservedContextWindow(model: string): number | undefined {
139
140
  const normalized = normalizeKiroModelId(model);
140
141
  if (normalized === "auto") return undefined;
141
- const accounts = getAccountSet("kiro")?.accounts.filter(account => account.needsReauth !== true) ?? [];
142
+ const accounts = getAccountSet("kiro")?.accounts.filter(account => account.needsReauth !== true && account.paused !== true) ?? [];
142
143
  const limits: number[] = [];
143
144
  let unknown = accounts.length === 0;
144
145
  for (const account of accounts) {
@@ -16,6 +16,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
16
16
  mcpMaxResultBytes: "none",
17
17
  modelAdapters: "record",
18
18
  fastWire: "none",
19
+ responseTierAuthoritative: "none",
19
20
  baseUrl: "none",
20
21
  responsesPath: "none",
21
22
  chatCompletionsPath: "none",
@@ -25,6 +25,15 @@ export function isCanonicalOpenAiForwardProvider(provider: OcxProviderConfig): b
25
25
  && normalizedBaseUrl(provider.baseUrl) === CODEX_FORWARD_BASE_URL;
26
26
  }
27
27
 
28
+ /**
29
+ * Response evidence is separate from Fast capability and request serialization. Preserve the
30
+ * known Codex exception (#2558); other destinations may declare their response contract without
31
+ * a gateway-name or URL heuristic. Absence retains the authoritative legacy default.
32
+ */
33
+ export function responseTierAuthorityForProvider(provider: OcxProviderConfig): boolean | undefined {
34
+ return isCanonicalOpenAiForwardProvider(provider) ? false : provider.responseTierAuthoritative;
35
+ }
36
+
28
37
  const OPENAI_API_ORIGIN = "https://api.openai.com";
29
38
  const OPENAI_API_BASE_URL = `${OPENAI_API_ORIGIN}/v1`;
30
39
  const OPENAI_API_RESPONSES_URL = `${OPENAI_API_BASE_URL}/responses`;