/ bridge / pollConfig.ts
pollConfig.ts
  1  import { z } from 'zod/v4'
  2  import { getFeatureValue_CACHED_WITH_REFRESH } from '../services/analytics/growthbook.js'
  3  import { lazySchema } from '../utils/lazySchema.js'
  4  import {
  5    DEFAULT_POLL_CONFIG,
  6    type PollIntervalConfig,
  7  } from './pollConfigDefaults.js'
  8  
  9  // .min(100) on the seek-work intervals restores the old Math.max(..., 100)
 10  // defense-in-depth floor against fat-fingered GrowthBook values. Unlike a
 11  // clamp, Zod rejects the whole object on violation — a config with one bad
 12  // field falls back to DEFAULT_POLL_CONFIG entirely rather than being
 13  // partially trusted.
 14  //
 15  // The at_capacity intervals use a 0-or-≥100 refinement: 0 means "disabled"
 16  // (heartbeat-only mode), ≥100 is the fat-finger floor. Values 1–99 are
 17  // rejected so unit confusion (ops thinks seconds, enters 10) doesn't poll
 18  // every 10ms against the VerifyEnvironmentSecretAuth DB path.
 19  //
 20  // The object-level refines require at least one at-capacity liveness
 21  // mechanism enabled: heartbeat OR the relevant poll interval. Without this,
 22  // the hb=0, atCapMs=0 drift config (ops disables heartbeat without
 23  // restoring at_capacity) falls through every throttle site with no sleep —
 24  // tight-looping /poll at HTTP-round-trip speed.
 25  const zeroOrAtLeast100 = {
 26    message: 'must be 0 (disabled) or ≥100ms',
 27  }
 28  const pollIntervalConfigSchema = lazySchema(() =>
 29    z
 30      .object({
 31        poll_interval_ms_not_at_capacity: z.number().int().min(100),
 32        // 0 = no at-capacity polling. Independent of heartbeat — both can be
 33        // enabled (heartbeat runs, periodically breaks out to poll).
 34        poll_interval_ms_at_capacity: z
 35          .number()
 36          .int()
 37          .refine(v => v === 0 || v >= 100, zeroOrAtLeast100),
 38        // 0 = disabled; positive value = heartbeat at this interval while at
 39        // capacity. Runs alongside at-capacity polling, not instead of it.
 40        // Named non_exclusive to distinguish from the old heartbeat_interval_ms
 41        // (either-or semantics in pre-#22145 clients). .default(0) so existing
 42        // GrowthBook configs without this field parse successfully.
 43        non_exclusive_heartbeat_interval_ms: z.number().int().min(0).default(0),
 44        // Multisession (bridgeMain.ts) intervals. Defaults match the
 45        // single-session values so existing configs without these fields
 46        // preserve current behavior.
 47        multisession_poll_interval_ms_not_at_capacity: z
 48          .number()
 49          .int()
 50          .min(100)
 51          .default(
 52            DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_not_at_capacity,
 53          ),
 54        multisession_poll_interval_ms_partial_capacity: z
 55          .number()
 56          .int()
 57          .min(100)
 58          .default(
 59            DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_partial_capacity,
 60          ),
 61        multisession_poll_interval_ms_at_capacity: z
 62          .number()
 63          .int()
 64          .refine(v => v === 0 || v >= 100, zeroOrAtLeast100)
 65          .default(DEFAULT_POLL_CONFIG.multisession_poll_interval_ms_at_capacity),
 66        // .min(1) matches the server's ge=1 constraint (work_v1.py:230).
 67        reclaim_older_than_ms: z.number().int().min(1).default(5000),
 68        session_keepalive_interval_v2_ms: z
 69          .number()
 70          .int()
 71          .min(0)
 72          .default(120_000),
 73      })
 74      .refine(
 75        cfg =>
 76          cfg.non_exclusive_heartbeat_interval_ms > 0 ||
 77          cfg.poll_interval_ms_at_capacity > 0,
 78        {
 79          message:
 80            'at-capacity liveness requires non_exclusive_heartbeat_interval_ms > 0 or poll_interval_ms_at_capacity > 0',
 81        },
 82      )
 83      .refine(
 84        cfg =>
 85          cfg.non_exclusive_heartbeat_interval_ms > 0 ||
 86          cfg.multisession_poll_interval_ms_at_capacity > 0,
 87        {
 88          message:
 89            'at-capacity liveness requires non_exclusive_heartbeat_interval_ms > 0 or multisession_poll_interval_ms_at_capacity > 0',
 90        },
 91      ),
 92  )
 93  
 94  /**
 95   * Fetch the bridge poll interval config from GrowthBook with a 5-minute
 96   * refresh window. Validates the served JSON against the schema; falls back
 97   * to defaults if the flag is absent, malformed, or partially-specified.
 98   *
 99   * Shared by bridgeMain.ts (standalone) and replBridge.ts (REPL) so ops
100   * can tune both poll rates fleet-wide with a single config push.
101   */
102  export function getPollIntervalConfig(): PollIntervalConfig {
103    const raw = getFeatureValue_CACHED_WITH_REFRESH<unknown>(
104      'tengu_bridge_poll_interval_config',
105      DEFAULT_POLL_CONFIG,
106      5 * 60 * 1000,
107    )
108    const parsed = pollIntervalConfigSchema().safeParse(raw)
109    return parsed.success ? parsed.data : DEFAULT_POLL_CONFIG
110  }