@bitkyc08/opencodex 2.75.0 → 2.76.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 (217) hide show
  1. package/README.md +6 -1
  2. package/bin/ocx.mjs +12 -1
  3. package/gui/dist/assets/App-CddorHdA.js +51 -0
  4. package/gui/dist/assets/App-DMudtISW.css +1 -0
  5. package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
  6. package/gui/dist/assets/index-Btze1I3T.css +1 -0
  7. package/gui/dist/assets/index-CvnY7E6i.js +86 -0
  8. package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/gui/dist/provider-icons/opengateway.svg +12 -0
  11. package/package.json +1 -1
  12. package/src/adapters/anthropic/account-metadata.ts +17 -0
  13. package/src/adapters/anthropic/client-identity.ts +69 -0
  14. package/src/adapters/anthropic/passthrough.ts +23 -4
  15. package/src/adapters/anthropic.ts +18 -1
  16. package/src/adapters/command-code-project-context.ts +7 -11
  17. package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
  18. package/src/adapters/kiro-retry.ts +23 -8
  19. package/src/adapters/openai-chat/messages.ts +1 -1
  20. package/src/adapters/registry.ts +8 -1
  21. package/src/adapters/zed.ts +377 -0
  22. package/src/bridge/sse.ts +4 -2
  23. package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
  24. package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
  25. package/src/chatgpt/app-server-shim/filter.ts +157 -0
  26. package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
  27. package/src/chatgpt/app-server-shim/launcher.ts +91 -0
  28. package/src/claude/desktop-3p-startup.ts +62 -0
  29. package/src/claude/desktop-3p.ts +5 -0
  30. package/src/claude/first-party-settings.ts +14 -0
  31. package/src/claude/intercept/cli-catalog.ts +150 -0
  32. package/src/claude/intercept/cli-picker.ts +82 -0
  33. package/src/claude/intercept/listener.ts +21 -6
  34. package/src/claude/intercept/picker-bootstrap.ts +23 -5
  35. package/src/claude/intercept/picker-models.ts +57 -13
  36. package/src/claude/intercept/runtime.ts +30 -2
  37. package/src/claude/intercept/settings.ts +3 -0
  38. package/src/cli/agent.ts +172 -0
  39. package/src/cli/alias.ts +2 -1
  40. package/src/cli/capabilities-command.ts +3 -2
  41. package/src/cli/capabilities.ts +87 -0
  42. package/src/cli/chatgpt-command.ts +140 -0
  43. package/src/cli/combo.ts +144 -19
  44. package/src/cli/config-command.ts +3 -1
  45. package/src/cli/dispatch.ts +10 -1
  46. package/src/cli/help.ts +1 -0
  47. package/src/cli/index.ts +5 -24
  48. package/src/cli/init.ts +16 -2
  49. package/src/cli/integrations.ts +13 -0
  50. package/src/cli/internal-command.ts +12 -5
  51. package/src/cli/observe.ts +6 -1
  52. package/src/cli/registry.ts +6 -0
  53. package/src/cli/route-policy.ts +3 -1
  54. package/src/cli/runtime-api.ts +5 -2
  55. package/src/cli/status-oauth.ts +5 -0
  56. package/src/cli/status.ts +8 -1
  57. package/src/cli/stop-approval.ts +55 -16
  58. package/src/cli/stop-report.ts +5 -0
  59. package/src/clients/lazycodex.ts +48 -0
  60. package/src/clients/omo-role-models.ts +111 -0
  61. package/src/codex/account-credit-use.ts +99 -0
  62. package/src/codex/account-lifecycle.ts +2 -0
  63. package/src/codex/agent-role-models.ts +171 -0
  64. package/src/codex/auth-api/account-list.ts +10 -3
  65. package/src/codex/auth-api/routes.ts +34 -0
  66. package/src/codex/auth-context.ts +52 -11
  67. package/src/codex/catalog/discovered-natives.ts +11 -1
  68. package/src/codex/catalog/gather-capture.ts +6 -1
  69. package/src/codex/catalog/model-hints.ts +9 -1
  70. package/src/codex/catalog/provider-models.ts +60 -0
  71. package/src/codex/catalog/routed-gather.ts +30 -5
  72. package/src/codex/convergence-types.ts +2 -1
  73. package/src/codex/main-account-external-usage.ts +62 -0
  74. package/src/codex/main-account-hard-lock.ts +24 -13
  75. package/src/codex/native-profile-processes.ts +53 -16
  76. package/src/codex/prompt-layers/toml-edit.ts +1 -1
  77. package/src/codex/quota-types.ts +3 -7
  78. package/src/codex/quota.ts +19 -4
  79. package/src/codex/role-auto-assign.ts +159 -0
  80. package/src/codex/role-sizing-limits.ts +2 -0
  81. package/src/codex/role-sizing.ts +216 -0
  82. package/src/codex/routing/selection.ts +36 -0
  83. package/src/codex/routing/thread-affinity.ts +2 -0
  84. package/src/codex/routing.ts +4 -4
  85. package/src/codex/subagent-model-fallback.ts +82 -26
  86. package/src/codex/subagent-selectable-models.ts +32 -0
  87. package/src/combos/index.ts +18 -0
  88. package/src/combos/jev-decision-contract.ts +20 -0
  89. package/src/combos/jev-dispatch.ts +18 -0
  90. package/src/combos/jev-model-backend.ts +127 -0
  91. package/src/combos/jev.ts +249 -33
  92. package/src/combos/types.ts +125 -2
  93. package/src/config/diagnostics.ts +18 -0
  94. package/src/config/macos-system-proxy.ts +72 -15
  95. package/src/config/owner-registry.ts +13 -4
  96. package/src/config/process-state.ts +17 -8
  97. package/src/config/proxy-env.ts +35 -5
  98. package/src/config/schema/config-schema.ts +30 -1
  99. package/src/config/schema/leaf-validators.ts +4 -0
  100. package/src/config/serving-runtimes.ts +95 -1
  101. package/src/generated/compatibility-version.json +336 -156
  102. package/src/lib/errors.ts +7 -7
  103. package/src/lib/jsonc.ts +13 -0
  104. package/src/lib/local-chat-completion.ts +144 -0
  105. package/src/lib/provider-outbound.ts +87 -1
  106. package/src/lib/provider-runtime-fetch.ts +23 -0
  107. package/src/lib/provider-tls-profile.ts +221 -0
  108. package/src/lib/request-execution-budget.ts +16 -6
  109. package/src/lib/spend-reservation-ledger.ts +54 -20
  110. package/src/lib/sse-decoder.ts +20 -17
  111. package/src/lib/synced-state-location.ts +96 -0
  112. package/src/lib/winsw.ts +14 -5
  113. package/src/oauth/anthropic-account-refusal.ts +53 -4
  114. package/src/oauth/anthropic-model-quota.ts +83 -0
  115. package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
  116. package/src/oauth/anthropic-routing.ts +112 -58
  117. package/src/oauth/health.ts +39 -3
  118. package/src/oauth/index.ts +14 -0
  119. package/src/oauth/kiro-account-load.ts +10 -0
  120. package/src/oauth/zed.ts +134 -0
  121. package/src/providers/model-discovery.ts +19 -8
  122. package/src/providers/model-rename-fields.ts +1 -0
  123. package/src/providers/provider-id-rewrite.ts +9 -2
  124. package/src/providers/quota/account-cache.ts +29 -7
  125. package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
  126. package/src/providers/quota/anthropic-family-headers.ts +25 -0
  127. package/src/providers/quota/vendor-probes-oauth.ts +5 -1
  128. package/src/providers/quota-types.ts +4 -0
  129. package/src/providers/registry/entries-core.ts +18 -0
  130. package/src/providers/registry/entries-extended.ts +55 -0
  131. package/src/providers/registry/types.ts +3 -0
  132. package/src/providers/xai-fast-model.ts +12 -4
  133. package/src/providers/zed.ts +598 -0
  134. package/src/responses/state/snapshot-select.ts +22 -9
  135. package/src/responses/state.ts +2 -3
  136. package/src/router.ts +9 -0
  137. package/src/server/anthropic-rate-limit-headers.ts +7 -0
  138. package/src/server/auth-cors.ts +7 -0
  139. package/src/server/chat-completions.ts +9 -4
  140. package/src/server/claude-messages.ts +59 -21
  141. package/src/server/index/claude-intercept-lifecycle.ts +71 -25
  142. package/src/server/index/optional-listeners.ts +4 -0
  143. package/src/server/index/spend-ledger-lifecycle.ts +4 -0
  144. package/src/server/management/agent-settings-routes.ts +74 -37
  145. package/src/server/management/claude-desktop-picker-routes.ts +7 -0
  146. package/src/server/management/claude-intercept-routes.ts +35 -0
  147. package/src/server/management/codex-agent-role-routes.ts +122 -0
  148. package/src/server/management/codex-role-auto-assign.ts +208 -0
  149. package/src/server/management/combo-routes.ts +62 -5
  150. package/src/server/management/config-routes.ts +35 -3
  151. package/src/server/management/context.ts +4 -1
  152. package/src/server/management/decision-discovery.ts +74 -0
  153. package/src/server/management/decision-model-validation.ts +80 -0
  154. package/src/server/management/decision-routes.ts +160 -0
  155. package/src/server/management/native-integration-routes.ts +16 -0
  156. package/src/server/management/provider-routes.ts +30 -34
  157. package/src/server/management/route-registry.ts +12 -0
  158. package/src/server/management/sibling-guard.ts +1 -0
  159. package/src/server/management/zed-provider-probe.ts +38 -0
  160. package/src/server/management-api.ts +12 -1
  161. package/src/server/messages-native-oauth.ts +7 -2
  162. package/src/server/messages-native.ts +7 -3
  163. package/src/server/relay.ts +3 -1
  164. package/src/server/request-log.ts +2 -0
  165. package/src/server/responses/adapter-continuation.ts +3 -3
  166. package/src/server/responses/adapter-dispatch.ts +18 -3
  167. package/src/server/responses/compact.ts +14 -1
  168. package/src/server/responses/core-auth.ts +2 -0
  169. package/src/server/responses/core-combo.ts +67 -11
  170. package/src/server/responses/core-normalize.ts +21 -1
  171. package/src/server/responses/core-options.ts +11 -1
  172. package/src/server/responses/core.ts +5 -3
  173. package/src/server/responses/fetch-helpers.ts +6 -3
  174. package/src/server/responses/jev-model-invoke.ts +190 -0
  175. package/src/server/responses/passthrough-delivery.ts +16 -0
  176. package/src/server/responses/policy-fallback.ts +55 -3
  177. package/src/server/responses/policy-request-scope.ts +80 -0
  178. package/src/server/responses/request-prepare.ts +70 -18
  179. package/src/server/responses/request-transport.ts +54 -23
  180. package/src/server/responses/run-turn-execution.ts +1 -0
  181. package/src/server/responses/sidecar-execution.ts +9 -3
  182. package/src/server/responses-hosted-image-display.ts +225 -0
  183. package/src/service/managing-cli.ts +34 -4
  184. package/src/service/systemd-env.ts +75 -0
  185. package/src/service/windows-guarded-manager.ts +60 -19
  186. package/src/service/windows-scheduler.ts +22 -3
  187. package/src/service/windows-taskxml.ts +32 -10
  188. package/src/service-manager-probe.ts +72 -19
  189. package/src/types/config.ts +31 -1
  190. package/src/types/provider.ts +2 -0
  191. package/src/types/request.ts +2 -0
  192. package/src/update/index.ts +39 -6
  193. package/src/update/job.ts +63 -63
  194. package/src/update/npm-cache-preflight.d.mts +33 -1
  195. package/src/update/npm-cache-preflight.mjs +131 -18
  196. package/src/update/ownership-transaction.ts +19 -1
  197. package/src/update/restart-ownership.ts +80 -6
  198. package/src/update/transactional-install.d.mts +2 -0
  199. package/src/update/transactional-install.mjs +6 -1
  200. package/src/usage/jev-stats.ts +28 -1
  201. package/src/usage/log.ts +1 -1
  202. package/src/vision/anthropic-describe.ts +5 -3
  203. package/src/vision/index.ts +1 -0
  204. package/src/vision/plan.ts +2 -1
  205. package/src/vision/routed-describe.ts +29 -121
  206. package/src/web-search/alpha-search.ts +3 -3
  207. package/src/web-search/anthropic-executor.ts +6 -4
  208. package/src/web-search/index.ts +1 -1
  209. package/src/web-search/loop.ts +3 -3
  210. package/src/web-search/passthrough-bridge.ts +2 -1
  211. package/src/web-search/run-turn-loop.ts +1 -1
  212. package/src/web-search/sidecar-providers.ts +2 -1
  213. package/gui/dist/assets/App-ia4U0ufY.css +0 -1
  214. package/gui/dist/assets/App-xm0zGNOe.js +0 -51
  215. package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
  216. package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
  217. package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
@@ -442,17 +442,54 @@ function ledgerEntryExists(path: string): boolean {
442
442
  }
443
443
  }
444
444
 
445
- function assertSafeLedgerFile(path: string): void {
446
- const stat = lstatSync(path);
447
- if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1
448
- || (process.platform !== "win32" && stat.uid !== process.getuid!())) {
449
- throw new SpendLedgerOwnerError(
445
+ /** Which ledger file a refusal is about. A role, never a path: the path names the user's home. */
446
+ export type SpendLedgerFileRole = "journal" | "journal-compaction" | "salt";
447
+
448
+ /** The one condition that refused the file, so a report can say which guard fired. */
449
+ export type SpendLedgerFileRefusal =
450
+ | "not-regular-file"
451
+ | "symbolic-link"
452
+ | "extra-hard-link"
453
+ | "foreign-owner"
454
+ | "invalid-salt";
455
+
456
+ /**
457
+ * A ledger file failed a safety condition.
458
+ *
459
+ * The guard is unchanged; this only says which file role and which condition refused it. Issue
460
+ * #6314 sat for days on "could not be opened safely" because the same sentence covered five
461
+ * conditions and two files, and the one that fired (a second hard link to the journal, most
462
+ * likely held briefly by a cloud-sync daemon) could only be found with an instrumented build.
463
+ * Role and condition are fixed vocabulary, so the message carries no path, salt, alias or
464
+ * request content.
465
+ */
466
+ export class SpendLedgerFileRefusedError extends SpendLedgerOwnerError {
467
+ constructor(readonly role: SpendLedgerFileRole, readonly refusal: SpendLedgerFileRefusal) {
468
+ super(
450
469
  "SPEND_LEDGER_OWNER_UNAVAILABLE",
451
- "Spend-ledger storage could not be opened safely.",
470
+ `Spend-ledger storage could not be opened safely (${role}: ${refusal}).`
471
+ + (refusal === "extra-hard-link"
472
+ ? " Another directory entry links to this file; if the opencodex state directory is inside an iCloud Drive or other synced folder, move it out."
473
+ : ""),
452
474
  );
475
+ this.name = "SpendLedgerFileRefusedError";
453
476
  }
454
477
  }
455
478
 
479
+ function ledgerFileRefusal(path: string): SpendLedgerFileRefusal | undefined {
480
+ const stat = lstatSync(path);
481
+ if (stat.isSymbolicLink()) return "symbolic-link";
482
+ if (!stat.isFile()) return "not-regular-file";
483
+ if (stat.nlink !== 1) return "extra-hard-link";
484
+ if (process.platform !== "win32" && stat.uid !== process.getuid!()) return "foreign-owner";
485
+ return undefined;
486
+ }
487
+
488
+ function assertSafeLedgerFile(path: string, role: SpendLedgerFileRole): void {
489
+ const refusal = ledgerFileRefusal(path);
490
+ if (refusal !== undefined) throw new SpendLedgerFileRefusedError(role, refusal);
491
+ }
492
+
456
493
  /**
457
494
  * The production journal. Its location comes from the owned state directory and every touch
458
495
  * proves that ownership, so there is no entrypoint here that writes a caller-chosen path.
@@ -471,7 +508,7 @@ export function createOwnedFileSpendJournal(storage: SpendLedgerStorage): SpendJ
471
508
  read(): string[] {
472
509
  assertStorageOwned(storage);
473
510
  if (!ledgerEntryExists(path)) return [];
474
- assertSafeLedgerFile(path);
511
+ assertSafeLedgerFile(path, "journal");
475
512
  // Replay is once per process and is the moment a journal inherited from an older build
476
513
  // or a restored backup first passes through here.
477
514
  hardenLedgerFile(path, { force: true });
@@ -481,9 +518,9 @@ export function createOwnedFileSpendJournal(storage: SpendLedgerStorage): SpendJ
481
518
  assertStorageOwned(storage);
482
519
  ensureDir();
483
520
  const created = !ledgerEntryExists(path);
484
- if (!created) assertSafeLedgerFile(path);
521
+ if (!created) assertSafeLedgerFile(path, "journal");
485
522
  appendFileSync(path, line + "\n", { encoding: "utf8", mode: 0o600 });
486
- assertSafeLedgerFile(path);
523
+ assertSafeLedgerFile(path, "journal");
487
524
  hardenLedgerFile(path, { force: created });
488
525
  },
489
526
  rewrite(lines: string[]): void {
@@ -491,7 +528,7 @@ export function createOwnedFileSpendJournal(storage: SpendLedgerStorage): SpendJ
491
528
  ensureDir();
492
529
  // Same directory, so the rename is atomic on the same filesystem: a crash mid-compaction
493
530
  // leaves either the old journal or the new one, never a half-written ledger.
494
- if (ledgerEntryExists(path)) assertSafeLedgerFile(path);
531
+ if (ledgerEntryExists(path)) assertSafeLedgerFile(path, "journal");
495
532
  const temp = `${path}.compact-${process.pid}-${randomBytes(6).toString("hex")}`;
496
533
  // The creation is INSIDE the cleanup, not before it. The name carries random bytes, so a
497
534
  // failure anywhere after the entry exists used to leave a uniquely named file and the next
@@ -514,7 +551,7 @@ export function createOwnedFileSpendJournal(storage: SpendLedgerStorage): SpendJ
514
551
  closeSync(fd);
515
552
  fd = undefined;
516
553
  journalFaultForTests?.("validate", temp);
517
- assertSafeLedgerFile(temp);
554
+ assertSafeLedgerFile(temp, "journal-compaction");
518
555
  journalFaultForTests?.("harden", temp);
519
556
  hardenLedgerFile(temp, { force: true });
520
557
  journalFaultForTests?.("rename", temp);
@@ -528,7 +565,7 @@ export function createOwnedFileSpendJournal(storage: SpendLedgerStorage): SpendJ
528
565
  try { unlinkSync(temp); } catch { /* same */ }
529
566
  }
530
567
  }
531
- assertSafeLedgerFile(path);
568
+ assertSafeLedgerFile(path, "journal");
532
569
  hardenLedgerFile(path, { force: true });
533
570
  },
534
571
  };
@@ -545,14 +582,11 @@ export function loadOrCreateSpendLedgerSalt(storage: SpendLedgerStorage): string
545
582
  assertStorageOwned(storage);
546
583
  const path = spendLedgerStoragePath(storage);
547
584
  if (ledgerEntryExists(path)) {
548
- assertSafeLedgerFile(path);
585
+ assertSafeLedgerFile(path, "salt");
549
586
  hardenLedgerFile(path, { force: true });
550
587
  const existing = readFileSync(path, "utf8").trim();
551
588
  if (/^[0-9a-f]{32,}$/.test(existing)) return existing;
552
- throw new SpendLedgerOwnerError(
553
- "SPEND_LEDGER_OWNER_UNAVAILABLE",
554
- "Spend-ledger storage could not be opened safely.",
555
- );
589
+ throw new SpendLedgerFileRefusedError("salt", "invalid-salt");
556
590
  }
557
591
  const dir = dirname(path);
558
592
  assertStorageOwned(storage);
@@ -560,7 +594,7 @@ export function loadOrCreateSpendLedgerSalt(storage: SpendLedgerStorage): string
560
594
  mkdirSync(dir, { recursive: true, mode: 0o700 });
561
595
  const salt = randomBytes(32).toString("hex");
562
596
  writeFileSync(path, salt + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
563
- assertSafeLedgerFile(path);
597
+ assertSafeLedgerFile(path, "salt");
564
598
  hardenLedgerFile(path, { force: true });
565
599
  return salt;
566
600
  }
@@ -1205,8 +1239,8 @@ export function sharedSpendLedger(): SpendReservationLedger {
1205
1239
  const saltPath = spendLedgerStoragePath(saltStorage);
1206
1240
  const assertOwnedAccounting = (): void => {
1207
1241
  assertStorageOwned(journalStorage);
1208
- if (ledgerEntryExists(journalPath)) assertSafeLedgerFile(journalPath);
1209
- if (ledgerEntryExists(saltPath)) assertSafeLedgerFile(saltPath);
1242
+ if (ledgerEntryExists(journalPath)) assertSafeLedgerFile(journalPath, "journal");
1243
+ if (ledgerEntryExists(saltPath)) assertSafeLedgerFile(saltPath, "salt");
1210
1244
  };
1211
1245
  sharedLedger = createSpendReservationLedger({
1212
1246
  journal: createOwnedFileSpendJournal(journalStorage),
@@ -56,6 +56,7 @@ export function sseFieldOffset(text: string, lineStart: number, lineEnd: number,
56
56
 
57
57
  /**
58
58
  * Decode text/event-stream records across arbitrary fetch chunk boundaries.
59
+ * CR, LF and CRLF terminate lines; a split CRLF is one delimiter.
59
60
  *
60
61
  * The final record is dispatched at EOF even when the upstream omits the trailing blank line or
61
62
  * final newline. That matters for compatible APIs that place a terminal event in the last bytes of
@@ -77,6 +78,7 @@ export async function* decodeServerSentEvents(
77
78
  const reader = source.getReader();
78
79
  const decoder = new TextDecoder();
79
80
  let lineBuffer = "";
81
+ let skipLineFeed = false;
80
82
  let lineRawBytes = 0;
81
83
  let lineRetainedBytes = 0;
82
84
  let event: string | undefined;
@@ -183,8 +185,8 @@ export async function* decodeServerSentEvents(
183
185
  };
184
186
 
185
187
  const acceptLine = (): { record: ServerSentEvent | SseRecord; bytes: number } | undefined => {
186
- let line = lineBuffer;
187
- let retainedLineBytes = lineRetainedBytes;
188
+ const line = lineBuffer;
189
+ const retainedLineBytes = lineRetainedBytes;
188
190
  let lineOwned = true;
189
191
  const releaseLine = (): void => {
190
192
  if (!lineOwned) return;
@@ -195,20 +197,6 @@ export async function* decodeServerSentEvents(
195
197
  lineRawBytes = 0;
196
198
  lineRetainedBytes = 0;
197
199
  try {
198
- if (line.endsWith("\r")) {
199
- const nextLineBytes = retainedLineBytes - 1;
200
- const reservation = translatorBudget.reserveTransient(nextLineBytes, scope);
201
- try {
202
- const nextLine = line.slice(0, -1);
203
- reservation.commitRetained();
204
- translatorBudget.releaseRetained(retainedLineBytes, scope);
205
- line = nextLine;
206
- retainedLineBytes = nextLineBytes;
207
- } catch (error) {
208
- reservation.release();
209
- throw error;
210
- }
211
- }
212
200
  if (line === "") {
213
201
  releaseLine();
214
202
  return dispatch();
@@ -296,13 +284,28 @@ export async function* decodeServerSentEvents(
296
284
  decoded: string,
297
285
  ): AsyncGenerator<ServerSentEvent | SseRecord> {
298
286
  let offset = 0;
287
+ // Cached positions of the next CR and LF at or after `offset`, found with the native search so
288
+ // every stream keeps linear scanning. -1 means this decoded string has none left; it is never
289
+ // searched again, which keeps LF-only and CR-only chunks linear too.
290
+ let nextCr = decoded.indexOf("\r");
291
+ let nextLf = decoded.indexOf("\n");
299
292
  while (offset < decoded.length) {
300
- const newline = decoded.indexOf("\n", offset);
293
+ if (skipLineFeed) {
294
+ skipLineFeed = false;
295
+ if (decoded[offset] === "\n") {
296
+ offset++;
297
+ continue;
298
+ }
299
+ }
300
+ if (nextCr >= 0 && nextCr < offset) nextCr = decoded.indexOf("\r", offset);
301
+ if (nextLf >= 0 && nextLf < offset) nextLf = decoded.indexOf("\n", offset);
302
+ const newline = nextCr < 0 ? nextLf : nextLf < 0 ? nextCr : Math.min(nextCr, nextLf);
301
303
  if (newline < 0) {
302
304
  appendLine(decoded, offset, decoded.length);
303
305
  return;
304
306
  }
305
307
  appendLine(decoded, offset, newline);
308
+ skipLineFeed = decoded[newline] === "\r";
306
309
  const accepted = acceptLine();
307
310
  if (accepted) {
308
311
  try {
@@ -0,0 +1,96 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ // POSIX path operations on purpose: every path this module classifies is a macOS path. The
4
+ // host's own path module would build backslash or drive-letter paths on Windows and make the
5
+ // injected-platform tests compare different strings there (dev CI windows 5/9 after #6398).
6
+ import { posix } from "node:path";
7
+
8
+ const { join, resolve } = posix;
9
+
10
+ /**
11
+ * Where a state directory sits relative to the folders macOS keeps in sync with a cloud.
12
+ *
13
+ * A sync daemon can hold a second hard link to a file while it stages or uploads a change. The
14
+ * spend ledger refuses a journal with a second link on purpose, so a state directory inside a
15
+ * synced folder turns ordinary requests into intermittent 502s (#6314). This only answers
16
+ * "is it likely synced"; the guard itself stays strict.
17
+ */
18
+ export type SyncedStateLocation = "icloud-drive" | "file-provider" | "icloud-desktop-documents";
19
+
20
+ export interface SyncedStateLocationProbe {
21
+ readonly platform?: NodeJS.Platform;
22
+ readonly home?: string;
23
+ /** Resolve symlinks. Throws when the path does not exist yet. */
24
+ readonly realpath?: (path: string) => string;
25
+ /** Whether a directory entry exists at the path, without following it. */
26
+ readonly entryExists?: (path: string) => boolean;
27
+ }
28
+
29
+ const defaultEntryExists = (path: string): boolean => {
30
+ try { lstatSync(path); return true; } catch { return false; }
31
+ };
32
+
33
+ /**
34
+ * Advisory only. Apple publishes no API a CLI can ask, so this reads the observed layout:
35
+ * iCloud Drive lives under ~/Library/Mobile Documents, File Provider clients (OneDrive, Dropbox,
36
+ * Google Drive) under ~/Library/CloudStorage, and with "Desktop & Documents Folders" turned on
37
+ * iCloud Drive holds a Desktop/Documents entry of its own. A false positive costs one warning
38
+ * line; nothing is refused on the strength of it.
39
+ */
40
+ export function syncedStateLocation(dir: string, probe: SyncedStateLocationProbe = {}): SyncedStateLocation | undefined {
41
+ if ((probe.platform ?? process.platform) !== "darwin") return undefined;
42
+ const realpath = probe.realpath ?? ((path: string) => realpathSync.native(path));
43
+ const entryExists = probe.entryExists ?? defaultEntryExists;
44
+ const canonical = (path: string): string => {
45
+ try { return realpath(path); } catch { return resolve(path); }
46
+ };
47
+ // The default APFS volume is case-insensitive, so ~/documents and ~/Documents are one folder.
48
+ const fold = (path: string): string => path.toLowerCase();
49
+ const home = canonical(probe.home ?? homedir());
50
+ const target = fold(canonical(dir));
51
+ const within = (root: string): boolean => {
52
+ const folded = fold(root);
53
+ return target === folded || target.startsWith(folded + "/");
54
+ };
55
+ const mobileDocuments = join(home, "Library", "Mobile Documents");
56
+ if (within(mobileDocuments)) return "icloud-drive";
57
+ if (within(join(home, "Library", "CloudStorage"))) return "file-provider";
58
+ for (const folder of ["Desktop", "Documents"]) {
59
+ if (within(join(home, folder)) && entryExists(join(mobileDocuments, "com~apple~CloudDocs", folder))) {
60
+ return "icloud-desktop-documents";
61
+ }
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ const LOCATION_LABEL: Record<SyncedStateLocation, string> = {
67
+ "icloud-drive": "inside iCloud Drive",
68
+ "file-provider": "inside a cloud-storage (File Provider) folder",
69
+ "icloud-desktop-documents": "in Desktop or Documents, which iCloud Drive appears to sync",
70
+ };
71
+
72
+ /** The startup warning. Names the location kind, never the path. */
73
+ export function syncedStateWarning(location: SyncedStateLocation): string[] {
74
+ return [
75
+ `⚠️ The opencodex state directory (OPENCODEX_HOME) is ${LOCATION_LABEL[location]}.`,
76
+ " A sync service can briefly add a second link to the spend ledger, which opencodex",
77
+ " refuses, so requests may fail intermittently. Set OPENCODEX_HOME to a folder outside",
78
+ " synced locations (the default ~/.opencodex is not synced).",
79
+ ];
80
+ }
81
+
82
+ let warned = false;
83
+
84
+ /** Warn once per process when the state directory looks synced. Never throws. */
85
+ export function warnIfSyncedStateDirectory(dir: string, warn: (line: string) => void = console.warn): void {
86
+ if (warned) return;
87
+ let location: SyncedStateLocation | undefined;
88
+ try { location = syncedStateLocation(dir); } catch { return; }
89
+ if (location === undefined) return;
90
+ warned = true;
91
+ // Runs while the startup owner lease is held and before its rollback is registered, so a
92
+ // throwing sink must not escape and strand the lease.
93
+ try {
94
+ for (const line of syncedStateWarning(location)) warn(line);
95
+ } catch { /* advisory output only */ }
96
+ }
package/src/lib/winsw.ts CHANGED
@@ -179,8 +179,16 @@ export async function ensureWinswBinary(fetchImpl: typeof fetch = fetch): Promis
179
179
  return exe;
180
180
  }
181
181
 
182
+ /** SCM round-trips are sub-second on a healthy system; a wedged service controller must not block a guarded stop forever — a killed command still classifies fail-closed. */
183
+ const SERVICE_COMMAND_TIMEOUT_MS = 15_000;
184
+ /** `stopwait` outlasts the SCM `<stoptimeout>` (20s in the generated service definition) plus overhead; still bounded so a wedged SCM cannot hang the CLI. */
185
+ const SERVICE_STOPWAIT_TIMEOUT_MS = 40_000;
186
+
182
187
  function runWinsw(args: string[]): string {
183
- return execFileSync(winswExePath(), args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
188
+ const timeoutMs = args[0] === "stopwait" ? SERVICE_STOPWAIT_TIMEOUT_MS : SERVICE_COMMAND_TIMEOUT_MS;
189
+ return execFileSync(winswExePath(), args, {
190
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true, timeout: timeoutMs,
191
+ }).trim();
184
192
  }
185
193
 
186
194
  /** `install /p` prompts for the service-account password on the console — stdin must be inherited. */
@@ -191,13 +199,14 @@ function runWinswInteractive(args: string[]): void {
191
199
  + "Run `ocx service install --native` from an elevated Command Prompt or PowerShell window, not a hidden or piped session.",
192
200
  );
193
201
  }
202
+ // Unbounded by design: the service-account password prompt waits on the user.
194
203
  execFileSync(winswExePath(), args, { stdio: "inherit" });
195
204
  }
196
205
 
197
206
  function scQc(): string {
198
207
  const sc = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "sc.exe");
199
208
  return execFileSync(existsSync(sc) ? sc : "sc.exe", ["qc", WINSW_SERVICE_ID], {
200
- encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true,
209
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true, timeout: SERVICE_COMMAND_TIMEOUT_MS,
201
210
  });
202
211
  }
203
212
 
@@ -269,7 +278,7 @@ function scExePath(): string {
269
278
 
270
279
  function queryScmForService(): string {
271
280
  return execFileSync(scExePath(), ["query", WINSW_SERVICE_ID], {
272
- encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true,
281
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true, timeout: SERVICE_COMMAND_TIMEOUT_MS,
273
282
  });
274
283
  }
275
284
 
@@ -370,9 +379,9 @@ export function uninstallWinswService(): void {
370
379
  }
371
380
  if (probe === true) {
372
381
  try {
373
- execFileSync(scExePath(), ["stop", WINSW_SERVICE_ID], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
382
+ execFileSync(scExePath(), ["stop", WINSW_SERVICE_ID], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true, timeout: SERVICE_COMMAND_TIMEOUT_MS });
374
383
  } catch { /* not running */ }
375
- execFileSync(scExePath(), ["delete", WINSW_SERVICE_ID], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
384
+ execFileSync(scExePath(), ["delete", WINSW_SERVICE_ID], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true, timeout: SERVICE_COMMAND_TIMEOUT_MS });
376
385
  }
377
386
  }
378
387
  return;
@@ -1,13 +1,16 @@
1
+ import { anthropicModelFamily } from "./anthropic-model-quota";
1
2
  /** Narrow pre-output account recovery, fenced to the bearer that physically sent the turn. */
2
3
  import { readBoundedResponseBody } from "../lib/bounded-body";
3
- import { isNonReplayableResponse } from "../lib/upstream-retry";
4
+ import { classifyAnthropic429, anthropicRetryAfterMs, pauseAnthropicRateAdmission, anthropicRatePauseUntil, ANTHROPIC_SHORT_RETRY_MS, ANTHROPIC_MAX_INLINE_THROTTLE_MS } from "./anthropic-rate-limit-policy";
5
+ import { isNonReplayableResponse, sleepWithAbort } from "../lib/upstream-retry";
4
6
  import { credentialGeneration, getAccountCredentialWithStatus } from "./store";
5
7
  import type { OAuthAccessSnapshot } from "./index";
6
8
  import type { OcxConfig } from "../types";
7
9
  import type { AnthropicRouteDecision } from "./anthropic-model-routes";
8
- import { recordAnthropicAccountRefusal, rotateAnthropicAccountOnRefusal } from "./anthropic-routing";
10
+ import { recordAnthropicAccountRefusal, rotateAnthropicAccountOnRefusal, hasAnthropicFailoverQuorum, isAnthropicAccountPoolEnabled, pickAlternateAnthropicAccount } from "./anthropic-routing";
9
11
 
10
12
  const responseCredentials = new WeakMap<Response, Pick<OAuthAccessSnapshot, "accountId" | "generation">>();
13
+ const retryStates = new WeakMap<object, { firstAccountId: string; sameAccount: boolean; detour: boolean }>();
11
14
  const verdicts = new WeakMap<Response, Promise<boolean>>();
12
15
 
13
16
  /** Called only when the outgoing headers prove ownership of the selected stored bearer. */
@@ -43,15 +46,61 @@ export async function rotateAnthropicAccountOnResponse(
43
46
  options: {
44
47
  config: OcxConfig;
45
48
  accountId: string;
49
+ model?: string;
46
50
  sessionKey?: string | null;
47
51
  decision?: AnthropicRouteDecision | null;
48
52
  signal?: AbortSignal;
49
53
  canRetry: boolean;
54
+ /** Shared by main, continuation and sidecar consumers of one logical request. */
55
+ requestKey?: object;
56
+ allow429Recovery?: boolean;
50
57
  allowAccountRefusal?: boolean;
51
58
  },
52
59
  ): Promise<string | null> {
53
60
  if (options.signal?.aborted || isNonReplayableResponse(response)) return null;
54
- if (response.status !== 429) {
61
+ if (response.status === 429) {
62
+ const sent = responseCredentials.get(response);
63
+ const current = sent && getAccountCredentialWithStatus("anthropic", sent.accountId);
64
+ if (!sent || sent.accountId !== options.accountId || !current || current.needsReauth
65
+ || credentialGeneration(current.credential) !== sent.generation) return null;
66
+ const kind = classifyAnthropic429(response.headers);
67
+ if (kind === "family-quota") {
68
+ if (anthropicModelFamily(options.model) !== "Fable" || !options.canRetry || options.allow429Recovery === false) return null;
69
+ return pickAlternateAnthropicAccount(options.config, sent.accountId, Date.now(), options.decision ?? null, options.model);
70
+ }
71
+ if (kind !== "shared-quota") {
72
+ if (!isAnthropicAccountPoolEnabled(options.config) && !hasAnthropicFailoverQuorum() && !current.paused) return null;
73
+ const now = Date.now();
74
+ const delay = anthropicRetryAfterMs(response.headers.get("retry-after"), now) ?? ANTHROPIC_SHORT_RETRY_MS;
75
+ if (kind === "transient-rate") pauseAnthropicRateAdmission(sent.accountId, now + delay);
76
+ if (!options.canRetry || options.allow429Recovery === false || !options.requestKey) return null;
77
+ let state = retryStates.get(options.requestKey);
78
+ if (!state) { state = { firstAccountId: sent.accountId, sameAccount: false, detour: false }; retryStates.set(options.requestKey, state); }
79
+ // A concurrent committed selection may have moved the same-account proposal.
80
+ if (state.firstAccountId !== sent.accountId) state.detour = true;
81
+ const wait = Math.max(delay, (anthropicRatePauseUntil(sent.accountId) ?? now) - now);
82
+ if (!state.sameAccount && wait <= ANTHROPIC_MAX_INLINE_THROTTLE_MS && !current.paused) {
83
+ state.sameAccount = true;
84
+ // Millisecond rounding can wake just before this deadline; later extensions still bind.
85
+ const retryAt = now + wait;
86
+ try { await sleepWithAbort(wait, options.signal); } catch { return null; }
87
+ const live = getAccountCredentialWithStatus("anthropic", sent.accountId);
88
+ if (!live || live.paused || live.needsReauth || options.signal?.aborted
89
+ || credentialGeneration(live.credential) !== sent.generation
90
+ || anthropicRatePauseUntil(sent.accountId, Math.max(Date.now(), retryAt))) return null;
91
+ return sent.accountId;
92
+ }
93
+ if (kind === "transient-rate" && !state.detour) {
94
+ state.detour = true;
95
+ return pickAlternateAnthropicAccount(options.config, sent.accountId, Date.now(), options.decision ?? null, options.model);
96
+ }
97
+ return null;
98
+ }
99
+ if (options.allow429Recovery === false) {
100
+ recordAnthropicAccountRefusal(options.config, options.accountId, 429, response.headers.get("retry-after"), Date.now(), response.headers);
101
+ return null;
102
+ }
103
+ } else {
55
104
  if (response.status !== 403 || options.allowAccountRefusal === false) return null;
56
105
  const sent = responseCredentials.get(response);
57
106
  if (!sent || sent.accountId !== options.accountId) return null;
@@ -72,5 +121,5 @@ export async function rotateAnthropicAccountOnResponse(
72
121
  return null;
73
122
  }
74
123
  return rotateAnthropicAccountOnRefusal(options.config, options.accountId, status,
75
- response.headers.get("retry-after"), options.sessionKey, Date.now(), response.headers, options.decision);
124
+ response.headers.get("retry-after"), options.sessionKey, Date.now(), response.headers, options.decision, options.model);
76
125
  }
@@ -0,0 +1,83 @@
1
+ /** Model-scoped passive quota evidence. Reimplemented from documented behavior, not ported code. */
2
+ import { credentialGeneration, getAccountCredential } from "./store";
3
+ import { getCachedProviderAccountQuota } from "../providers/quota/account-cache";
4
+ import type { ProviderQuota, ProviderQuotaWindow } from "../providers/quota-types";
5
+
6
+ export const ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS = 30 * 60_000;
7
+ export type AnthropicModelFamily = "Fable" | "Opus" | "Sonnet";
8
+ export function anthropicModelFamily(model?: string): AnthropicModelFamily | undefined {
9
+ return /(?:^|[/])claude-fable-5(?:-|$)/i.test(model ?? "") ? "Fable"
10
+ : /(?:^|[/])claude-opus-/i.test(model ?? "") ? "Opus"
11
+ : /(?:^|[/])claude-sonnet-/i.test(model ?? "") ? "Sonnet" : undefined;
12
+ }
13
+
14
+ export function anthropicModelPercents(quota: ProviderQuota | null, model?: string): number[] {
15
+ const family = anthropicModelFamily(model);
16
+ return [quota?.fiveHourPercent, quota?.weeklyPercent, ...(!model ? [quota?.monthlyPercent] : []),
17
+ ...(quota?.customWindows ?? []).filter(window => !model || window.scope !== "model" || (family && window.label === family)).map(window => window.percent)]
18
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
19
+ }
20
+ export function anthropicModelExhausted(accountId: string, model?: string): boolean {
21
+ return anthropicModelPercents(getCachedProviderAccountQuota("anthropic", accountId), model).some(percent => percent >= 100);
22
+ }
23
+
24
+ interface FamilyObservation { generation: string; seenAt: number; resetAt?: number; busy: boolean; }
25
+ const spentFamilies = new Map<string, FamilyObservation>();
26
+ const familyGenerations = new Map<string, number>();
27
+ let nextFamilyGeneration = 1;
28
+ export function anthropicFamilyQuotaGeneration(id: string): number { return familyGenerations.get(id) ?? 0; }
29
+ const keyFor = (id: string, family: string) => `${id}\0${family}`;
30
+ function ownedObservation(id: string, family: string): FamilyObservation | undefined {
31
+ const key = keyFor(id, family);
32
+ const entry = spentFamilies.get(key);
33
+ if (!entry) return undefined;
34
+ const credential = getAccountCredential("anthropic", id);
35
+ if (!credential || credentialGeneration(credential) !== entry.generation) { spentFamilies.delete(key); return undefined; }
36
+ return entry;
37
+ }
38
+ export function observeAnthropicFamilyQuota(id: string, windows: ProviderQuotaWindow[], now: number, authoritative = false): void {
39
+ const credential = getAccountCredential("anthropic", id);
40
+ if (!credential) return;
41
+ if (authoritative || windows.some(window => window.scope === "model")) familyGenerations.set(id, nextFamilyGeneration++);
42
+ if (authoritative) for (const key of spentFamilies.keys()) if (key.startsWith(`${id}\0`)) spentFamilies.delete(key);
43
+ for (const window of windows) {
44
+ if (window.scope !== "model") continue;
45
+ const key = keyFor(id, window.label);
46
+ if (!window.rejected) { spentFamilies.delete(key); continue; }
47
+ spentFamilies.set(key, { generation: credentialGeneration(credential), seenAt: now, resetAt: window.resetAt, busy: false });
48
+ }
49
+ }
50
+ export function anthropicFamilyRejected(id: string, model?: string, now = Date.now()): boolean {
51
+ const family = anthropicModelFamily(model);
52
+ const entry = family && ownedObservation(id, family);
53
+ if (!entry) return false;
54
+ if (entry.resetAt !== undefined && entry.resetAt <= now) return entry.busy;
55
+ return entry.busy || now - entry.seenAt < ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS;
56
+ }
57
+ /** Only stale passive family exclusions require a request-driven single-flight physical send. */
58
+ export function claimAnthropicFamilyRevalidation(id: string, model?: string, now = Date.now()): (() => void) | null {
59
+ const family = anthropicModelFamily(model);
60
+ const entry = family && ownedObservation(id, family);
61
+ if (!entry) return () => {};
62
+ if (anthropicFamilyRejected(id, model, now)) return null;
63
+ entry.busy = true;
64
+ return () => { entry.busy = false; };
65
+ }
66
+ /** An owned successful send proves admission for its requested family even without headers. */
67
+ export function clearAnthropicRequestedFamilyQuota(id: string, model?: string): void {
68
+ const family = anthropicModelFamily(model);
69
+ if (family && spentFamilies.delete(keyFor(id, family))) familyGenerations.set(id, nextFamilyGeneration++);
70
+ }
71
+ export function clearAnthropicFamilyQuota(): void { spentFamilies.clear(); familyGenerations.clear(); }
72
+
73
+ export function anthropicModelWeeklyPercent(quota: ProviderQuota | null, model?: string): number | undefined {
74
+ const family = anthropicModelFamily(model);
75
+ return family ? quota?.customWindows?.find(window => window.scope === "model" && window.label === family)?.percent : undefined;
76
+ }
77
+
78
+ export function anthropicFamilyRetryAt(id: string, model?: string, now = Date.now()): number | undefined {
79
+ const family = anthropicModelFamily(model);
80
+ const entry = family && ownedObservation(id, family);
81
+ if (!entry || !anthropicFamilyRejected(id, model, now)) return undefined;
82
+ return entry.busy ? now + 1000 : Math.min(entry.resetAt ?? Infinity, entry.seenAt + ANTHROPIC_PASSIVE_FAMILY_MAX_AGE_MS);
83
+ }
@@ -0,0 +1,37 @@
1
+ /** Response-header policy for managed Anthropic sends; no account or request payload data. */
2
+ export type AnthropicRateLimitKind = "shared-quota" | "family-quota" | "transient-rate" | "request-scoped-unknown";
3
+ export type RateLimitHeaders = Pick<Headers, "get">;
4
+ export const ANTHROPIC_SHORT_RETRY_MS = 100;
5
+ export const ANTHROPIC_MAX_INLINE_THROTTLE_MS = 1_000;
6
+
7
+ export function anthropicRetryAfterMs(value: string | null | undefined, now: number): number | undefined {
8
+ const text = value?.trim();
9
+ if (!text || text.length > 128) return undefined;
10
+ const at = /^\d+(?:\.\d+)?$/.test(text) ? now + Number(text) * 1000 : Date.parse(text);
11
+ return Number.isFinite(new Date(at).getTime()) && at > now ? Math.ceil(at - now) : undefined;
12
+ }
13
+
14
+ export function classifyAnthropic429(headers: RateLimitHeaders, now = Date.now()): AnthropicRateLimitKind {
15
+ const rejected = (window: string) => headers.get(`anthropic-ratelimit-unified-${window}-status`)?.trim() === "rejected";
16
+ if (rejected("5h") || rejected("7d")) return "shared-quota";
17
+ if (rejected("7d_oi")) return "family-quota";
18
+ if (headers.get("anthropic-ratelimit-unified-status")?.trim() === "rejected") return "shared-quota";
19
+ // Retry-After is an account-local admission pause, not proof of spent subscription quota.
20
+ if (anthropicRetryAfterMs(headers.get("retry-after"), now) !== undefined
21
+ || headers.get("anthropic-ratelimit-unified-status")?.trim() === "allowed") return "transient-rate";
22
+ return "request-scoped-unknown";
23
+ }
24
+
25
+ const pauses = new Map<string, number>();
26
+ export function pauseAnthropicRateAdmission(accountId: string, until: number): void {
27
+ const now = Date.now();
28
+ for (const [id, deadline] of pauses) if (deadline <= now) pauses.delete(id);
29
+ pauses.set(accountId, Math.max(pauses.get(accountId) ?? 0, until));
30
+ }
31
+ export function anthropicRatePauseUntil(accountId: string, now = Date.now()): number | undefined {
32
+ const until = pauses.get(accountId);
33
+ if (until === undefined) return undefined;
34
+ if (until <= now) { pauses.delete(accountId); return undefined; }
35
+ return until;
36
+ }
37
+ export function clearAnthropicRatePauses(): void { pauses.clear(); }