cartethyia 2.7.4 → 2.7.6
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.
- package/.env.example +5 -0
- package/README.md +18 -6
- package/dist/dashboard/assets/BannedPage-B1N603cM.js +1 -0
- package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js +3 -0
- package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-D762MIkp.js +1 -0
- package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js +899 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js +1 -0
- package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-C6WlpU5E.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-DxjvUzSD.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +1 -0
- package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js +1 -0
- package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-44BZrxFC.js +1 -0
- package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-DRPavCdU.js} +4 -4
- package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B7M7LvMM.js +56 -0
- package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
- package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
- package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js +1 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-BaX2GTrn.js} +1 -1
- package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DympBNq4.js → banner-DFH4Bkte.js} +1 -1
- package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CxDw0lK6.js} +1 -1
- package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +1 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-DfS1ntiq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-QqPul6sC.js} +1 -1
- package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
- package/dist/dashboard/assets/entry-D6WvojeD.js +4 -0
- package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-u7JPB5BX.js} +1 -1
- package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
- package/dist/dashboard/assets/{index-DBNPOfhV.js → index-Ko4dLRnV.js} +2 -2
- package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-CHJQwxLa.js} +1 -1
- package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
- package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BlmlizVV.js} +1 -1
- package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-Bz93I-yE.js} +1 -1
- package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Ml16KCfu.js} +1 -1
- package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
- package/dist/dashboard/assets/live-7_QpnYGu.js +1 -0
- package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/network-CuMDaTzT.js +1 -0
- package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
- package/dist/dashboard/assets/pagination-B5aOoweZ.js +1 -0
- package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai.webp +0 -0
- package/dist/dashboard/assets/providers/zcode.webp +0 -0
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-fnCsxgNO.js +1 -0
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BHqpd-bg.js} +2 -2
- package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
- package/dist/dashboard/assets/settings-BxslsErn.js +1 -0
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-j7cXjgYX.js} +1 -1
- package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
- package/dist/dashboard/assets/{state-CkfdYWel.js → state-DHmEnLa2.js} +1 -1
- package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
- package/dist/dashboard/assets/{switch-SYm237HG.js → switch-DmZcyTIC.js} +1 -1
- package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-CbCP4w1o.js} +1 -1
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +60 -59
- package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
- package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
- package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
- package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
- package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
- package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
- package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
- package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
- package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
- package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
- package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
- package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
- package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
- package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
- package/package.json +4 -6
- package/scripts/cli/update.ts +15 -41
- package/src/boot/app.ts +58 -2
- package/src/boot/dependencies.ts +794 -732
- package/src/boot/lifecycle.ts +11 -12
- package/src/config/cooldowns.ts +34 -55
- package/src/config/network.ts +1 -1
- package/src/config/package-paths.ts +1 -1
- package/src/config/providers.ts +19 -14
- package/src/config/resources.ts +0 -5
- package/src/config/runtime.ts +2 -5
- package/src/config/spec.ts +33 -6
- package/src/config/usage.ts +27 -0
- package/src/console/api-keys/ban-constants.ts +38 -0
- package/src/console/api-keys/contracts.ts +71 -62
- package/src/console/api-keys/routes.ts +56 -24
- package/src/console/auth/access.ts +4 -4
- package/src/console/auth/account-reset.ts +1 -1
- package/src/console/auth/audit-recorder.ts +1 -1
- package/src/console/auth/lockout.ts +7 -3
- package/src/console/auth/row-coercion.ts +57 -12
- package/src/console/auth/session-resolver.ts +1 -1
- package/src/console/auth/session-service.ts +8 -2
- package/src/console/auth/session.ts +37 -31
- package/src/console/auth/setup-service.ts +4 -3
- package/src/console/auth/user-store.ts +1 -1
- package/src/console/backup/9router-map.ts +0 -3
- package/src/console/backup/contracts.ts +9 -9
- package/src/console/backup/export-endpoint.ts +5 -6
- package/src/console/backup/nine-router.ts +1 -7
- package/src/console/backup/routes.ts +29 -33
- package/src/console/backup/service.ts +468 -468
- package/src/console/backup/store.ts +47 -29
- package/src/console/backup/validate.ts +11 -1
- package/src/console/cli-tools/contracts.ts +43 -299
- package/src/console/cli-tools/fs-ops.ts +29 -0
- package/src/console/cli-tools/injectors/claude.ts +3 -3
- package/src/console/cli-tools/injectors/codex.ts +181 -181
- package/src/console/cli-tools/injectors/driver.ts +150 -247
- package/src/console/cli-tools/injectors/kilo.ts +146 -21
- package/src/console/cli-tools/injectors/opencode.ts +176 -153
- package/src/console/cli-tools/routes.ts +7 -1
- package/src/console/cli-tools/service.ts +8 -7
- package/src/console/cli-tools/store.ts +2 -2
- package/src/console/console-router.ts +32 -0
- package/src/console/dashboard-assets.ts +9 -9
- package/src/console/domain-registration.ts +16 -2
- package/src/console/inferhub/client.ts +0 -7
- package/src/console/inferhub/key-store.ts +1 -1
- package/src/console/inferhub/normalize.ts +2 -2
- package/src/console/inferhub/routes.ts +1 -1
- package/src/console/inferhub/types.ts +1 -1
- package/src/console/observability/contracts.ts +7 -1
- package/src/console/observability/live.ts +31 -18
- package/src/console/observability/logs.ts +1 -1
- package/src/console/observability/sse.ts +62 -8
- package/src/console/observability/store.ts +23 -4
- package/src/console/providers/catalog/catalog-projections.ts +1 -1
- package/src/console/providers/catalog/contracts.ts +39 -0
- package/src/console/providers/catalog/model-operations.ts +1 -1
- package/src/console/providers/catalog/provider-operations.ts +95 -17
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +27 -3
- package/src/console/providers/catalog/store.ts +147 -8
- package/src/console/providers/detail/routes.ts +1 -1
- package/src/console/providers/oauth/callback-listener.ts +100 -18
- package/src/console/providers/oauth/routes.ts +92 -13
- package/src/console/quota/account-quota-global.ts +41 -3
- package/src/console/quota/account-quota-shared.ts +10 -3
- package/src/console/quota/account-quota-tenant.ts +259 -66
- package/src/console/quota/daily-growth-job.ts +308 -0
- package/src/console/quota/native-identity.ts +2 -2
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/routes.ts +1 -1
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/contracts.ts +1 -1
- package/src/console/routing/pools/contracts.ts +2 -2
- package/src/console/routing/pools/import-parse.ts +3 -3
- package/src/console/routing/pools/relay-deploy.ts +3 -3
- package/src/console/settings/contracts.ts +29 -2
- package/src/console/settings/store.ts +20 -0
- package/src/console/share/share-router.ts +100 -32
- package/src/console/share/share-stats.ts +11 -13
- package/src/console/share/share-usage.ts +1 -1
- package/src/console/shared/errors.ts +2 -2
- package/src/console/shared/object.ts +18 -0
- package/src/console/shared/read-through-cache.ts +4 -4
- package/src/console/shared/transaction.ts +14 -0
- package/src/console/system-prompt/contracts.ts +89 -12
- package/src/console/system-prompt/routes.ts +98 -45
- package/src/console/updates/apply-update.ts +244 -0
- package/src/console/updates/routes.ts +252 -97
- package/src/main.ts +11 -5
- package/src/network/http2-bidi.ts +1 -1
- package/src/network/ip-utils.ts +298 -0
- package/src/network/outbound-fetch.ts +1 -1
- package/src/network/pool/agent.ts +7 -6
- package/src/network/pool/byte-accounting.ts +1 -1
- package/src/network/pool/resolver.ts +16 -12
- package/src/network/pool/selector.ts +150 -136
- package/src/network/pool-health-machine.ts +1 -1
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/retry.ts +1 -1
- package/src/network/ssrf.ts +20 -71
- package/src/observability/event-log.ts +9 -2
- package/src/observability/event-query.ts +61 -291
- package/src/observability/log-sanitize.ts +1 -1
- package/src/observability/metrics.ts +4 -4
- package/src/observability/payload-capture.ts +6 -5
- package/src/observability/payload-store.ts +1 -1
- package/src/observability/runtime-metrics.ts +1 -1
- package/src/observability/telemetry-buffer.ts +19 -3
- package/src/observability/telemetry-reconciliation.ts +3 -3
- package/src/observability/token-speed.ts +1 -1
- package/src/persistence/connection-url.ts +1 -1
- package/src/persistence/db-handle.ts +1 -1
- package/src/persistence/db-pg.ts +2 -2
- package/src/persistence/db-sqlite.ts +3 -3
- package/src/persistence/dialect.ts +5 -1
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/redis.ts +1 -1
- package/src/persistence/schema.ts +121 -40
- package/src/persistence/shared-cache.ts +2 -2
- package/src/persistence/sort-order.ts +1 -1
- package/src/persistence/stores/api-key-store.ts +29 -2
- package/src/persistence/stores/share-store.ts +20 -8
- package/src/persistence/system-prompt-store.ts +63 -18
- package/src/persistence/tenant-preferences.ts +1 -1
- package/src/persistence/ttl-cache.ts +3 -3
- package/src/persistence/usage-flush.ts +7 -2
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/codecs.ts +6 -10
- package/src/protocol/dsml.ts +8 -125
- package/src/protocol/image-clamp.ts +2 -2
- package/src/protocol/reasoning-text.ts +1 -1
- package/src/protocol/reasoning.ts +7 -52
- package/src/protocol/registry.ts +50 -15
- package/src/protocol/request/chat.ts +46 -19
- package/src/protocol/request/codex.ts +1 -1
- package/src/protocol/request/messages.ts +46 -14
- package/src/protocol/request/responses.ts +19 -10
- package/src/protocol/response/chat.ts +25 -10
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/messages.ts +1 -1
- package/src/protocol/response/omp-tool-call-markup.ts +1 -1
- package/src/protocol/response/responses.ts +1 -1
- package/src/protocol/sse-decoder.ts +50 -1
- package/src/protocol/stop-reason.ts +2 -2
- package/src/protocol/think-split.ts +347 -0
- package/src/protocol/translation/attachment-integrity.ts +3 -3
- package/src/protocol/translation/cache-controls.ts +8 -20
- package/src/protocol/translation/capabilities.ts +6 -6
- package/src/protocol/translation/dialect/deepseek.ts +10 -0
- package/src/protocol/translation/dialect/inband.ts +39 -0
- package/src/protocol/translation/dialect/model-thinking.ts +89 -0
- package/src/protocol/translation/dialect/qwen.ts +114 -0
- package/src/protocol/translation/dialect/resolver.ts +83 -0
- package/src/protocol/translation/dialect/rules.ts +93 -0
- package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
- package/src/protocol/translation/dialect/types.ts +58 -0
- package/src/protocol/translation/quirks.ts +18 -4
- package/src/protocol/translation/thinking.ts +12 -252
- package/src/protocol/translation/tool-id.ts +3 -3
- package/src/protocol/translation/tool-repair.ts +1 -1
- package/src/protocol/upstream/post-json.ts +6 -1
- package/src/providers/adapters/compatible-adapter.ts +71 -4
- package/src/providers/authentication/jwt-validator.ts +0 -5
- package/src/providers/authentication/oauth-client.ts +254 -253
- package/src/providers/authentication/oauth-flow-store.ts +42 -7
- package/src/providers/bundled-discovery-providers.ts +6 -30
- package/src/providers/configured-openai-providers.ts +82 -97
- package/src/providers/default-registry.ts +35 -37
- package/src/providers/discovery/catalog/catalog-format.ts +13 -2
- package/src/providers/discovery/discovery-types.ts +0 -1
- package/src/providers/discovery/openai-model-discovery.ts +2 -2
- package/src/providers/discovery/probe-phases.ts +48 -13
- package/src/providers/discovery/probing-service.ts +6 -8
- package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
- package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +88 -28
- package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
- package/src/providers/integrations/antigravity/antigravity.ts +22 -13
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
- package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
- package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
- package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
- package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
- package/src/providers/integrations/claude/claude-cch.ts +148 -148
- package/src/providers/integrations/claude/fallback-credit.ts +3 -7
- package/src/providers/integrations/cline/cline.ts +1 -1
- package/src/providers/integrations/codex/codex-device-code.ts +101 -101
- package/src/providers/integrations/codex/codex-oauth.ts +0 -1
- package/src/providers/integrations/commandcode/commandcode.ts +75 -115
- package/src/providers/integrations/commandcode/quota.ts +131 -51
- package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
- package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
- package/src/providers/integrations/cursor/cursor-request.ts +0 -87
- package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
- package/src/providers/integrations/cursor/cursor.ts +35 -3
- package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
- package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
- package/src/providers/integrations/factory/factory.ts +9 -9
- package/src/providers/integrations/github/github-discovery.ts +70 -12
- package/src/providers/integrations/github/github-oauth.ts +184 -14
- package/src/providers/integrations/github/github.ts +134 -26
- package/src/providers/integrations/grok/grok-oauth.ts +261 -261
- package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
- package/src/providers/integrations/grok/grok-quota.ts +16 -7
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
- package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
- package/src/providers/integrations/grok/grok.ts +86 -36
- package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
- package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
- package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
- package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
- package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
- package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
- package/src/providers/integrations/kiro/kiro-request.ts +8 -11
- package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
- package/src/providers/integrations/kiro/kiro.ts +80 -68
- package/src/providers/integrations/muse/muse-oauth.ts +0 -1
- package/src/providers/integrations/muse/muse-quota.ts +0 -4
- package/src/providers/integrations/opencode/fingerprint.ts +7 -5
- package/src/providers/integrations/opencode/provider.ts +17 -24
- package/src/providers/integrations/qoder/qoder-catalog.ts +94 -11
- package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
- package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
- package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
- package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
- package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
- package/src/providers/integrations/qoder/qoder.ts +422 -71
- package/src/providers/integrations/shared/connect.ts +0 -54
- package/src/providers/integrations/shared/hash.ts +15 -0
- package/src/providers/integrations/shared/session-cache.ts +136 -0
- package/src/providers/integrations/trae/trae-cache.ts +7 -87
- package/src/providers/integrations/trae/trae-catalog.ts +1 -6
- package/src/providers/integrations/trae/trae-protocol.ts +17 -8
- package/src/providers/integrations/trae/trae-realm.ts +0 -7
- package/src/providers/integrations/trae/trae.ts +51 -8
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
- package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
- package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
- package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
- package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
- package/src/providers/operations/account-error-classification.ts +198 -133
- package/src/providers/operations/account-health-service.ts +31 -39
- package/src/providers/operations/account-reset-service.ts +58 -1
- package/src/providers/operations/byok-wire-profile.ts +19 -15
- package/src/providers/operations/cli-platform.ts +31 -5
- package/src/providers/operations/client-version-resolver.ts +4 -6
- package/src/providers/operations/client-versions.ts +10 -7
- package/src/providers/operations/provider-catalog-seeder.ts +2 -0
- package/src/providers/operations/provider-catalog-service.ts +18 -2
- package/src/providers/operations/provider-credential-service.ts +19 -2
- package/src/providers/provider-contracts.ts +18 -7
- package/src/providers/provider-metadata.ts +17 -2
- package/src/providers/provider-model-definition.ts +22 -1
- package/src/providers/provider-probe.ts +4 -2
- package/src/providers/provider-usage.ts +50 -19
- package/src/providers/quota/cache.ts +29 -0
- package/src/providers/quota/quota-contracts.ts +4 -0
- package/src/providers/quota/refresh.ts +27 -4
- package/src/providers/quota/snapshot.ts +94 -0
- package/src/providers/search/search-provider.ts +1 -3
- package/src/providers/search/search-providers.ts +2 -1
- package/src/security/abuse.ts +4 -4
- package/src/security/admission/service.ts +13 -6
- package/src/security/admission/ttl.ts +61 -61
- package/src/security/api-key-auth.ts +17 -3
- package/src/security/client-router-fingerprint.ts +4 -4
- package/src/security/credential-codec.ts +3 -3
- package/src/security/csrf.ts +2 -2
- package/src/security/ip-boundary.ts +16 -189
- package/src/security/model-access-rule.ts +1 -1
- package/src/security/outbound-headers.ts +18 -0
- package/src/shared/invoke-guard.ts +47 -0
- package/src/transport/contracts.ts +13 -4
- package/src/transport/dispatch/attempt-finalize.ts +70 -22
- package/src/transport/dispatch/attempt-loop.ts +22 -0
- package/src/transport/dispatch/capture-policy.ts +34 -37
- package/src/transport/dispatch/empty-stream.ts +23 -20
- package/src/transport/dispatch/generation.ts +25 -45
- package/src/transport/dispatch/native-shared.ts +127 -0
- package/src/transport/dispatch/proxy-request.ts +3 -1
- package/src/transport/dispatch/responses-compact.ts +25 -46
- package/src/transport/dispatch/streaming/encoder.ts +18 -72
- package/src/transport/dispatch/streaming/finalize.ts +2 -2
- package/src/transport/dispatch/streaming/stream.ts +7 -3
- package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
- package/src/transport/dispatch/systemone.ts +20 -48
- package/src/transport/dispatch/token-count.ts +41 -38
- package/src/transport/dispatch/websearch.ts +19 -45
- package/src/transport/drain-endpoint.ts +1 -1
- package/src/transport/failure-policy.ts +21 -1
- package/src/transport/guards/auth-guard.ts +0 -6
- package/src/transport/guards/body-policy.ts +3 -3
- package/src/transport/guards/error-lifecycle.ts +4 -0
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +1 -1
- package/src/transport/guards/request-context.ts +12 -17
- package/src/transport/request/preparer.ts +286 -7
- package/src/transport/request/rtk/compress-request.ts +3 -3
- package/src/transport/request/rtk/rtk-constants.ts +2 -2
- package/src/transport/request/rtk/rtk-filters.ts +8 -8
- package/src/transport/request/state.ts +7 -13
- package/src/transport/request/system-prompt-injection.ts +74 -11
- package/src/transport/request/tenant-preferences.ts +22 -0
- package/src/transport/routing/candidate-index.ts +2 -2
- package/src/transport/routing/round-robin.ts +1 -1
- package/src/transport/routing/route-catalog.ts +17 -18
- package/src/transport/routing/route-model.ts +126 -33
- package/src/transport/routing/router.ts +159 -37
- package/src/transport/shutdown-notice.ts +2 -2
- package/src/transport/surface/adapters.ts +3 -3
- package/src/transport/surface/chat/encode.ts +7 -7
- package/src/transport/surface/chat/parse.ts +7 -7
- package/src/transport/surface/completion.ts +2 -2
- package/src/transport/surface/content-parts.ts +1 -1
- package/src/transport/surface/dialects.ts +2 -2
- package/src/transport/surface/messages/parse.ts +3 -3
- package/src/transport/surface/reasoning-summary.ts +9 -14
- package/src/transport/surface/responses/adapter.ts +1 -1
- package/src/transport/surface/responses/errors.ts +0 -9
- package/src/transport/vision-handoff-cache.ts +58 -0
- package/src/transport/vision-handoff.ts +431 -0
- package/src/workers/account-identity-sweep.ts +2 -2
- package/src/workers/buddy-growth-tasks.ts +392 -346
- package/src/workers/daily-checkin.ts +102 -49
- package/src/workers/maintenance-lease.ts +2 -35
- package/src/workers/oauth-refresh-sweep.ts +8 -1
- package/src/workers/quota-refresh-sweep.ts +120 -82
- package/src/workers/scheduler.ts +67 -4
- package/src/workers/sweep-runner.ts +24 -5
- package/src/workers/zcode-gift-claim.ts +108 -0
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js +0 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js +0 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +0 -1
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-BayTTy01.js +0 -9
- package/dist/dashboard/assets/app-BayTTy01.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +0 -2
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/dialog-8fiE6tec.js +0 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSAsYsmV.js +0 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-CPK47xQx.js +0 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/src/console/cli-tools/injectors/cline.ts +0 -110
- package/src/console/cli-tools/injectors/copilot.ts +0 -108
- package/src/console/cli-tools/injectors/cowork.ts +0 -64
- package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
- package/src/console/cli-tools/injectors/droid.ts +0 -96
- package/src/console/cli-tools/injectors/grok-build.ts +0 -70
- package/src/console/cli-tools/injectors/hermes.ts +0 -101
- package/src/console/cli-tools/injectors/jcode.ts +0 -115
- package/src/console/cli-tools/injectors/openclaw.ts +0 -161
- package/src/network/response-headers.ts +0 -39
- package/src/protocol/response/dsml.ts +0 -38
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
- /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
- /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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
}
|
package/src/workers/scheduler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|