/** * v2.8 — Dashboard-state decay broadcaster. * * Periodically computes `deriveDashboardState` for every registered * agent and fires `broadcastDashboardEvent({ event: 'agent.status_changed', kind: })` * when the derived state CHANGES between ticks. Pure time-based decay: * even when no mutation fires, transitions like `active → waiting → * stale → closed` need to surface on the dashboard wire. * * Architectural calls locked during the v2.8 dashboard state-machine * design review: * - HTTP daemon process only (stdio skips it). * - `setInterval` at `RELAY_DECAY_TICK_MS` (default 30s). * - O(N) per tick where N = registered agents; tiny. * - In-process Map for dedup. Lost on restart; * first-tick-after-restart emits a fresh event for every agent. * - Opt-out via `RELAY_DECAY_TICK_DISABLED=1` (test rigs, smoke). * * The broadcaster takes its dependencies via constructor injection so * unit tests inject a fake clock + fake getAgents + fake broadcastFn * without monkey-patching globals. The HTTP-side wiring at * `src/transport/http.ts` constructs the real instance with the live * dependencies. */ import { type AgentStateInputs, type AgentStateThresholds, type DashboardAgentState } from "./agent-state-machine.js"; import type { DashboardEvent } from "./transport/websocket.js"; /** * Per-agent snapshot the broadcaster needs to derive state. Sourced * from the agents table + a pre-computed pending count. Caller is * responsible for the COUNT query (broadcaster stays pure-ish — no DB * handle here, just data in). */ export interface BroadcasterAgentSnapshot { /** Agent name — used as entity_id on the broadcast event. */ name: string; /** Observable facts for deriveDashboardState. */ inputs: AgentStateInputs; } export type AgentSnapshotsProvider = () => BroadcasterAgentSnapshot[]; export type BroadcastFn = (evt: DashboardEvent) => void; export interface BroadcasterDeps { /** Snapshot provider — typically reads from `agents` table + pending-message counts. */ getAgents: AgentSnapshotsProvider; /** Broadcast sink — typically `broadcastDashboardEvent` from websocket.ts. */ broadcast: BroadcastFn; /** * Tick interval in ms. Defaults to `RELAY_DECAY_TICK_MS` env (30000). * Must be > 0. */ tickIntervalMs?: number; /** Thresholds — defaults to env-resolved. */ thresholds?: AgentStateThresholds; /** Clock for transition timestamps. Defaults to Date.now. */ now?: () => number; /** * Timer factory. Tests inject a fake to drive ticks without * `vi.useFakeTimers()` global pollution. */ scheduler?: { setInterval: (cb: () => void, ms: number) => { stop: () => void; }; }; } /** Default scheduler — real setInterval. */ export declare function realIntervalScheduler(): NonNullable; /** Resolve the decay tick interval from env + defaults. */ export declare function resolveTickIntervalMs(env?: Record): number; /** Is the broadcaster disabled via env? */ export declare function isDisabledByEnv(env?: Record): boolean; export declare class DashboardStateBroadcaster { private readonly deps; /** * Last broadcasted state per agent. Used to dedup: only fire a * broadcast when the derived state actually changes between ticks. */ private readonly lastBroadcastedState; private handle; constructor(deps: BroadcasterDeps); /** * Start the periodic tick. No-op if already started. Returns this * for fluent chaining at the http.ts wire-up site. */ start(): this; /** Stop the tick + clear the dedup state. Idempotent. */ stop(): void; /** * Public entrypoint for tests to drive a single tick without timer * scheduling. Production callers should `start()` instead. */ tick(): void; /** Test introspection — return a snapshot of the dedup map. */ getLastBroadcastedState(): Map; /** Test introspection — running state. */ isRunning(): boolean; } //# sourceMappingURL=dashboard-state-broadcaster.d.ts.map