| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- import { z } from 'zod/v4'
- import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics/growthbook.js'
- import { lazySchema } from '../utils/lazySchema.js'
- import {
- DEFAULT_POLL_CONFIG,
- type PollIntervalConfig,
- } from './pollConfigDefaults.js'
- // .min(100) on the seek-work intervals restores the old Math.max(..., 100)
- // defense-in-depth floor against fat-fingered GrowthBook values. Unlike a
- // clamp, Zod rejects the whole object on violation — a config with one bad
- // field falls back to DEFAULT_POLL_CONFIG entirely rather than being
- // partially trusted.
- //
- // The at_capacity intervals use a 0-or-≥100 refinement: 0 means "disabled"
- // (heartbeat-only mode), ≥100 is the fat-finger floor. Values 1–99 are
- // rejected so unit confusion (ops thinks seconds, enters 10) doesn't poll
- // every 10ms against the VerifyEnvironmentSecretAuth DB path.
- //
- // The object-level refines require at least one at-capacity liveness
- // mechanism enabled: heartbeat OR the relevant poll interval. Without this,
- // the hb=0, atCapMs=0 drift config (ops disables heartbeat without
- // restoring at_capacity) falls through every throttle site with no sleep —
- // tight-looping /poll at HTTP-round-trip speed.
- const zeroOrAtLeast100 = {
- message: 'must be 0 (disabled) or ≥100ms',
- }
- const pollIntervalConfigSchema = lazySchema(() =>
- z
- .object({
- poll_interval_ms_not_at_capacity: z.number().int().min(100),
- // 0 = no at-capacity polling. Independent of heartbeat — both can be
- // enabled (heartbeat runs, periodically breaks out to poll).
- poll_interval_ms_at_capacity: z
- .number()
- .int()
- .refine(v => v === 0 || v >= 100, zeroOrAtLeast100),
- // 0 = disabled; positive value = heartbeat at this interval while at
- // capacity. Runs alongside at-capacity polling, not instead of it.
- // Named non_exclusive to distinguish from the old heartbeat_interval_ms
- // (either-or semantics in pre-#22145 clients). .default(0) so existing
- // GrowthBook configs without this field parse successfully.
- non_exclusive_heartbeat_interval_ms: z.number().int().min(0).default(0),
- // Multisession (bridgeMain.ts) intervals. Defaults match the
- // single-session values so existing configs without these fields
- // preserve current behavior.
- multisession_poll_interval_ms_not_at_capacity: z
- .number()
- .int()
- .min(100)
- .default(
- DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_not_at_capacity,
- ),
- multisession_poll_interval_ms_partial_capacity: z
- .number()
- .int()
- .min(100)
- .default(
- DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_partial_capacity,
- ),
- multisession_poll_interval_ms_at_capacity: z
- .number()
- .int()
- .refine(v => v === 0 || v >= 100, zeroOrAtLeast100)
- .default(DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_at_capacity),
- // .min(1) matches the server's ge=1 constraint (work_v1.py:230).
- reclaim_older_than_ms: z.number().int().min(1).default(5000),
- session_keepalive_interval_v2_ms: z
- .number()
- .int()
- .min(0)
- .default(120_000),
- })
- .refine(
- cfg =>
- cfg.non_exclusive_heartbeat_interval_ms > 0 ||
- cfg.poll_interval_ms_at_capacity > 0,
- {
- message:
- 'at-capacity liveness requires non_exclusive_heartbeat_interval_ms > 0 or poll_interval_ms_at_capacity > 0',
- },
- )
- .refine(
- cfg =>
- cfg.non_exclusive_heartbeat_interval_ms > 0 ||
- cfg.multisession_poll_interval_ms_at_capacity > 0,
- {
- message:
- 'at-capacity liveness requires non_exclusive_heartbeat_interval_ms > 0 or multisession_poll_interval_ms_at_capacity > 0',
- },
- ),
- )
- /**
- * Fetch the bridge poll interval config from GrowthBook with a 5-minute
- * refresh window. Validates the served JSON against the schema; falls back
- * to defaults if the flag is absent, malformed, or partially-specified.
- *
- * Shared by bridgeMain.ts (standalone) and replBridge.ts (REPL) so ops
- * can tune both poll rates fleet-wide with a single config push.
- */
- export function getPollIntervalConfig(): PollIntervalConfig {
- const raw = getFeatureValue_CACHED_WITH_REFRESH<unknown>(
- 'tengu_bridge_poll_interval_config',
- DEFAULT_POLL_CONFIG,
- 5 * 60 * 1000,
- )
- const parsed = pollIntervalConfigSchema().safeParse(raw)
- return parsed.success ? parsed.data : DEFAULT_POLL_CONFIG
- }
|