/** * Fleet liveness reader (RFC-001 ยง4.5, A2/A4). * * Reads `mosaic.presence` heartbeats from the fleet presence room and computes * deterministic online/away/offline for every agent. This is the surface a * human (or the escalation watchdog, P2+) reads to answer "who's alive?". */ import { computeFleetLiveness } from './liveness.js'; import type { MinimalMatrixClient } from './matrix-client.js'; import { DEFAULT_LIVENESS_POLICY, type AgentLiveness, type LivenessPolicy } from './types.js'; export interface FleetLivenessReaderOptions { client: MinimalMatrixClient; /** The fleet presence room (id or resolved id). */ roomId: string; policy?: LivenessPolicy; /** Injectable clock for tests. Default Date.now. */ now?: () => number; /** How many timeline events to scan back. Default 200. */ scanLimit?: number; } export class FleetLivenessReader { private readonly policy: LivenessPolicy; private readonly now: () => number; constructor(private readonly opts: FleetLivenessReaderOptions) { this.policy = opts.policy ?? DEFAULT_LIVENESS_POLICY; this.now = opts.now ?? Date.now; } /** Read the room and compute current liveness for every seen agent. */ async read(): Promise { const observations = await this.opts.client.readHeartbeats( this.opts.roomId, this.opts.scanLimit ?? 200, ); return computeFleetLiveness(observations, this.now(), this.policy); } /** A compact human-readable liveness board (A4 CLI view). */ async formatBoard(): Promise { const rows = await this.read(); rows.sort((a, b) => a.slug.localeCompare(b.slug)); const dot: Record = { online: '๐ŸŸข', away: '๐ŸŸก', offline: '๐Ÿ”ด' }; const lines = rows.map( (r) => `${dot[r.status] ?? 'โšช'} ${r.slug.padEnd(16)} ${r.status.padEnd(8)} ` + `age=${(r.ageMs / 1000).toFixed(1)}s seq=${r.lastSeq} ${r.mxid}`, ); const summary = `online=${rows.filter((r) => r.status === 'online').length} ` + `away=${rows.filter((r) => r.status === 'away').length} ` + `offline=${rows.filter((r) => r.status === 'offline').length}`; return [`Fleet presence โ€” ${summary}`, ...lines].join('\n'); } }