cartethyia 2.6.2 → 2.6.4

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 (142) hide show
  1. package/dist/dashboard/assets/{BannedPage-BMFQM5pm.js → BannedPage-rvAOt5Ad.js} +1 -1
  2. package/dist/dashboard/assets/BannedPage-rvAOt5Ad.js.gz +0 -0
  3. package/dist/dashboard/assets/{CliToolDetailPage-BP-8qZqc.js → CliToolDetailPage-ZoKZDsob.js} +1 -1
  4. package/dist/dashboard/assets/CliToolDetailPage-ZoKZDsob.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolsPage-DpJeG_Op.js → CliToolsPage-1_a6Tw8Y.js} +1 -1
  6. package/dist/dashboard/assets/CliToolsPage-1_a6Tw8Y.js.gz +0 -0
  7. package/dist/dashboard/assets/{CombosPage-jNI-D44o.js → CombosPage-Czo7GLxL.js} +1 -1
  8. package/dist/dashboard/assets/CombosPage-Czo7GLxL.js.gz +0 -0
  9. package/dist/dashboard/assets/{ConsoleLogPage-CQpDMg8K.js → ConsoleLogPage-BrlBFGtF.js} +1 -1
  10. package/dist/dashboard/assets/ConsoleLogPage-BrlBFGtF.js.gz +0 -0
  11. package/dist/dashboard/assets/{CustomizationPage-DKU4KjlR.js → CustomizationPage-COvQuDTc.js} +1 -1
  12. package/dist/dashboard/assets/CustomizationPage-COvQuDTc.js.gz +0 -0
  13. package/dist/dashboard/assets/HealthEventsModal-gTD1Qv_d.js +1 -0
  14. package/dist/dashboard/assets/HealthEventsModal-gTD1Qv_d.js.gz +0 -0
  15. package/dist/dashboard/assets/{LoginPage-DaUYRu4n.js → LoginPage-BlGbMdYo.js} +1 -1
  16. package/dist/dashboard/assets/LoginPage-BlGbMdYo.js.gz +0 -0
  17. package/dist/dashboard/assets/{ModelPicker-DVny4S5_.js → ModelPicker-CA1kLs45.js} +1 -1
  18. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  19. package/dist/dashboard/assets/{OverviewPage-Cvo6sK7M.js → OverviewPage-BhUFw9Qp.js} +1 -1
  20. package/dist/dashboard/assets/OverviewPage-BhUFw9Qp.js.gz +0 -0
  21. package/dist/dashboard/assets/ProviderDetailPage-CrBfX_gj.js +3 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-CrBfX_gj.js.gz +0 -0
  23. package/dist/dashboard/assets/{ProvidersPage-CIo_CRe_.js → ProvidersPage-CZ7sCKxk.js} +1 -1
  24. package/dist/dashboard/assets/ProvidersPage-CZ7sCKxk.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProxyPage-BrsHDiRx.js → ProxyPage-PnJWpC9C.js} +1 -1
  26. package/dist/dashboard/assets/ProxyPage-PnJWpC9C.js.gz +0 -0
  27. package/dist/dashboard/assets/{QuotaPage-DkUwJFpM.js → QuotaPage-CrGTXWjQ.js} +1 -1
  28. package/dist/dashboard/assets/QuotaPage-CrGTXWjQ.js.gz +0 -0
  29. package/dist/dashboard/assets/{SettingsPage-BXK4gIFl.js → SettingsPage-DXjUbFh_.js} +1 -1
  30. package/dist/dashboard/assets/SettingsPage-DXjUbFh_.js.gz +0 -0
  31. package/dist/dashboard/assets/{SetupPage-BiIPnecp.js → SetupPage-DCFo3R5_.js} +1 -1
  32. package/dist/dashboard/assets/SetupPage-DCFo3R5_.js.gz +0 -0
  33. package/dist/dashboard/assets/{SortableList-CclP89S9.js → SortableList-lFdVPMo_.js} +1 -1
  34. package/dist/dashboard/assets/SortableList-lFdVPMo_.js.gz +0 -0
  35. package/dist/dashboard/assets/{StudioPage-Djt-Mk-N.js → StudioPage-DPbSWHny.js} +1 -1
  36. package/dist/dashboard/assets/StudioPage-DPbSWHny.js.gz +0 -0
  37. package/dist/dashboard/assets/{UsagePage-DaGf9BI1.js → UsagePage-B-CTPrqB.js} +1 -1
  38. package/dist/dashboard/assets/UsagePage-B-CTPrqB.js.gz +0 -0
  39. package/dist/dashboard/assets/app-BUSiYL6F.css +2 -0
  40. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  41. package/dist/dashboard/assets/{badge-CKsF123d.js → badge-Bkz_-RrM.js} +1 -1
  42. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  43. package/dist/dashboard/assets/{banner-8j7uChA9.js → banner-C-7y3VKV.js} +1 -1
  44. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  45. package/dist/dashboard/assets/{card-CdsSlWA3.js → card-BOPBuBzD.js} +1 -1
  46. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  47. package/dist/dashboard/assets/{clipboard-button-uL5aYLfW.js → clipboard-button-e-Mlk0FH.js} +1 -1
  48. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  49. package/dist/dashboard/assets/{dialog-C07hr5_G.js → dialog-Iec_ywbD.js} +1 -1
  50. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  51. package/dist/dashboard/assets/{drawer-BPO1TcKH.js → drawer-Bs8MIIHc.js} +1 -1
  52. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  53. package/dist/dashboard/assets/{entry-Bz_CtDfC.js → entry-BRszm4_Z.js} +1 -1
  54. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  55. package/dist/dashboard/assets/{entry-BY4QChsA.js → entry-BcfdW4C_.js} +3 -3
  56. package/dist/dashboard/assets/entry-BcfdW4C_.js.gz +0 -0
  57. package/dist/dashboard/assets/{entry-GJRbmrpA.js → entry-DcobaKFu.js} +1 -1
  58. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  59. package/dist/dashboard/assets/{github-badge-DmQreSDd.js → github-badge-BuDsrsPn.js} +1 -1
  60. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  61. package/dist/dashboard/assets/{index-isGWt18Z.js → index-_rbyCXD2.js} +2 -2
  62. package/dist/dashboard/assets/index-_rbyCXD2.js.gz +0 -0
  63. package/dist/dashboard/assets/{inline-C9dCYaBZ.js → inline-DToMICwl.js} +1 -1
  64. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  65. package/dist/dashboard/assets/{input-CjCnXweJ.js → input-WlyHWeG2.js} +1 -1
  66. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  67. package/dist/dashboard/assets/{layout-V6y8DAkl.js → layout-DaJ5baJr.js} +1 -1
  68. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  69. package/dist/dashboard/assets/{list-row-CM1AaG_y.js → list-row-B3oSK13z.js} +1 -1
  70. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  71. package/dist/dashboard/assets/{numbers-BOevJzdz.js → numbers-Dulqgys5.js} +1 -1
  72. package/dist/dashboard/assets/numbers-Dulqgys5.js.gz +0 -0
  73. package/dist/dashboard/assets/{select-Dpp8tE7Z.js → select-BTEzH1iH.js} +1 -1
  74. package/dist/dashboard/assets/select-BTEzH1iH.js.gz +0 -0
  75. package/dist/dashboard/assets/{stack-_gfTdLVB.js → stack-B1w5BM0u.js} +1 -1
  76. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  77. package/dist/dashboard/assets/{state-2c5V4zo7.js → state-DhZBfWgx.js} +1 -1
  78. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  79. package/dist/dashboard/assets/{switch-Dkke7Naf.js → switch-yD7D99Lg.js} +1 -1
  80. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  81. package/dist/dashboard/index.html +1 -1
  82. package/dist/dashboard/index.html.gz +0 -0
  83. package/dist/dashboard/precompressed-assets.json +41 -41
  84. package/package.json +1 -1
  85. package/src/boot/app.ts +11 -1
  86. package/src/console/backup/store.ts +10 -2
  87. package/src/console/providers/catalog/contracts.ts +7 -0
  88. package/src/console/providers/catalog/store.ts +25 -2
  89. package/src/observability/event-query.ts +10 -5
  90. package/src/providers/integrations/buddy/buddy-chat-shared.ts +105 -0
  91. package/src/providers/integrations/buddy/buddy-cli-headers.ts +24 -20
  92. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -2
  93. package/src/providers/integrations/buddy/codebuddy/provider.ts +15 -15
  94. package/src/providers/integrations/buddy/codebuddy/shared.ts +7 -5
  95. package/src/providers/integrations/buddy/workbuddy/provider.ts +13 -12
  96. package/src/providers/integrations/buddy/workbuddy/shared.ts +14 -4
  97. package/src/providers/operations/account-health-service.ts +211 -17
  98. package/dist/dashboard/assets/BannedPage-BMFQM5pm.js.gz +0 -0
  99. package/dist/dashboard/assets/CliToolDetailPage-BP-8qZqc.js.gz +0 -0
  100. package/dist/dashboard/assets/CliToolsPage-DpJeG_Op.js.gz +0 -0
  101. package/dist/dashboard/assets/CombosPage-jNI-D44o.js.gz +0 -0
  102. package/dist/dashboard/assets/ConsoleLogPage-CQpDMg8K.js.gz +0 -0
  103. package/dist/dashboard/assets/CustomizationPage-DKU4KjlR.js.gz +0 -0
  104. package/dist/dashboard/assets/HealthEventsModal-DUKqhSoT.js +0 -1
  105. package/dist/dashboard/assets/HealthEventsModal-DUKqhSoT.js.gz +0 -0
  106. package/dist/dashboard/assets/LoginPage-DaUYRu4n.js.gz +0 -0
  107. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  108. package/dist/dashboard/assets/OverviewPage-Cvo6sK7M.js.gz +0 -0
  109. package/dist/dashboard/assets/ProviderDetailPage-BrL6Yl9w.js +0 -3
  110. package/dist/dashboard/assets/ProviderDetailPage-BrL6Yl9w.js.gz +0 -0
  111. package/dist/dashboard/assets/ProvidersPage-CIo_CRe_.js.gz +0 -0
  112. package/dist/dashboard/assets/ProxyPage-BrsHDiRx.js.gz +0 -0
  113. package/dist/dashboard/assets/QuotaPage-DkUwJFpM.js.gz +0 -0
  114. package/dist/dashboard/assets/SettingsPage-BXK4gIFl.js.gz +0 -0
  115. package/dist/dashboard/assets/SetupPage-BiIPnecp.js.gz +0 -0
  116. package/dist/dashboard/assets/SortableList-CclP89S9.js.gz +0 -0
  117. package/dist/dashboard/assets/StudioPage-Djt-Mk-N.js.gz +0 -0
  118. package/dist/dashboard/assets/UsagePage-DaGf9BI1.js.gz +0 -0
  119. package/dist/dashboard/assets/app-CEeaMHOy.css +0 -2
  120. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  121. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  122. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  123. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  124. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  125. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  126. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  127. package/dist/dashboard/assets/entry-BY4QChsA.js.gz +0 -0
  128. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  129. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  130. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  131. package/dist/dashboard/assets/index-isGWt18Z.js.gz +0 -0
  132. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  133. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  134. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  135. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  136. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  137. package/dist/dashboard/assets/select-Dpp8tE7Z.js.gz +0 -0
  138. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  139. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  140. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  141. /package/dist/dashboard/assets/{app-2KhtHCv9.js → app-MdVfwraB.js} +0 -0
  142. /package/dist/dashboard/assets/{app-2KhtHCv9.js.gz → app-MdVfwraB.js.gz} +0 -0
@@ -160,7 +160,29 @@ export type AccountErrorCategory =
160
160
  | "model_capacity"
161
161
  | "model_unreachable"
162
162
  | "auth_invalidated"
163
+ /**
164
+ * Deprecated: an upstream ban. Kept so existing health rows keep rendering
165
+ * instead of showing a blank cause. No longer produced — see
166
+ * `policy_blocked_banned` and `policy_blocked_invalid_request`.
167
+ */
163
168
  | "policy_blocked"
169
+ /**
170
+ * The upstream banned this credential: a verdict on the account, e.g. buddy
171
+ * `11140` "request illegal" with "Your account has been restricted due to
172
+ * unusual activity". Cooldown cannot help — the account is not throttled, it
173
+ * is refused. Disabling immediately is a *feature*: a client's retry budget
174
+ * is spent in seconds, so surviving attempts burn sibling accounts that are
175
+ * still healthy.
176
+ */
177
+ | "policy_blocked_banned"
178
+ /**
179
+ * The upstream refused this request: a verdict on the request. e.g. buddy
180
+ * `11-128` "request illegal" with no account restriction. Distinguished from
181
+ * {@link policy_blocked_banned} because the correct responses differ — a
182
+ * banned account must be disabled, a malformed request must only stop being
183
+ * retried verbatim.
184
+ */
185
+ | "policy_blocked_invalid_request"
164
186
  | "server_error"
165
187
  | "timeout"
166
188
  | "unknown";
@@ -338,6 +360,35 @@ function accountDeadlineMs(
338
360
  return latest > 0 ? new Date(latest) : null;
339
361
  }
340
362
 
363
+ /**
364
+ * Whether the upstream named a restriction on the account itself.
365
+ *
366
+ * The buddy family reuses "request illegal" for two unrelated rejections, and
367
+ * the difference is only in the body:
368
+ *
369
+ * - `11140` carries `displayMsg` about the account being restricted ("Your
370
+ * account has been restricted due to unusual activity") and `actions`
371
+ * offering an appeal. That is a ban — the account is refused, not throttled.
372
+ * - `11-128` is a refusal of this request; the account is fine.
373
+ *
374
+ * Proven account-scoped (2026-10-16): two live share-page requests carrying
375
+ * byte-identical bodies and headers to the same model differed only in the
376
+ * pooled account, and the blocked one 400'd while the other 200'd.
377
+ *
378
+ * Matched on the code *and* the account-restriction wording, so an unrelated
379
+ * upstream that happens to say "restricted" in prose cannot disable accounts.
380
+ */
381
+ function isBuddyAccountBanned(lower: string, providerCode: string): boolean {
382
+ if (providerCode !== "11140") return false;
383
+ return (
384
+ lower.includes("restricted") ||
385
+ lower.includes("has been banned") ||
386
+ lower.includes("unusual activity") ||
387
+ lower.includes("appeal this restriction") ||
388
+ lower.includes("submit feedback to appeal")
389
+ );
390
+ }
391
+
341
392
  /**
342
393
  * Bounded to a day, matching `MAX_COOLDOWN_MS` in the failure policy: a stated
343
394
  * window longer than that is a monthly or plan-level limit, and parking an
@@ -421,6 +472,12 @@ interface AccountErrorContext {
421
472
  readonly canMutate: boolean;
422
473
  readonly providerCode: string;
423
474
  readonly statedCooldownMs: number | null;
475
+ /**
476
+ * The upstream named a restriction on the account, not a refusal of the
477
+ * request. Read by the policy arm to pick between disabling the account and
478
+ * merely refusing the request.
479
+ */
480
+ readonly buddyBanned: boolean;
424
481
  readonly reason: (prefix: string) => string;
425
482
  readonly result: (
426
483
  category: AccountErrorCategory,
@@ -476,6 +533,13 @@ interface AccountErrorSignals {
476
533
  readonly credentialKind: string | undefined;
477
534
  /** Buddy-family provider: the only family with provider-specific codes below. */
478
535
  readonly buddy: boolean;
536
+ /**
537
+ * The upstream named a restriction on this account, not a refusal of the
538
+ * request. Only the `11140` shape carries `displayMsg` about the account
539
+ * being restricted, so the policy arm can tell a ban from a bad request
540
+ * instead of treating both as one cooldown.
541
+ */
542
+ readonly buddyBanned: boolean;
479
543
  readonly priceRefusal: boolean;
480
544
  readonly deploymentInfraError: boolean;
481
545
  readonly quotaSignal: boolean;
@@ -601,6 +665,7 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
601
665
  canMutate,
602
666
  providerCode,
603
667
  statedCooldownMs,
668
+ buddyBanned: isBuddyAccountBanned(lower, providerCode),
604
669
  reason,
605
670
  result,
606
671
  };
@@ -623,6 +688,7 @@ export function classifyAccountError(
623
688
  providerCode,
624
689
 
625
690
  statedCooldownMs,
691
+ buddyBanned,
626
692
  reason,
627
693
  result,
628
694
  } = accountErrorContext(error, options);
@@ -761,19 +827,46 @@ export function classifyAccountError(
761
827
  lower.includes("request illegal") ||
762
828
  lower.includes("content blocked") ||
763
829
  lower.includes("account suspended");
830
+ // A ban is a verdict on the credential, and cooldown cannot answer it: the
831
+ // account is refused, not throttled. Disabling is the fix, and disabling
832
+ // *immediately* is a feature — a client's retry budget is spent in seconds,
833
+ // so a banned account that merely cooled stayed in the plan and burned
834
+ // sibling accounts that were still healthy before the window expired.
835
+ //
836
+ // The two shapes are told apart by the body, not the code alone: `11140`
837
+ // names a restriction on the account, `11-128` refuses the request.
764
838
  const policyBlock =
765
839
  policyRejection &&
766
840
  options?.providerId !== undefined &&
767
841
  BUDDY_PROVIDER_IDS.has(options.providerId.toLowerCase());
768
842
  if (policyBlock) {
769
- const policyBlockMs = resolveAccountPolicyBlockCooldownMs();
843
+ if (buddyBanned) {
844
+ return result(
845
+ "policy_blocked_banned",
846
+ "disabled",
847
+ 0,
848
+ null,
849
+ reason("Account banned by upstream"),
850
+ // A ban is evidence about the credential even at provider scope, for
851
+ // the same reason a named revocation is: the message is the evidence.
852
+ // Without this a stream frame's missing credential detail would leave a
853
+ // banned account serving.
854
+ true,
855
+ );
856
+ }
857
+ // Not a ban: the upstream refused this request. The account is fine, so the
858
+ // only correct move is to stop retrying it verbatim. Cooling the account
859
+ // here was the bug that made one malformed request look like a dead
860
+ // credential.
770
861
  return result(
771
- "policy_blocked",
862
+ "policy_blocked_invalid_request",
772
863
  "cooldown",
773
- policyBlockMs,
774
- new Date(Date.now() + policyBlockMs),
775
- reason("Provider content-policy block"),
776
- origin === "upstream",
864
+ 0,
865
+ null,
866
+ reason("Upstream refused this request"),
867
+ // Never mutates: a malformed body fails identically on every account, so
868
+ // cooling would burn a healthy credential on the gateway's own fault.
869
+ false,
777
870
  );
778
871
  }
779
872
  // These codes are a verdict on the request, not the account. Cooling the
@@ -829,14 +922,30 @@ export function classifyAccountError(
829
922
  options?.providerId !== undefined &&
830
923
  BUDDY_PROVIDER_IDS.has(options.providerId.toLowerCase());
831
924
  if (channelBlock) {
832
- const channelBlockMs = resolveAccountPolicyBlockCooldownMs();
925
+ // Same split as the policy arm above: a ban disables, a channel refusal
926
+ // only stops this request. `11-128` is the request shape, so this branch
927
+ // nearly always takes the refusal path — but it shares the ban test so a
928
+ // `11140`-shaped body arriving as a channel rejection still disables.
929
+ if (buddyBanned) {
930
+ return result(
931
+ "policy_blocked_banned",
932
+ "disabled",
933
+ 0,
934
+ null,
935
+ reason("Account banned by upstream"),
936
+ true,
937
+ );
938
+ }
833
939
  return result(
834
- "policy_blocked",
940
+ "policy_blocked_invalid_request",
835
941
  "cooldown",
836
- channelBlockMs,
837
- new Date(Date.now() + channelBlockMs),
942
+ 0,
943
+ null,
838
944
  reason("Provider channel rejection"),
839
- origin === "upstream",
945
+ // The credential is valid; the upstream refuses this calling channel.
946
+ // Cooling was the bug: it kept a refused account in the plan, where a
947
+ // client's retry budget burned healthy siblings for the whole window.
948
+ false,
840
949
  );
841
950
  }
842
951
  // server-side tool (web search, x search, web fetch), not evidence about the
@@ -907,6 +1016,7 @@ export function classifyAccountError(
907
1016
  staticToken,
908
1017
  credentialKind,
909
1018
  buddy,
1019
+ buddyBanned,
910
1020
  priceRefusal,
911
1021
  deploymentInfraError,
912
1022
  quotaSignal,
@@ -941,6 +1051,62 @@ export function classifyAccountError(
941
1051
  * {@link AccountErrorRule}.
942
1052
  */
943
1053
  const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1054
+ {
1055
+ // Must precede "rate-limit": this rejection is a 429 whose own text says
1056
+ // "rate limit", so the broader arm claimed it and cooled the account for
1057
+ // the full 15-minute rate-limit window.
1058
+ //
1059
+ // A full max-inflight is a statement about the gateway's own routing, not
1060
+ // about the credential: every account serving the model is busy right now.
1061
+ // Cooling them marks healthy accounts as `cooldown` for a limit their
1062
+ // provider never applied, which is what made every account read busy while
1063
+ // incoming requests piled up — the accounts were never the problem, the
1064
+ // ceiling was.
1065
+ name: "gateway-capacity-exhausted",
1066
+ match: (s) =>
1067
+ s.lower.includes("at its concurrency limit") ||
1068
+ s.lower.includes("every proxy pool for this provider is at its concurrency limit") ||
1069
+ s.lower.includes("the gateway is at its concurrency limit"),
1070
+ decide: (s) =>
1071
+ s.result(
1072
+ "model_capacity",
1073
+ // `status` is the status the account would move to; it is never
1074
+ // applied because `mutatesAccount` is false below, so this names the
1075
+ // shape rather than a transition. Zero cooldown is the point: no wait
1076
+ // is attached to the account, so the next request may use it at once.
1077
+ "cooldown",
1078
+ 0,
1079
+ null,
1080
+ s.reason("Gateway concurrency ceiling reached; no account was at fault"),
1081
+ // Never mutates: there is nothing wrong with the account to record.
1082
+ false,
1083
+ ),
1084
+ },
1085
+ {
1086
+ // Must precede the unclassified fallback: the fallback is account-blind and
1087
+ // would stamp this as a provider-wide failure, which is how one proxy
1088
+ // refusing CONNECT put every account of every provider behind that pool
1089
+ // into cooldown — opencode's dead front door taking codebuddy down with it.
1090
+ //
1091
+ // A CONNECT refusal is a statement about the proxy pool, not about a
1092
+ // credential: the tunnel was never established, so no upstream ever saw the
1093
+ // request and no account can be at fault. Scoped to the pool so the
1094
+ // provider's own accounts stay in rotation.
1095
+ name: "proxy-connect-refusal",
1096
+ match: (s) =>
1097
+ s.lower.includes("proxy connect failed") ||
1098
+ (s.lower.includes("connect") && s.lower.includes("tunnel") && s.lower.includes("refus")),
1099
+ decide: (s) =>
1100
+ s.result(
1101
+ "model_unreachable",
1102
+ "cooldown",
1103
+ 0,
1104
+ null,
1105
+ s.reason("Proxy pool refused the CONNECT tunnel"),
1106
+ // Never mutates: the pool owns this failure, not the account.
1107
+ false,
1108
+ ),
1109
+ },
944
1110
  {
945
1111
  name: "deployment-infrastructure",
946
1112
  match: (s) => s.deploymentInfraError,
@@ -976,14 +1142,42 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
976
1142
  name: "content-policy-block",
977
1143
  match: (s) => s.policyRejection && s.buddy,
978
1144
  decide: (s) => {
979
- const policyBlockMs = resolveAccountPolicyBlockCooldownMs();
1145
+ // A ban is a verdict on the credential, and cooldown cannot answer it:
1146
+ // the account is refused, not throttled. Disabling is the fix, and
1147
+ // disabling *immediately* is a feature — a client's retry budget is spent
1148
+ // in seconds, so a banned account that merely cooled stayed in the plan
1149
+ // and burned sibling accounts that were still healthy before the window
1150
+ // expired.
1151
+ //
1152
+ // The two shapes are told apart by the body, not the code alone: `11140`
1153
+ // names a restriction on the account, `11-128` refuses the request.
1154
+ if (s.buddyBanned) {
1155
+ return s.result(
1156
+ "policy_blocked_banned",
1157
+ "disabled",
1158
+ 0,
1159
+ null,
1160
+ s.reason("Account banned by upstream"),
1161
+ // A ban is evidence about the credential even at provider scope, for
1162
+ // the same reason a named revocation is: the message is the evidence.
1163
+ // Without this a stream frame's missing credential detail would leave
1164
+ // a banned account serving.
1165
+ true,
1166
+ );
1167
+ }
1168
+ // Not a ban: the upstream refused this request. The account is fine, so
1169
+ // the only correct move is to stop retrying it verbatim. Cooling the
1170
+ // account here was the bug that made one malformed request look like a
1171
+ // dead credential.
980
1172
  return s.result(
981
- "policy_blocked",
1173
+ "policy_blocked_invalid_request",
982
1174
  "cooldown",
983
- policyBlockMs,
984
- new Date(Date.now() + policyBlockMs),
985
- s.reason("Provider content-policy block"),
986
- s.origin === "upstream",
1175
+ 0,
1176
+ null,
1177
+ s.reason("Upstream refused this request"),
1178
+ // Never mutates: a malformed body fails identically on every account,
1179
+ // so cooling would burn a healthy credential on the gateway's own fault.
1180
+ false,
987
1181
  );
988
1182
  },
989
1183
  },
@@ -1 +0,0 @@
1
- import{o as e,r as t,t as n}from"./jsx-runtime-CXUMngI_.js";import{i as r}from"./terminal-0_IjPWTk.js";import{c as i,s as a}from"./numbers-BOevJzdz.js";import{n as o,r as s,t as c}from"./state-2c5V4zo7.js";import{t as l}from"./dialog-C07hr5_G.js";import{t as u}from"./rotate-ccw-CufcqITi.js";import{t as d}from"./trash-Cppn5c3R.js";import{C as f,T as p,v as m}from"./assert-DU6qrMA_.js";import{o as h,t as g}from"./query-keys-UETqfytp.js";import{t as _}from"./useMutation-EDFRkn1i.js";import{k as v,n as y,v as b,y as x,z as S}from"./entry-BY4QChsA.js";import{t as C}from"./badge-CKsF123d.js";import{n as w}from"./inline-C9dCYaBZ.js";import{t as T}from"./layout-V6y8DAkl.js";async function E(e){return h(`/quota/overview`,e?{signal:e}:void 0)}function D(){return f({queryKey:g.quota.all,queryFn:e=>E(m(e)),staleTime:1/0,refetchInterval:!1,refetchIntervalInBackground:!1,refetchOnMount:!1,refetchOnWindowFocus:!1,placeholderData:e=>e})}function O(e){let t=p(),n=g.quota.account(e);return _({mutationFn:async()=>h(`/accounts/${encodeURIComponent(e)}/quota/refresh`,{method:`POST`}),onSuccess:async()=>{await Promise.all([t.invalidateQueries({queryKey:n,exact:!0}),t.invalidateQueries({queryKey:g.quota.all}),t.invalidateQueries({queryKey:g.providers.all})])}})}function k(){let e=p();return _({mutationFn:async({accountIds:e,onProgress:t})=>{let n=e.length,r=0,i=await h(`/quota/refresh`,{method:`POST`,body:JSON.stringify({accountIds:e})});return r=i.queued,t?.(r,n),i},onSuccess:async(t,n)=>{let r=n.accountIds,i=new Set(t.failures.map(e=>e.accountId)),a=r.filter(e=>!i.has(e));await Promise.all([...a.map(t=>e.invalidateQueries({queryKey:g.quota.account(t),exact:!0})),e.invalidateQueries({queryKey:g.quota.all})])}})}function A(){let e=p();return _({mutationFn:({id:e,active:t})=>h(`/accounts/${encodeURIComponent(e)}`,{method:`PATCH`,body:JSON.stringify({active:t})}),onSuccess:async()=>{await Promise.all([e.invalidateQueries({queryKey:g.quota.all}),e.invalidateQueries({queryKey:g.providers.all})])}})}function j(){let e=p();return _({mutationFn:async({ids:e,active:t})=>{e.length!==0&&await h(`/accounts/batch`,{method:`PATCH`,body:JSON.stringify({ids:e,active:t})})},onSuccess:async()=>{await Promise.all([e.invalidateQueries({queryKey:g.quota.all}),e.invalidateQueries({queryKey:g.providers.all})])}})}function M(){let e=p();return _({mutationFn:async e=>{await h(`/accounts/${encodeURIComponent(e)}`,{method:`DELETE`})},onSuccess:async(t,n)=>{e.removeQueries({queryKey:g.quota.account(n),exact:!0}),await Promise.all([e.invalidateQueries({queryKey:g.quota.all}),e.invalidateQueries({queryKey:g.providers.all})])}})}var N=new Set([`workbuddy`,`workbuddycn`,`cb`,`cbcn`]);function P(e){return N.has(e.trim().toLowerCase())}function F(e){let t=p();return _({mutationFn:async()=>{if(e.length===0)throw Error(`Select a provider first`);return h(`/quota/daily-growth`,{method:`POST`,body:JSON.stringify({providerId:e})})},onSuccess:async()=>{await Promise.all([t.invalidateQueries({queryKey:g.quota.all}),t.invalidateQueries({queryKey:g.providers.all})])}})}function I(e){let t=p();return _({mutationFn:async()=>{let t=await h(`/accounts/${encodeURIComponent(e)}/activity-report`,{method:`POST`}),n=t.message??`Activity reported`,r=await h(`/accounts/${encodeURIComponent(e)}/checkin`,{method:`POST`}),i=r.message??`Daily check-in done`;return{ok:t.ok,message:t.ok?`Report success · Check-in ${i}`:`Report failed: ${n} · Check-in ${i}`,checkin:r,report:t}},onSuccess:async()=>{await Promise.all([t.invalidateQueries({queryKey:g.quota.all}),t.invalidateQueries({queryKey:g.providers.all})])}})}var L=new Set([`codex`,`claude`,`anthropic`]);function R(e){return L.has(e.trim().toLowerCase())}function z(e,t){return f({queryKey:g.quota.resets(e),queryFn:({signal:t})=>h(`/accounts/${encodeURIComponent(e)}/resets`,t?{signal:t}:void 0),enabled:t,staleTime:3e4})}function B(e){let t=p();return _({mutationFn:async t=>h(`/accounts/${encodeURIComponent(e)}/reset`,{method:`POST`,body:JSON.stringify(t??{})}),onSettled:async()=>{await Promise.all([t.invalidateQueries({queryKey:g.quota.all}),t.invalidateQueries({queryKey:g.providers.all})])}})}var V=e(t(),1),H=n();function U(e){return e===`cooldown`}function W(e){return e===`active`?`ok`:e===`disabled`?`disabled`:`warn`}function G(e,t){return e?{title:`Enable static-token mode?`,message:`${t} will be used exactly as issued and will not be refreshed. Re-enable refresh before this token expires.`,confirmLabel:`Enable static mode`}:{title:`Re-enable credential refresh?`,message:`${t} will return to the OAuth refresh sweep. Use this only when a refresh token is available.`,confirmLabel:`Re-enable refresh`}}function K({summary:e,onClose:t}){let{providerId:n,accountId:f,title:p,status:m,errorCategory:h,errorMessage:g}=e,_=y(n,f),E=x(),D=_.data??[],O=v(),k=e.credentialKind===`oauth`||e.credentialMode===`jwt`||e.staticToken===!0,[A,j]=(0,V.useState)(e.staticToken===!0),[M,N]=(0,V.useState)(null),[P,F]=(0,V.useState)(!1);(0,V.useEffect)(()=>j(e.staticToken===!0),[e.staticToken]);let I=b();return(0,H.jsx)(l,{open:!0,onClose:t,title:`Health & Error Log — ${p}`,size:`lg`,children:(0,H.jsxs)(`div`,{className:`flex flex-col gap-7 min-w-0`,children:[(0,H.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-5 rounded-control border border-inner-border bg-surface-2 py-5 px-7`,children:[(0,H.jsxs)(`div`,{className:`min-w-0`,children:[(0,H.jsxs)(w,{gap:`8px`,className:`flex-wrap`,children:[(0,H.jsx)(`span`,{className:`text-body font-semibold`,children:`Account status:`}),(0,H.jsx)(C,{tone:W(m),dot:!0,children:m.toUpperCase()}),h?(0,H.jsx)(C,{tone:`warn`,title:g??void 0,children:h}):null]}),g?(0,H.jsx)(`p`,{className:`text-badge text-red mt-2 wrap-anywhere`,children:g}):null]}),U(m)?(0,H.jsx)(i,{size:`sm`,variant:`secondary`,icon:(0,H.jsx)(u,{size:12,className:E.isPending?`animate-spin`:``}),disabled:E.isPending,onClick:()=>E.mutate({providerId:n,accountId:f},{onSuccess:()=>void _.refetch()}),children:E.isPending?`Recovering...`:`Recover Now`}):null]}),k?(0,H.jsxs)(`div`,{className:`flex items-center justify-between gap-6 rounded-control border border-inner-border bg-surface-2 py-5 px-7`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`text-caption font-semibold`,children:`Credential management`}),(0,H.jsx)(`div`,{className:`text-badge text-text-tertiary`,children:A?`Static mode: no refresh request will be sent.`:`OAuth refresh is enabled for this account.`})]}),(0,H.jsx)(i,{size:`sm`,variant:A?`primary`:`secondary`,disabled:O.isPending,onClick:()=>N(!A),children:A?`Static · no refresh`:`Enable static mode`})]}):null,(0,H.jsxs)(`div`,{className:`min-w-0`,children:[(0,H.jsxs)(`div`,{className:`flex items-center justify-between gap-5 mb-4`,children:[(0,H.jsxs)(`h4`,{className:`text-caption font-semibold text-text-secondary`,children:[`Transition Events & Audit (`,D.length,`)`]}),(0,H.jsx)(i,{size:`sm`,variant:`secondary`,icon:(0,H.jsx)(d,{size:12}),disabled:D.length===0||I.isPending,onClick:()=>F(!0),title:`Deletes this account's recorded transitions and audit entries.`,children:`Purge log`})]}),_.isPending?(0,H.jsx)(s,{compact:!0,label:`Loading health events...`}):_.isError?(0,H.jsx)(o,{compact:!0,message:`Failed to load health events`,onRetry:()=>void _.refetch()}):D.length===0?(0,H.jsx)(c,{compact:!0,title:`No health events recorded`,message:e.emptyMessage,icon:(0,H.jsx)(r,{size:20})}):(0,H.jsx)(`div`,{className:`rounded-inset border border-inner-border`,children:(0,H.jsx)(T,{maxHeight:480,scrollRegion:!0,headers:[`Timestamp`,`From → To`,`Model`,`Category`,`Reason / Details`],children:D.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`text-badge text-text-tertiary whitespace-nowrap`,children:new Date(e.createdAt).toLocaleString()}),(0,H.jsxs)(`td`,{className:`whitespace-nowrap`,children:[e.fromStatus!==null&&e.fromStatus!==e.toStatus?(0,H.jsxs)(`span`,{className:`text-badge text-text-secondary`,children:[e.fromStatus,` →`,` `]}):(0,H.jsx)(`span`,{className:`text-badge text-text-tertiary`,children:`logged · `}),(0,H.jsx)(C,{tone:W(e.toStatus),children:e.toStatus})]}),(0,H.jsx)(`td`,{className:`font-mono tabular-nums text-badge text-text-secondary wrap-anywhere`,children:e.modelId??`—`}),(0,H.jsx)(`td`,{className:`font-mono tabular-nums text-badge text-text-secondary`,children:e.errorCategory??`—`}),(0,H.jsx)(`td`,{className:`text-badge text-text-primary min-w-110 max-w-260 wrap-anywhere`,children:e.reason??`—`})]},e.id))})})]}),(0,H.jsx)(S,{open:M!==null,onClose:()=>N(null),onConfirm:()=>{let e=M===!0;O.mutate({providerId:n,accountId:f,request:{staticToken:e}},{onSuccess:()=>{j(e),N(null),a.success(e?`Static mode enabled`:`Credential refresh re-enabled`,p),_.refetch()},onError:e=>a.error(`Credential update failed`,e instanceof Error?e.message:`Unable to update credential mode`)})},title:G(M===!0,p).title,message:G(M===!0,p).message,confirmLabel:G(M===!0,p).confirmLabel,cancelLabel:`Cancel`}),(0,H.jsx)(S,{open:P,onClose:()=>F(!1),onConfirm:()=>{I.mutate({providerId:n,accountId:f,title:p},{onSuccess:()=>F(!1)})},title:`Purge the health & error log for ${p}?`,message:`Deletes every transition and audit entry recorded for this account. Its status, credentials, and counters are untouched. This cannot be undone.`,confirmLabel:`Purge log`,cancelLabel:`Cancel`}),(0,H.jsx)(w,{justify:`flex-end`,gap:`0px`,children:(0,H.jsx)(i,{variant:`secondary`,size:`sm`,onClick:t,children:`Close`})})]})})}export{M as a,k as c,I as d,F as f,z as i,j as l,P as n,D as o,A as p,R as r,O as s,K as t,B as u};