cartethyia 2.7.3 → 2.7.5

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 (625) hide show
  1. package/.env.example +2 -0
  2. package/README.md +17 -5
  3. package/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-Cj15GsMp.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
  5. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
  6. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-BIccZmeL.js} +1 -1
  10. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
  11. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
  12. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
  14. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
  15. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js +1 -0
  16. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-DkXb6dx-.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-DnQPSIA8.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
  31. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js +9 -0
  32. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-WTa0sktC.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-BfAb5-o_.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
  44. package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
  45. package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
  46. package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C0YYuZJ4.js → assert-C0K-c4eo.js} +1 -1
  48. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-DfbhQU4k.js} +1 -1
  50. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-BllT4LcF.js} +1 -1
  52. package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
  53. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  54. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  55. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  56. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  57. package/dist/dashboard/assets/{card-Be5Uzamx.js → card-CtkX66Wd.js} +1 -1
  58. package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
  59. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  60. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  61. package/dist/dashboard/assets/{cli-tools-muFRphDu.js → cli-tools-CQXVMn4P.js} +1 -1
  62. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-BQqBQd2n.js} +1 -1
  64. package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
  65. package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-BEK7KP0H.js} +1 -1
  66. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-BrTEZ8zu.js} +1 -1
  68. package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
  70. package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
  71. package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-EBHJ3DrE.js} +1 -1
  72. package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
  74. package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
  75. package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-BAACA727.js} +1 -1
  76. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  77. package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-YSCE4UC2.js} +1 -1
  78. package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
  79. package/dist/dashboard/assets/{index-cPrX5vn6.js → index-FQlj0HUT.js} +2 -2
  80. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-C4gU5iOL.js} +1 -1
  82. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-BHHX_Tmm.js} +1 -1
  84. package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
  85. package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-CajrYUnD.js} +1 -1
  86. package/dist/dashboard/assets/{layout-C_7A6H55.js.gz → layout-CajrYUnD.js.gz} +0 -0
  87. package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-Bx2I7XUe.js} +1 -1
  88. package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
  89. package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
  90. package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
  91. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  92. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
  94. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  95. package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
  96. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  97. package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
  98. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  99. package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
  100. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  101. package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
  102. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  103. package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
  104. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  105. package/dist/dashboard/assets/{routing-BEH32QOC.js → routing-in0f_IfK.js} +1 -1
  106. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  107. package/dist/dashboard/assets/{select-B3iTetHN.js → select-BLULNqTp.js} +2 -2
  108. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  109. package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
  110. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  111. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  112. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  113. package/dist/dashboard/assets/{stack-KrOrm7JZ.js → stack-D2lIR0tg.js} +1 -1
  114. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  115. package/dist/dashboard/assets/{state-DLS7IDMy.js → state-DTARjfiW.js} +1 -1
  116. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  117. package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-c0oPSnqW.js} +1 -1
  118. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  119. package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-m8WGNPrz.js} +1 -1
  120. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  121. package/dist/dashboard/console-backgrounds.jpg +0 -0
  122. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  123. package/dist/dashboard/index.html +2 -2
  124. package/dist/dashboard/index.html.gz +0 -0
  125. package/dist/dashboard/precompressed-assets.json +59 -57
  126. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
  127. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  128. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  129. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  130. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  131. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  132. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  133. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  134. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  135. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  136. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  137. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
  138. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  139. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  140. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  141. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  142. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  143. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  144. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  145. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  146. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  147. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  148. package/package.json +4 -5
  149. package/scripts/cli/update.ts +15 -41
  150. package/src/boot/app.ts +67 -10
  151. package/src/boot/dependencies.ts +786 -732
  152. package/src/boot/lifecycle.ts +11 -12
  153. package/src/config/cooldowns.ts +34 -55
  154. package/src/config/network.ts +8 -4
  155. package/src/config/package-paths.ts +1 -1
  156. package/src/config/providers.ts +13 -14
  157. package/src/config/resources.ts +0 -5
  158. package/src/config/runtime.ts +2 -5
  159. package/src/config/spec.ts +26 -6
  160. package/src/console/api-keys/ban-constants.ts +38 -0
  161. package/src/console/api-keys/contracts.ts +92 -31
  162. package/src/console/api-keys/routes.ts +131 -6
  163. package/src/console/audit/routes.ts +3 -3
  164. package/src/console/auth/access.ts +4 -4
  165. package/src/console/auth/account-reset.ts +5 -5
  166. package/src/console/auth/audit-recorder.ts +1 -1
  167. package/src/console/auth/lockout.ts +7 -3
  168. package/src/console/auth/row-coercion.ts +57 -12
  169. package/src/console/auth/session-resolver.ts +1 -1
  170. package/src/console/auth/session-service.ts +8 -2
  171. package/src/console/auth/session.ts +40 -34
  172. package/src/console/auth/setup-service.ts +4 -3
  173. package/src/console/auth/user-store.ts +1 -1
  174. package/src/console/backup/9router-map.ts +0 -3
  175. package/src/console/backup/contracts.ts +11 -9
  176. package/src/console/backup/export-endpoint.ts +11 -12
  177. package/src/console/backup/nine-router.ts +1 -7
  178. package/src/console/backup/routes.ts +32 -36
  179. package/src/console/backup/service.ts +468 -468
  180. package/src/console/backup/store.ts +47 -29
  181. package/src/console/backup/validate.ts +11 -1
  182. package/src/console/cli-tools/contracts.ts +7 -7
  183. package/src/console/cli-tools/injectors/cline.ts +110 -110
  184. package/src/console/cli-tools/injectors/codex.ts +181 -181
  185. package/src/console/cli-tools/injectors/copilot.ts +108 -108
  186. package/src/console/cli-tools/injectors/driver.ts +247 -247
  187. package/src/console/cli-tools/injectors/jcode.ts +115 -115
  188. package/src/console/cli-tools/injectors/openclaw.ts +161 -161
  189. package/src/console/cli-tools/injectors/opencode.ts +153 -153
  190. package/src/console/cli-tools/routes.ts +4 -4
  191. package/src/console/cli-tools/service.ts +1 -1
  192. package/src/console/cli-tools/store.ts +2 -2
  193. package/src/console/console-router-guards.ts +2 -2
  194. package/src/console/console-router.ts +36 -4
  195. package/src/console/dashboard-assets.ts +9 -9
  196. package/src/console/data-management/routes.ts +3 -3
  197. package/src/console/domain-registration.ts +18 -4
  198. package/src/console/inferhub/client.ts +0 -7
  199. package/src/console/inferhub/key-store.ts +1 -1
  200. package/src/console/inferhub/normalize.ts +2 -2
  201. package/src/console/inferhub/routes.ts +1 -1
  202. package/src/console/inferhub/types.ts +1 -1
  203. package/src/console/observability/contracts.ts +5 -1
  204. package/src/console/observability/live.ts +34 -21
  205. package/src/console/observability/logs.ts +4 -4
  206. package/src/console/observability/performance.ts +3 -3
  207. package/src/console/observability/routes.ts +3 -3
  208. package/src/console/observability/sse.ts +62 -8
  209. package/src/console/observability/store.ts +36 -20
  210. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  211. package/src/console/providers/catalog/contracts.ts +82 -22
  212. package/src/console/providers/catalog/model-operations.ts +1 -1
  213. package/src/console/providers/catalog/provider-operations.ts +110 -17
  214. package/src/console/providers/catalog/public-model-store.ts +0 -0
  215. package/src/console/providers/catalog/routes.ts +30 -6
  216. package/src/console/providers/catalog/store-helpers.ts +349 -326
  217. package/src/console/providers/catalog/store.ts +1662 -1292
  218. package/src/console/providers/detail/routes.ts +4 -4
  219. package/src/console/providers/oauth/callback-listener.ts +100 -18
  220. package/src/console/providers/oauth/routes.ts +95 -16
  221. package/src/console/quota/account-quota-global.ts +41 -3
  222. package/src/console/quota/account-quota-shared.ts +10 -3
  223. package/src/console/quota/account-quota-tenant.ts +285 -67
  224. package/src/console/quota/account-test-egress.ts +4 -4
  225. package/src/console/quota/daily-growth-job.ts +308 -0
  226. package/src/console/quota/native-identity.ts +2 -2
  227. package/src/console/quota/quota-view.ts +1 -1
  228. package/src/console/remote-support/routes.ts +1 -1
  229. package/src/console/remote-support/service.ts +2 -2
  230. package/src/console/routing/model/contracts.ts +1 -1
  231. package/src/console/routing/model/routes.ts +3 -3
  232. package/src/console/routing/pools/contracts.ts +13 -10
  233. package/src/console/routing/pools/import-parse.ts +3 -3
  234. package/src/console/routing/pools/operations.ts +14 -7
  235. package/src/console/routing/pools/probe-verdict.ts +8 -3
  236. package/src/console/routing/pools/relay-deploy.ts +12 -12
  237. package/src/console/routing/pools/routes.ts +3 -3
  238. package/src/console/routing/pools/store.ts +21 -1
  239. package/src/console/settings/contracts.ts +29 -2
  240. package/src/console/settings/routes.ts +3 -3
  241. package/src/console/settings/store.ts +20 -0
  242. package/src/console/share/share-router.ts +112 -44
  243. package/src/console/share/share-stats.ts +109 -39
  244. package/src/console/share/share-usage.ts +1 -1
  245. package/src/console/shared/errors.ts +2 -2
  246. package/src/console/shared/object.ts +18 -0
  247. package/src/console/shared/read-through-cache.ts +42 -6
  248. package/src/console/shared/transaction.ts +14 -0
  249. package/src/console/system-prompt/contracts.ts +89 -12
  250. package/src/console/system-prompt/routes.ts +98 -45
  251. package/src/console/updates/apply-update.ts +244 -0
  252. package/src/console/updates/routes.ts +256 -98
  253. package/src/main.ts +11 -5
  254. package/src/network/http2-bidi.ts +1 -1
  255. package/src/network/ip-utils.ts +298 -0
  256. package/src/network/outbound-fetch.ts +76 -5
  257. package/src/network/pool/agent.ts +7 -6
  258. package/src/network/pool/byte-accounting.ts +1 -1
  259. package/src/network/pool/resolver.ts +55 -51
  260. package/src/network/pool/selector.ts +150 -136
  261. package/src/network/pool-cooldown-store.ts +71 -16
  262. package/src/network/pool-health-machine.ts +22 -2
  263. package/src/network/rate-limit-verdict.ts +68 -0
  264. package/src/network/retry.ts +1 -1
  265. package/src/network/ssrf.ts +24 -75
  266. package/src/observability/event-log.ts +2 -2
  267. package/src/observability/event-query.ts +286 -214
  268. package/src/observability/log-sanitize.ts +1 -1
  269. package/src/observability/metrics.ts +4 -4
  270. package/src/observability/payload-capture.ts +6 -5
  271. package/src/observability/payload-store.ts +1 -1
  272. package/src/observability/runtime-metrics.ts +1 -1
  273. package/src/observability/telemetry-buffer.ts +12 -3
  274. package/src/observability/telemetry-reconciliation.ts +3 -3
  275. package/src/observability/telemetry-status.ts +1 -1
  276. package/src/observability/token-speed.ts +1 -1
  277. package/src/persistence/connection-url.ts +1 -1
  278. package/src/persistence/db-handle.ts +1 -1
  279. package/src/persistence/db-pg.ts +2 -2
  280. package/src/persistence/db-sqlite.ts +10 -5
  281. package/src/persistence/dialect.ts +5 -1
  282. package/src/persistence/readiness.ts +19 -7
  283. package/src/persistence/redis.ts +1 -1
  284. package/src/persistence/schema.ts +177 -45
  285. package/src/persistence/shared-cache.ts +2 -2
  286. package/src/persistence/sort-order.ts +1 -1
  287. package/src/persistence/stores/api-key-store.ts +109 -4
  288. package/src/persistence/stores/share-store.ts +20 -8
  289. package/src/persistence/stores/telemetry-store.ts +1 -1
  290. package/src/persistence/system-prompt-store.ts +63 -18
  291. package/src/persistence/tenant-preferences.ts +1 -1
  292. package/src/persistence/ttl-cache.ts +3 -3
  293. package/src/persistence/usage-flush.ts +7 -2
  294. package/src/protocol/canonical-model.ts +1 -1
  295. package/src/protocol/codecs.ts +12 -16
  296. package/src/protocol/dsml.ts +3 -49
  297. package/src/protocol/gateway-error.ts +6 -3
  298. package/src/protocol/image-clamp.ts +2 -2
  299. package/src/protocol/image-generation.ts +2 -2
  300. package/src/protocol/reasoning-text.ts +1 -1
  301. package/src/protocol/reasoning.ts +7 -52
  302. package/src/protocol/registry.ts +50 -15
  303. package/src/protocol/request/chat.ts +46 -19
  304. package/src/protocol/request/codex.ts +1 -1
  305. package/src/protocol/request/messages.ts +46 -14
  306. package/src/protocol/request/responses.ts +19 -10
  307. package/src/protocol/response/chat.ts +27 -12
  308. package/src/protocol/response/codex.ts +2 -2
  309. package/src/protocol/response/gemini.ts +1 -1
  310. package/src/protocol/response/messages.ts +6 -6
  311. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  312. package/src/protocol/response/responses.ts +3 -3
  313. package/src/protocol/sse-decoder.ts +64 -4
  314. package/src/protocol/stop-reason.ts +2 -2
  315. package/src/protocol/think-split.ts +347 -0
  316. package/src/protocol/translation/attachment-integrity.ts +3 -3
  317. package/src/protocol/translation/cache-controls.ts +8 -20
  318. package/src/protocol/translation/capabilities.ts +8 -8
  319. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  320. package/src/protocol/translation/dialect/inband.ts +39 -0
  321. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  322. package/src/protocol/translation/dialect/qwen.ts +114 -0
  323. package/src/protocol/translation/dialect/resolver.ts +83 -0
  324. package/src/protocol/translation/dialect/rules.ts +93 -0
  325. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  326. package/src/protocol/translation/dialect/types.ts +58 -0
  327. package/src/protocol/translation/quirks.ts +18 -4
  328. package/src/protocol/translation/thinking.ts +12 -252
  329. package/src/protocol/translation/tool-id.ts +3 -3
  330. package/src/protocol/translation/tool-repair.ts +1 -1
  331. package/src/protocol/upstream/post-json.ts +6 -1
  332. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  333. package/src/providers/adapters/base-adapter.ts +1 -1
  334. package/src/providers/adapters/compatible-adapter.ts +44 -5
  335. package/src/providers/authentication/jwt-validator.ts +0 -5
  336. package/src/providers/authentication/oauth-client.ts +254 -253
  337. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  338. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  339. package/src/providers/bundled-discovery-providers.ts +6 -30
  340. package/src/providers/configured-openai-providers.ts +82 -97
  341. package/src/providers/configured-provider.ts +1 -1
  342. package/src/providers/default-registry.ts +23 -35
  343. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  344. package/src/providers/discovery/discovery-types.ts +0 -1
  345. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  346. package/src/providers/discovery/probe-phases.ts +50 -15
  347. package/src/providers/discovery/probing-service.ts +18 -20
  348. package/src/providers/integrations/agentrouter/agentrouter.ts +14 -5
  349. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  350. package/src/providers/integrations/anthropic.ts +4 -11
  351. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  352. package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
  353. package/src/providers/integrations/antigravity/antigravity.ts +15 -26
  354. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +2 -2
  355. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  356. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  357. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  358. package/src/providers/integrations/buddy/buddy-identity.ts +108 -1
  359. package/src/providers/integrations/buddy/buddy-media.ts +16 -16
  360. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  361. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  362. package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -16
  363. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  364. package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -10
  365. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  366. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  367. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  368. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  369. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  370. package/src/providers/integrations/claude/claude.ts +3 -10
  371. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  372. package/src/providers/integrations/cline/cline.ts +2 -2
  373. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  374. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  375. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  376. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  377. package/src/providers/integrations/codex/codex.ts +14 -15
  378. package/src/providers/integrations/commandcode/commandcode.ts +76 -116
  379. package/src/providers/integrations/commandcode/quota.ts +132 -48
  380. package/src/providers/integrations/cursor/cursor-http2.ts +11 -14
  381. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  382. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  383. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  384. package/src/providers/integrations/cursor/cursor.ts +53 -20
  385. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  386. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  387. package/src/providers/integrations/factory/factory.ts +13 -13
  388. package/src/providers/integrations/gemini.ts +11 -11
  389. package/src/providers/integrations/github/github-discovery.ts +55 -10
  390. package/src/providers/integrations/github/github-oauth.ts +6 -5
  391. package/src/providers/integrations/github/github.ts +47 -20
  392. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  393. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  394. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  395. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  396. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  397. package/src/providers/integrations/grok/grok.ts +88 -42
  398. package/src/providers/integrations/inferhub.ts +4 -11
  399. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  400. package/src/providers/integrations/kimi/kimi.ts +4 -11
  401. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  402. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  403. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  404. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  405. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  406. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  407. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  408. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  409. package/src/providers/integrations/kiro/kiro.ts +91 -78
  410. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  411. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  412. package/src/providers/integrations/muse/muse.ts +6 -9
  413. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  414. package/src/providers/integrations/opencode/provider.ts +17 -24
  415. package/src/providers/integrations/perplexity.ts +2 -2
  416. package/src/providers/integrations/qoder/qoder-catalog.ts +132 -17
  417. package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
  418. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
  419. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  420. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  421. package/src/providers/integrations/qoder/qoder-realm.ts +22 -28
  422. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  423. package/src/providers/integrations/qoder/qoder-umid.ts +33 -38
  424. package/src/providers/integrations/qoder/qoder-virtual.ts +14 -2
  425. package/src/providers/integrations/qoder/qoder.ts +521 -63
  426. package/src/providers/integrations/shared/connect.ts +0 -54
  427. package/src/providers/integrations/shared/hash.ts +15 -0
  428. package/src/providers/integrations/shared/session-cache.ts +136 -0
  429. package/src/providers/integrations/tokenharbor.ts +2 -2
  430. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  431. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  432. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  433. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  434. package/src/providers/integrations/trae/trae.ts +59 -19
  435. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  436. package/src/providers/integrations/zcode/oauth.ts +1 -1
  437. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  438. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  439. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  440. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  441. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  442. package/src/providers/operations/account-error-classification.ts +131 -116
  443. package/src/providers/operations/account-health-service.ts +31 -39
  444. package/src/providers/operations/account-reset-service.ts +58 -1
  445. package/src/providers/operations/cli-platform.ts +31 -5
  446. package/src/providers/operations/client-version-resolver.ts +4 -6
  447. package/src/providers/operations/client-versions.ts +10 -7
  448. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  449. package/src/providers/operations/provider-catalog-service.ts +22 -6
  450. package/src/providers/operations/provider-credential-service.ts +35 -6
  451. package/src/providers/operations/upstream-deadline.ts +3 -3
  452. package/src/providers/provider-contracts.ts +65 -10
  453. package/src/providers/provider-credential-resolver.ts +1 -1
  454. package/src/providers/provider-metadata.ts +17 -2
  455. package/src/providers/provider-model-definition.ts +22 -1
  456. package/src/providers/provider-probe.ts +4 -2
  457. package/src/providers/provider-registry.ts +4 -4
  458. package/src/providers/provider-usage.ts +81 -16
  459. package/src/providers/quota/cache.ts +29 -0
  460. package/src/providers/quota/quota-contracts.ts +28 -10
  461. package/src/providers/quota/refresh.ts +29 -6
  462. package/src/providers/quota/snapshot.ts +94 -0
  463. package/src/providers/search/search-provider.ts +6 -8
  464. package/src/providers/search/search-providers.ts +2 -1
  465. package/src/security/abuse.ts +8 -8
  466. package/src/security/admission/reasons.ts +17 -17
  467. package/src/security/admission/service.ts +2 -2
  468. package/src/security/admission/ttl.ts +61 -61
  469. package/src/security/api-key-auth.ts +20 -8
  470. package/src/security/client-router-fingerprint.ts +4 -4
  471. package/src/security/credential-codec.ts +3 -3
  472. package/src/security/csrf.ts +2 -2
  473. package/src/security/ip-boundary.ts +16 -189
  474. package/src/security/model-access-rule.ts +1 -1
  475. package/src/security/outbound-headers.ts +18 -0
  476. package/src/shared/coerce.ts +19 -0
  477. package/src/shared/invoke-guard.ts +47 -0
  478. package/src/transport/contracts.ts +13 -4
  479. package/src/transport/dispatch/attempt-finalize.ts +5 -4
  480. package/src/transport/dispatch/attempt-loop.ts +10 -7
  481. package/src/transport/dispatch/capture-policy.ts +34 -37
  482. package/src/transport/dispatch/empty-stream.ts +28 -25
  483. package/src/transport/dispatch/generation.ts +33 -53
  484. package/src/transport/dispatch/leases.ts +8 -8
  485. package/src/transport/dispatch/native-shared.ts +127 -0
  486. package/src/transport/dispatch/proxy-request.ts +10 -8
  487. package/src/transport/dispatch/responses-compact.ts +35 -56
  488. package/src/transport/dispatch/retry-policy.ts +4 -67
  489. package/src/transport/dispatch/stream-bridge.ts +1 -1
  490. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  491. package/src/transport/dispatch/streaming/finalize.ts +11 -10
  492. package/src/transport/dispatch/streaming/stream.ts +7 -3
  493. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  494. package/src/transport/dispatch/systemone.ts +27 -55
  495. package/src/transport/dispatch/token-count.ts +49 -46
  496. package/src/transport/dispatch/websearch.ts +24 -50
  497. package/src/transport/drain-endpoint.ts +3 -3
  498. package/src/transport/failure-policy.ts +19 -6
  499. package/src/transport/guards/auth-guard.ts +6 -11
  500. package/src/transport/guards/body-policy.ts +7 -7
  501. package/src/transport/guards/error-lifecycle.ts +18 -3
  502. package/src/transport/guards/guard-errors.ts +7 -3
  503. package/src/transport/guards/headroom-guard.ts +2 -2
  504. package/src/transport/guards/pipeline.ts +7 -7
  505. package/src/transport/guards/readiness-guard.ts +16 -2
  506. package/src/transport/guards/request-context.ts +15 -20
  507. package/src/transport/request/preparer.ts +306 -27
  508. package/src/transport/request/rtk/compress-request.ts +3 -3
  509. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  510. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  511. package/src/transport/request/state.ts +5 -20
  512. package/src/transport/request/system-prompt-injection.ts +74 -11
  513. package/src/transport/request/tenant-preferences.ts +22 -0
  514. package/src/transport/routing/candidate-index.ts +2 -2
  515. package/src/transport/routing/round-robin.ts +1 -1
  516. package/src/transport/routing/route-catalog.ts +113 -19
  517. package/src/transport/routing/route-model.ts +131 -38
  518. package/src/transport/routing/router.ts +154 -39
  519. package/src/transport/shutdown-notice.ts +21 -9
  520. package/src/transport/surface/adapters.ts +3 -3
  521. package/src/transport/surface/chat/encode.ts +7 -7
  522. package/src/transport/surface/chat/parse.ts +7 -7
  523. package/src/transport/surface/completion.ts +3 -3
  524. package/src/transport/surface/content-parts.ts +1 -1
  525. package/src/transport/surface/dialects.ts +2 -2
  526. package/src/transport/surface/messages/encode.ts +1 -1
  527. package/src/transport/surface/messages/parse.ts +3 -3
  528. package/src/transport/surface/responses/adapter.ts +1 -1
  529. package/src/transport/surface/responses/errors.ts +0 -9
  530. package/src/transport/vision-handoff-cache.ts +58 -0
  531. package/src/transport/vision-handoff.ts +376 -0
  532. package/src/workers/account-identity-sweep.ts +2 -2
  533. package/src/workers/buddy-growth-tasks.ts +392 -346
  534. package/src/workers/daily-checkin.ts +102 -49
  535. package/src/workers/maintenance-lease.ts +2 -35
  536. package/src/workers/oauth-refresh-sweep.ts +8 -1
  537. package/src/workers/quota-refresh-sweep.ts +120 -82
  538. package/src/workers/scheduler.ts +67 -4
  539. package/src/workers/sweep-runner.ts +24 -5
  540. package/src/workers/zcode-gift-claim.ts +108 -0
  541. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  542. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  543. package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
  544. package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
  545. package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
  546. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js +0 -1
  547. package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
  548. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
  549. package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
  550. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
  551. package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
  552. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
  553. package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
  554. package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
  555. package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
  556. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
  557. package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
  558. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
  559. package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
  560. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  561. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  562. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +0 -1
  563. package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
  564. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
  565. package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
  566. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
  567. package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
  568. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
  569. package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
  570. package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
  571. package/dist/dashboard/assets/SortableList-BAL5npBC.js +0 -1
  572. package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
  573. package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
  574. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
  575. package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
  576. package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
  577. package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
  578. package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
  579. package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
  580. package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
  581. package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
  582. package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
  583. package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
  584. package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
  585. package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
  586. package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
  587. package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
  588. package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
  589. package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
  590. package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
  591. package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
  592. package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
  593. package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
  594. package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
  595. package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
  596. package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
  597. package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
  598. package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
  599. package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
  600. package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
  601. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  602. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  603. package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
  604. package/dist/dashboard/assets/numbers-BNDZnsUE.js +0 -2
  605. package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
  606. package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
  607. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  608. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  609. package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
  610. package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
  611. package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
  612. package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
  613. package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
  614. package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
  615. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  616. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  617. package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
  618. package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
  619. package/src/network/response-headers.ts +0 -39
  620. package/src/protocol/response/dsml.ts +0 -38
  621. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  622. /package/dist/dashboard/assets/{app-wvwCi4Eh.js → app-Bgd3zANQ.js} +0 -0
  623. /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-Bgd3zANQ.js.gz} +0 -0
  624. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  625. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -1,5 +1,9 @@
1
1
  // Daily check-in ride-along.
2
2
  //
3
+ // dynamic-import-ok: the qoder/buddy check-in imports are lazy-loaded
4
+ // inside the account loop (B-02) so the boot graph stays clean — this runs
5
+ // as a ride-along with the quota sweep, not at startup.
6
+ //
3
7
  // WorkBuddy/CodeBuddy grant a free credit package once per calendar day. This
4
8
  // claims it for every stored account and logs one line per account, so an
5
9
  // operator can confirm each account got its daily login credit.
@@ -23,21 +27,36 @@ import type { CartethyiaDatabase } from "../persistence/db-handle";
23
27
  import { dailyCheckinLedger, networkPools } from "../persistence/schema";
24
28
  import type { ProviderId } from "../providers/provider-contracts";
25
29
  import { log } from "../observability/logger";
26
- import {
27
- DAILY_CHECKIN_PROVIDER_IDS,
28
- checkinDayKey,
29
- fetchDailyCheckin,
30
- type DailyCheckinResult,
31
- } from "../providers/integrations/buddy/buddy-checkin";
32
- import {
33
- fetchQoderDailyCheckin,
34
- isQoderCheckinProvider,
35
- } from "../providers/integrations/qoder/qoder-checkin";
36
- import { DrizzleQoderNativeIdentityStore } from "../providers/integrations/qoder/qoder-native-identity-store";
30
+ import type { DailyCheckinResult } from "../providers/integrations/buddy/buddy-checkin";
31
+ import { BUDDY_PROVIDER_IDS } from "../providers/provider-metadata";
37
32
 
38
- export { DAILY_CHECKIN_PROVIDER_IDS, checkinDayKey };
39
33
  export type { DailyCheckinResult };
40
34
 
35
+ /**
36
+ * Calendar-day key in UTC, matching the provider's own day boundary.
37
+ *
38
+ * Mirrors `buddy-checkin.ts`'s `checkinDayKey` (a pure two-line util). Kept
39
+ * local so this worker module does not statically import the heavy buddy-checkin
40
+ * graph just for a date key (B-02).
41
+ */
42
+ export function checkinDayKey(now: Date = new Date()): string {
43
+ return now.toISOString().slice(0, 10);
44
+ }
45
+
46
+ /**
47
+ * The Buddy and Qoder providers the daily check-in claims a credit for.
48
+ *
49
+ * Derived from the shared buddy family set plus the Qoder check-in list, the
50
+ * same shape as `buddy-checkin.ts`'s `DAILY_CHECKIN_PROVIDER_IDS`. Kept local
51
+ * so this worker module does not statically import the heavy buddy/qoder
52
+ * graphs for a constant id list (B-02).
53
+ */
54
+ const QODER_CHECKIN_PROVIDER_IDS: readonly string[] = ["qoder", "qodercn"];
55
+ export const DAILY_CHECKIN_PROVIDER_IDS: readonly string[] = [
56
+ ...BUDDY_PROVIDER_IDS,
57
+ ...QODER_CHECKIN_PROVIDER_IDS,
58
+ ];
59
+
41
60
  /**
42
61
  * Pending check-in requests handed to the sweep by a caller outside it.
43
62
  *
@@ -270,6 +289,38 @@ export async function markCheckinAttempted(args: {
270
289
  * An account absent from the result was not attempted, matching the single
271
290
  * read's "absent means unknown, never claimed".
272
291
  */
292
+ /**
293
+ * Shared Redis marker read. One `MGET` when the client supports it; otherwise
294
+ * (a minimal double, or a client without the command) a `GET` loop. Returns
295
+ * `undefined` when the ledger is unreadable — unknown, never "not claimed".
296
+ */
297
+ async function readRedisMarkers(
298
+ redis: RedisClient,
299
+ dayKey: string,
300
+ accountIds: readonly string[],
301
+ ): Promise<Map<string, boolean> | undefined> {
302
+ const attempted = new Map<string, boolean>();
303
+ try {
304
+ if (typeof redis.mget === "function") {
305
+ // One command for every account: one key per account is small, and a
306
+ // single `MGET` keeps the cost flat instead of linear in the count.
307
+ const values = await redis.mget(
308
+ ...accountIds.map((id) => ledgerKey(dayKey, id)),
309
+ );
310
+ accountIds.forEach((accountId, index) => {
311
+ attempted.set(accountId, values[index] != null);
312
+ });
313
+ return attempted;
314
+ }
315
+ for (const accountId of accountIds) {
316
+ attempted.set(accountId, (await redis.get(ledgerKey(dayKey, accountId))) !== null);
317
+ }
318
+ return attempted;
319
+ } catch {
320
+ return undefined;
321
+ }
322
+ }
323
+
273
324
  export async function checkinAttemptedTodayBatch(args: {
274
325
  readonly db: CartethyiaDatabase | undefined;
275
326
  readonly redis: RedisClient | undefined;
@@ -279,23 +330,11 @@ export async function checkinAttemptedTodayBatch(args: {
279
330
  const attempted = new Map<string, boolean>();
280
331
  if (args.accountIds.length === 0) return attempted;
281
332
  const dayKey = checkinDayKey(args.now?.() ?? new Date());
282
- // A client without MGET (the memory backend) takes the DB path below, or
283
- // reports unknown when there is no DB either.
284
- if (args.redis !== undefined && typeof args.redis.mget === "function") {
285
- try {
286
- // One command for every account: one key per account is small, and a
287
- // single `MGET` keeps the cost flat instead of linear in the count.
288
- const values = (await args.redis.mget(
289
- ...args.accountIds.map((id) => ledgerKey(dayKey, id)),
290
- )) as Array<string | null>;
291
- args.accountIds.forEach((accountId, index) => {
292
- attempted.set(accountId, values[index] != null);
293
- });
294
- return attempted;
295
- } catch {
296
- // Fall through: an unreadable ledger means unknown, and the DB path is
297
- // the only other place the marker could be.
298
- }
333
+ if (args.redis !== undefined) {
334
+ const fromRedis = await readRedisMarkers(args.redis, dayKey, args.accountIds);
335
+ if (fromRedis !== undefined) return fromRedis;
336
+ // Fall through: an unreadable ledger means unknown, and the DB path is
337
+ // the only other place the marker could be.
299
338
  }
300
339
  if (args.db === undefined) return attempted;
301
340
  try {
@@ -326,11 +365,8 @@ export async function checkinAttemptedToday(args: {
326
365
  }): Promise<boolean> {
327
366
  const dayKey = checkinDayKey(args.now?.() ?? new Date());
328
367
  if (args.redis !== undefined) {
329
- try {
330
- return (await args.redis.get(ledgerKey(dayKey, args.accountId))) !== null;
331
- } catch {
332
- return false;
333
- }
368
+ const fromRedis = await readRedisMarkers(args.redis, dayKey, [args.accountId]);
369
+ if (fromRedis !== undefined) return fromRedis.get(args.accountId) ?? false;
334
370
  }
335
371
  if (args.db === undefined) return false;
336
372
  try {
@@ -384,7 +420,7 @@ export function withDirectFallback(fetcher: typeof fetch): typeof fetch {
384
420
  * (`buddy-activity-report`) with its own ledger, so it is not re-POSTed on
385
421
  * every sweep. See `buddy-growth-tasks.ts`.
386
422
  */
387
- export type DailyGrowthPassResult = {
423
+ type DailyGrowthPassResult = {
388
424
  readonly checkin: DailyCheckinResult;
389
425
  };
390
426
 
@@ -428,20 +464,37 @@ export async function attemptDailyGrowthPass(args: {
428
464
  }
429
465
  let checkin: DailyCheckinResult;
430
466
  try {
431
- checkin = isQoderCheckinProvider(String(providerId))
432
- ? await fetchQoderDailyCheckin({
433
- providerId: String(providerId),
434
- accountId,
435
- credential,
436
- fetcher,
437
- ...(db ? { nativeIdentityStore: new DrizzleQoderNativeIdentityStore(db) } : {}),
438
- })
439
- : await fetchDailyCheckin({
440
- providerId,
441
- accountId,
442
- credential,
443
- fetcher,
444
- });
467
+ // The buddy/qoder check-in graphs are lazy-loaded: only the provider on
468
+ // this account needs its surface, and neither should sit on the boot graph
469
+ // (B-02).
470
+ const isQoderCheckinProvider = (
471
+ await import("../providers/integrations/qoder/qoder-checkin")
472
+ ).isQoderCheckinProvider;
473
+ if (isQoderCheckinProvider(String(providerId))) {
474
+ const { fetchQoderDailyCheckin } = await import(
475
+ "../providers/integrations/qoder/qoder-checkin"
476
+ );
477
+ const { DrizzleQoderNativeIdentityStore } = await import(
478
+ "../providers/integrations/qoder/qoder-native-identity-store"
479
+ );
480
+ checkin = await fetchQoderDailyCheckin({
481
+ providerId: String(providerId),
482
+ accountId,
483
+ credential,
484
+ fetcher,
485
+ ...(db ? { nativeIdentityStore: new DrizzleQoderNativeIdentityStore(db) } : {}),
486
+ });
487
+ } else {
488
+ const { fetchDailyCheckin } = await import(
489
+ "../providers/integrations/buddy/buddy-checkin"
490
+ );
491
+ checkin = await fetchDailyCheckin({
492
+ providerId,
493
+ accountId,
494
+ credential,
495
+ fetcher,
496
+ });
497
+ }
445
498
  } catch (error) {
446
499
  checkin = {
447
500
  providerId,
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import { randomUUID } from "node:crypto";
18
18
 
19
+ import { redisEvalNumber } from "../persistence/redis";
19
20
  import type { RedisClient } from "../persistence/redis";
20
21
 
21
22
  /** Key prefix; the task name is the discriminator. */
@@ -98,29 +99,6 @@ export function leaseTtlMs(intervalMs: number): number {
98
99
  return Math.min(MAX_LEASE_TTL_MS, Math.max(MIN_LEASE_TTL_MS, Math.trunc(intervalMs)));
99
100
  }
100
101
 
101
- /** A store that hands out no lease — used when Redis is absent. */
102
- export function createNoopMaintenanceLeaseStore(): MaintenanceLeaseStore {
103
- return {
104
- async tryAcquire(taskName) {
105
- return {
106
- taskName,
107
- // Held = true: with no shared store this process cannot coordinate, so
108
- // the local registry's own re-entrancy rule is the only guard. The
109
- // alternative — reporting not-held — would silently disable every
110
- // maintenance task on single-process deployments, which is most of
111
- // them.
112
- held: true,
113
- async renew() {
114
- return true;
115
- },
116
- async release() {
117
- /* nothing to give back */
118
- },
119
- };
120
- },
121
- };
122
- }
123
-
124
102
  /**
125
103
  * A Redis-backed lease store.
126
104
  *
@@ -134,18 +112,7 @@ export function createRedisMaintenanceLeaseStore(redis: RedisClient): Maintenanc
134
112
  script: string,
135
113
  key: string,
136
114
  args: readonly (string | number)[],
137
- ): Promise<number> => {
138
- const raw = await (
139
- redis as unknown as {
140
- eval: (
141
- script: string,
142
- numKeys: number,
143
- ...args: (string | number)[]
144
- ) => Promise<unknown>;
145
- }
146
- ).eval(script, 1, key, ...args);
147
- return Number(raw);
148
- };
115
+ ): Promise<number> => redisEvalNumber(redis, script, 1, key, ...args);
149
116
 
150
117
  return {
151
118
  async tryAcquire(taskName, ttlMs) {
@@ -9,7 +9,7 @@ export interface DueOAuthAccount {
9
9
  readonly providerId: string;
10
10
  }
11
11
 
12
- export interface OAuthRefreshSweepDeps {
12
+ interface OAuthRefreshSweepDeps {
13
13
  readonly loadDueAccounts: () => Promise<readonly DueOAuthAccount[]>;
14
14
  readonly refreshService: Pick<OAuthRefreshService, "ensureFreshAccessToken">;
15
15
  /** Resolves a provider's token-endpoint client; accounts with no registered refresher are skipped. */
@@ -17,6 +17,12 @@ export interface OAuthRefreshSweepDeps {
17
17
  readonly skewMs?: number;
18
18
  /** Maximum accounts in one completed wave. Defaults to 5. */
19
19
  readonly maxConcurrency?: number;
20
+ /**
21
+ * Wall-clock cap for the whole pass; the next wave does not start past it
22
+ * (G-04). Mirrors the quota refresh sweep's 45 s pass budget so OAuth
23
+ * refreshes cannot run away into a heavy foreground minute.
24
+ */
25
+ readonly budgetMs?: number;
20
26
  /**
21
27
  * Milliseconds to pause between accounts when running sequentially. Absent
22
28
  * keeps the default wave behavior. Providers whose token endpoint rate-limits
@@ -70,6 +76,7 @@ export async function oauthRefreshSweep(deps: OAuthRefreshSweepDeps): Promise<vo
70
76
  ...(deps.interItemDelayMs === undefined
71
77
  ? {}
72
78
  : { pace: { interItemDelayMs: deps.interItemDelayMs } }),
79
+ ...(deps.budgetMs === undefined ? {} : { budgetMs: deps.budgetMs }),
73
80
  });
74
81
 
75
82
  // A pass that could not even list its accounts reports no tick: an all-zero
@@ -97,7 +97,7 @@ const DEFAULT_MAX_CHECKINS_PER_PASS = 10;
97
97
  * its automatic check-in — only a manual press, which bypasses the cap, ever
98
98
  * reaches it.
99
99
  */
100
- export function checkinSlotAvailable(
100
+ function checkinSlotAvailable(
101
101
  providerId: string,
102
102
  checkinsSoFar: number,
103
103
  maxCheckins: number,
@@ -136,6 +136,112 @@ function getErrorMessage(error: unknown): string {
136
136
  return error instanceof Error ? error.message : String(error);
137
137
  }
138
138
 
139
+ /** One candidate the sweep considers: the target plus its cache age. */
140
+ type SweepEntry = { target: QuotaRefreshTarget; ageMs: number };
141
+
142
+ /**
143
+ * Selects the batch for one pass: eligible (due or queued) accounts first,
144
+ * buddy accounts pulled ahead of age ordering so the check-in ride-along is
145
+ * not starved by older non-buddy rows, capped at `maxPerPass`.
146
+ */
147
+ function selectBatch(
148
+ entries: readonly SweepEntry[],
149
+ minAgeMs: number,
150
+ maxPerPass: number,
151
+ listPendingCheckins: () => readonly PendingCheckinRequest[],
152
+ ): { batch: SweepEntry[]; skipped: number } {
153
+ // An account added since the last pass is admitted regardless of cache
154
+ // age: it has no quota reading at all yet, so the normal freshness gate
155
+ // would make a brand-new buddy account wait a full sweep cycle before it
156
+ // could claim its credit — the import enqueued it precisely so it would
157
+ // not have to. Only the check-in needs this; the quota leg is happy to
158
+ // fill a cold cache the moment it is asked to.
159
+ const queued = new Set(
160
+ listPendingCheckins()
161
+ .filter((request) => supportsDailyCheckin(request.providerId))
162
+ .map((request) => request.accountId),
163
+ );
164
+ const due = entries.filter(
165
+ (entry) => entry.ageMs >= minAgeMs || queued.has(entry.target.accountId),
166
+ );
167
+ due.sort((left, right) => {
168
+ const leftQueued = queued.has(left.target.accountId) ? 0 : 1;
169
+ const rightQueued = queued.has(right.target.accountId) ? 0 : 1;
170
+ if (leftQueued !== rightQueued) return leftQueued - rightQueued;
171
+ const leftBuddy = supportsDailyCheckin(left.target.providerId) ? 0 : 1;
172
+ const rightBuddy = supportsDailyCheckin(right.target.providerId) ? 0 : 1;
173
+ if (leftBuddy !== rightBuddy) return leftBuddy - rightBuddy;
174
+ return right.ageMs - left.ageMs;
175
+ });
176
+ const batch = due.slice(0, maxPerPass);
177
+ return { batch, skipped: entries.length - due.length + (due.length - batch.length) };
178
+ }
179
+
180
+ /**
181
+ * Runs the daily check-in ride-along for one batch item: claims its slot
182
+ * before the first await (the wave runs concurrently, so the count read after
183
+ * resolution would let the whole wave through), then attempts the growth pass
184
+ * and records the outcome. Bounded by the per-pass cap.
185
+ */
186
+ async function runCheckinRideAlong(
187
+ deps: QuotaRefreshSweepDeps,
188
+ entry: SweepEntry,
189
+ resolveCredentialCached: (providerId: ProviderId, accountId: string) => Promise<string>,
190
+ state: { checkins: number; claimed: number; maxCheckins: number },
191
+ checkinOutcomes: Map<string, DailyCheckinResult>,
192
+ ): Promise<void> {
193
+ // The provider filter comes *before* the slot is consumed. The batch is
194
+ // oldest-first across every provider, so checking the cap first spends the
195
+ // whole per-pass budget on non-buddy accounts that the next line would
196
+ // skip anyway — a buddy account past the cap (a cbcn row behind ten older
197
+ // non-buddy rows) is never checked in automatically, only when an operator
198
+ // presses the button, which bypasses this cap entirely.
199
+ if (!checkinSlotAvailable(entry.target.providerId, state.checkins, state.maxCheckins)) return;
200
+ // Claim the slot before the first await. The wave runs its items
201
+ // concurrently, and reading `checkins` only after the growth pass
202
+ // resolves lets every item in the wave pass the cap check against the
203
+ // same stale count — the cap then bounds nothing.
204
+ state.checkins += 1;
205
+ try {
206
+ // A `ValidatedFetch` shares the fetch call signature; the only
207
+ // difference is Bun's `preconnect` static, so a single assertion is
208
+ // enough (the double cast added nothing).
209
+ const pooled: typeof fetch | undefined = deps.checkinFetcherFor
210
+ ? ((await deps.checkinFetcherFor(
211
+ entry.target.accountId,
212
+ entry.target.tenantId,
213
+ )) as typeof fetch)
214
+ : undefined;
215
+ const fetcher = pooled ?? deps.checkinFetcher;
216
+ const pass = await attemptDailyGrowthPass({
217
+ db: deps.db,
218
+ redis: deps.redis,
219
+ providerId: entry.target.providerId as ProviderId,
220
+ accountId: entry.target.accountId,
221
+ resolveCredential: resolveCredentialCached,
222
+ ...(fetcher ? { fetcher } : {}),
223
+ });
224
+ // The request has been served: the pass either ran and wrote its own
225
+ // day marker, or declined because today's slot was already settled.
226
+ // Holding the entry would only make the next pass re-admit an account
227
+ // it just handled.
228
+ dequeueCheckin(entry.target.accountId);
229
+ if (pass !== null) {
230
+ if (pass.checkin.state === "claimed") state.claimed += 1;
231
+ checkinOutcomes.set(entry.target.accountId, pass.checkin);
232
+ }
233
+ } catch (error) {
234
+ checkinOutcomes.set(entry.target.accountId, {
235
+ providerId: entry.target.providerId,
236
+ accountId: entry.target.accountId,
237
+ state: "error",
238
+ credit: null,
239
+ streakDays: null,
240
+ error: getErrorMessage(error),
241
+ });
242
+ }
243
+ }
244
+
139
245
  /** `provider/identity` — the account's own label, else its id. */
140
246
  function accountName(target: QuotaRefreshTarget): string {
141
247
  const label = (target.label ?? "").trim();
@@ -278,9 +384,10 @@ export async function quotaRefreshSweep(deps: QuotaRefreshSweepDeps): Promise<vo
278
384
  };
279
385
 
280
386
  let failed = 0;
281
- let claimed = 0;
282
- let checkins = 0;
283
387
  const maxCheckins = deps.maxCheckinsPerPass ?? DEFAULT_MAX_CHECKINS_PER_PASS;
388
+ // The ride-along mutates this shared object (not local copies), so its
389
+ // counters stay visible to the summary and the tick report after the wave.
390
+ const checkinState = { checkins: 0, claimed: 0, maxCheckins };
284
391
 
285
392
  /**
286
393
  * One log line per account, emitted after the wave completes so the quota
@@ -361,33 +468,7 @@ export async function quotaRefreshSweep(deps: QuotaRefreshSweepDeps): Promise<vo
361
468
  // happened when an operator pressed the button. Attendance is once a day
362
469
  // and quota is refreshed every pass, so letting the check-in go first
363
470
  // costs the quota leg nothing.
364
- select: (entries) => {
365
- // An account added since the last pass is admitted regardless of cache
366
- // age: it has no quota reading at all yet, so the normal freshness gate
367
- // would make a brand-new buddy account wait a full sweep cycle before it
368
- // could claim its credit — the import enqueued it precisely so it would
369
- // not have to. Only the check-in needs this; the quota leg is happy to
370
- // fill a cold cache the moment it is asked to.
371
- const queued = new Set(
372
- listPendingCheckins()
373
- .filter((request) => supportsDailyCheckin(request.providerId))
374
- .map((request) => request.accountId),
375
- );
376
- const due = entries.filter(
377
- (entry) => entry.ageMs >= minAgeMs || queued.has(entry.target.accountId),
378
- );
379
- due.sort((left, right) => {
380
- const leftQueued = queued.has(left.target.accountId) ? 0 : 1;
381
- const rightQueued = queued.has(right.target.accountId) ? 0 : 1;
382
- if (leftQueued !== rightQueued) return leftQueued - rightQueued;
383
- const leftBuddy = supportsDailyCheckin(left.target.providerId) ? 0 : 1;
384
- const rightBuddy = supportsDailyCheckin(right.target.providerId) ? 0 : 1;
385
- if (leftBuddy !== rightBuddy) return leftBuddy - rightBuddy;
386
- return right.ageMs - left.ageMs;
387
- });
388
- const batch = due.slice(0, maxPerPass);
389
- return { batch, skipped: entries.length - due.length + (due.length - batch.length) };
390
- },
471
+ select: (entries) => selectBatch(entries, minAgeMs, maxPerPass, listPendingCheckins),
391
472
  run: async (entry) => {
392
473
  attemptedTargets.push(entry.target);
393
474
  const outcome = await refreshAccountQuota(
@@ -418,56 +499,13 @@ export async function quotaRefreshSweep(deps: QuotaRefreshSweepDeps): Promise<vo
418
499
  // `attemptDailyGrowthPass`). Bounded per pass, and skipped for providers
419
500
  // without the billing route — a day marker makes this a no-op on every
420
501
  // pass after the first of the day.
421
- //
422
- // The provider filter comes *before* the slot is consumed. The batch is
423
- // oldest-first across every provider, so checking the cap first spends the
424
- // whole per-pass budget on non-buddy accounts that the next line would
425
- // skip anyway — a buddy account past the cap (a cbcn row behind ten older
426
- // non-buddy rows) is never checked in automatically, only when an operator
427
- // presses the button, which bypasses this cap entirely.
428
- if (!checkinSlotAvailable(entry.target.providerId, checkins, maxCheckins)) return;
429
- // Claim the slot before the first await. The wave runs its items
430
- // concurrently, and reading `checkins` only after the growth pass
431
- // resolves lets every item in the wave pass the cap check against the
432
- // same stale count — the cap then bounds nothing.
433
- checkins += 1;
434
- try {
435
- // is only for the `preconnect` static Bun adds to its own fetch (the
436
- // same cast the adapters use for `outbound_fetch`).
437
- const pooled = deps.checkinFetcherFor
438
- ? ((await deps.checkinFetcherFor(
439
- entry.target.accountId,
440
- entry.target.tenantId,
441
- )) as unknown as typeof fetch)
442
- : undefined;
443
- const fetcher = pooled ?? deps.checkinFetcher;
444
- const pass = await attemptDailyGrowthPass({
445
- db: deps.db,
446
- redis: deps.redis,
447
- providerId: entry.target.providerId as ProviderId,
448
- accountId: entry.target.accountId,
449
- resolveCredential: resolveCredentialCached,
450
- ...(fetcher ? { fetcher } : {}),
451
- });
452
- // The request has been served: the pass either ran and wrote its own
453
- // day marker, or declined because today's slot was already settled.
454
- // Holding the entry would only make the next pass re-admit an account
455
- // it just handled.
456
- dequeueCheckin(entry.target.accountId);
457
- if (pass !== null) {
458
- if (pass.checkin.state === "claimed") claimed += 1;
459
- checkinOutcomes.set(entry.target.accountId, pass.checkin);
460
- }
461
- } catch (error) {
462
- checkinOutcomes.set(entry.target.accountId, {
463
- providerId: entry.target.providerId,
464
- accountId: entry.target.accountId,
465
- state: "error",
466
- credit: null,
467
- streakDays: null,
468
- error: getErrorMessage(error),
469
- });
470
- }
502
+ await runCheckinRideAlong(
503
+ deps,
504
+ entry,
505
+ resolveCredentialCached,
506
+ checkinState,
507
+ checkinOutcomes,
508
+ );
471
509
  },
472
510
  onItemError: (entry, error) => {
473
511
  failed += 1;
@@ -486,7 +524,7 @@ export async function quotaRefreshSweep(deps: QuotaRefreshSweepDeps): Promise<vo
486
524
  now,
487
525
  });
488
526
 
489
- logSweepAccounts(attemptedTargets, quotaOutcomes, checkinOutcomes, failed, claimed);
527
+ logSweepAccounts(attemptedTargets, quotaOutcomes, checkinOutcomes, failed, checkinState.claimed);
490
528
 
491
529
  // Release the slots for the accounts whose check-in errored, in one
492
530
  // round-trip. Releasing inline per account makes a pass that budgets out with
@@ -507,6 +545,6 @@ export async function quotaRefreshSweep(deps: QuotaRefreshSweepDeps): Promise<vo
507
545
  attempted: result.attempted,
508
546
  skipped: result.skipped,
509
547
  failed,
510
- checkins,
548
+ checkins: checkinState.checkins,
511
549
  });
512
550
  }
@@ -4,6 +4,7 @@
4
4
  * behavior.
5
5
  */
6
6
  import { log } from "../observability/logger";
7
+ import { foregroundBusy } from "../shared/invoke-guard";
7
8
  import type { MaintenanceLeaseStore } from "./maintenance-lease";
8
9
  import { leaseTtlMs } from "./maintenance-lease";
9
10
 
@@ -12,6 +13,21 @@ export interface ScheduledTask {
12
13
  readonly intervalMs: number;
13
14
  /** Delay before the first run after the registry starts. */
14
15
  readonly initialDelayMs?: number;
16
+ /**
17
+ * True when this task should defer its tick while foreground activity is in
18
+ * flight (console requests / proxy invocations) — the G-03 InvokeGuard.
19
+ *
20
+ * Opt-in: tasks that only publish in-process gauges (`runtime-metrics`) or
21
+ * run at a far cadence (`telemetry-retention`) have no reason to yield, and
22
+ * a task that must run on schedule (a lease hold) would be the wrong place
23
+ * to pause. The heavy sweeps set it.
24
+ *
25
+ * The defer is a bounded pause, never a skip: a task waits up to
26
+ * {@link FOREGROUND_DEFER_MAX_MS} for the foreground to clear, then runs
27
+ * regardless. Foreground starvation of maintenance is worse than a sweep
28
+ * and foreground sharing one tick.
29
+ */
30
+ readonly deferWhenBusy?: boolean;
15
31
  /**
16
32
  * Whether this task's work is SHARED across the fleet, so only one process
17
33
  * should run it per interval.
@@ -28,7 +44,7 @@ export interface ScheduledTask {
28
44
  run(): Promise<unknown> | void;
29
45
  }
30
46
 
31
- export interface ScheduledTaskRegistryOptions {
47
+ interface ScheduledTaskRegistryOptions {
32
48
  /**
33
49
  * Cross-process coordinator. When present, a task that cannot acquire the
34
50
  * fleet lease SKIPS its tick rather than waiting — the next interval is the
@@ -62,6 +78,12 @@ export async function runGrowingWaves<T>(
62
78
  readonly maxConcurrency: number;
63
79
  readonly onItem: (item: T) => Promise<void> | void;
64
80
  readonly shouldStop?: () => boolean;
81
+ /**
82
+ * Invoked before each wave (after `shouldStop`). The sweep runner uses it
83
+ * to pause while foreground activity is busy (G-03); a caller that does not
84
+ * need that passes nothing.
85
+ */
86
+ readonly beforeWave?: () => Promise<void> | void;
65
87
  },
66
88
  ): Promise<void> {
67
89
  const limit = Math.max(1, options.maxConcurrency);
@@ -69,6 +91,7 @@ export async function runGrowingWaves<T>(
69
91
  let waveSize = Math.min(DEFAULT_INITIAL_BATCH_SIZE, limit);
70
92
  while (offset < items.length) {
71
93
  if (options.shouldStop?.() === true) return;
94
+ await options.beforeWave?.();
72
95
  const wave = items.slice(offset, offset + waveSize);
73
96
  offset += wave.length;
74
97
  await Promise.all(wave.map((item) => options.onItem(item)));
@@ -76,7 +99,7 @@ export async function runGrowingWaves<T>(
76
99
  }
77
100
  }
78
101
 
79
- export const DEFAULT_INITIAL_BATCH_SIZE = 2;
102
+ const DEFAULT_INITIAL_BATCH_SIZE = 2;
80
103
 
81
104
  /**
82
105
  * How long a tick may wait for the maintenance lease.
@@ -88,6 +111,28 @@ export const DEFAULT_INITIAL_BATCH_SIZE = 2;
88
111
  */
89
112
  const LEASE_ACQUIRE_TIMEOUT_MS = 5_000;
90
113
 
114
+ /**
115
+ * How long a deferring task waits for foreground activity to clear before
116
+ * running anyway (G-03). Deliberately shorter than any sweep budget: the pause
117
+ * is a courtesy, not a schedule, and a continuous page/stream flood must not
118
+ * starve maintenance forever.
119
+ */
120
+ const FOREGROUND_DEFER_MAX_MS = 3_000;
121
+
122
+ /** Poll interval while waiting for foreground to clear. */
123
+ const FOREGROUND_DEFER_POLL_MS = 50;
124
+
125
+ /**
126
+ * Bounded wait for foreground activity to clear. Returns once the foreground
127
+ * is idle or the deadline elapses — never blocks past `FOREGROUND_DEFER_MAX_MS`.
128
+ */
129
+ async function deferForForeground(now: () => number, sleep: (ms: number) => Promise<void>): Promise<void> {
130
+ const until = now() + FOREGROUND_DEFER_MAX_MS;
131
+ while (foregroundBusy() && now() < until) {
132
+ await sleep(FOREGROUND_DEFER_POLL_MS);
133
+ }
134
+ }
135
+
91
136
  /** Resolves with the value, or rejects if `promise` has not settled in time. */
92
137
  async function withDeadline<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
93
138
  // `clearTimeout` no-ops for an absent handle, so it needs no guard. A live
@@ -277,6 +322,14 @@ export class ScheduledTaskRegistry {
277
322
  // every task fires at once once the blocker finishes, which is the burst
278
323
  // this rule exists to prevent.
279
324
  if (this.busy !== undefined) return;
325
+ // G-03: a foreground-deferring task waits out foreground activity (bounded)
326
+ // before latching the shared gate. The gate is NOT latched during the wait,
327
+ // so the re-check below is required: a different task can claim the gate
328
+ // (and finish) while this one is waiting.
329
+ if (task.deferWhenBusy === true) {
330
+ await deferForForeground(this.now, (ms) => new Promise((r) => setTimeout(r, ms)));
331
+ }
332
+ if (this.running.has(task.name) || this.busy !== undefined) return;
280
333
  this.running.add(task.name);
281
334
  this.busy = task.name;
282
335
  let lease: Awaited<ReturnType<MaintenanceLeaseStore["tryAcquire"]>> | undefined;
@@ -332,8 +385,18 @@ export class ScheduledTaskRegistry {
332
385
  log.error(`[scheduled-task:${task.name}] failed`, error as Error);
333
386
  // Released on failure too: the error is this process's, not the
334
387
  // fleet's, and holding the lease until it expires would block the
335
- // next tick for no reason.
336
- if (lease !== undefined) await lease.release().catch(() => undefined);
388
+ // next tick for no reason. A failed release is logged — the TTL is
389
+ // the backstop, but the failure should stay visible.
390
+ if (lease !== undefined) {
391
+ try {
392
+ await lease.release();
393
+ } catch (releaseError) {
394
+ log.warn(
395
+ `[scheduled-task:${task.name}] lease release failed after task error`,
396
+ releaseError as Error,
397
+ );
398
+ }
399
+ }
337
400
  } finally {
338
401
  // No-ops for an absent handle, and the renewal timer is always
339
402
  // assigned before this runs.