/** * Deterministic liveness computation (RFC-001 §4.5). * * The authoritative liveness signal is the `mosaic.presence` heartbeat, NOT * native Matrix presence. Given the age of an agent's last heartbeat and a * policy, these pure functions classify online/away/offline the same way every * time — which is exactly what makes the A3 "hard-killed agent flips to * offline within dark_threshold" guarantee deterministic and testable without * standing up a homeserver. */ import type { AgentLiveness, HeartbeatObservation, LivenessPolicy, PresenceStatus, } from './types.js'; /** * Classify a single agent from the age (ms) of its last heartbeat. * * - `age <= heartbeatIntervalMs * missTolerance` → **online** * - `age < darkThresholdMs` → **away** * - otherwise (or non-finite age) → **offline / dark** * * A non-finite age (never seen / NaN) fails safe to `offline`: we never assert * a liveness we cannot substantiate. */ export function classifyLiveness(ageMs: number, policy: LivenessPolicy): PresenceStatus { if (!Number.isFinite(ageMs)) { return 'offline'; } const onlineWindowMs = policy.heartbeatIntervalMs * policy.missTolerance; if (ageMs <= onlineWindowMs) { return 'online'; } if (ageMs < policy.darkThresholdMs) { return 'away'; } return 'offline'; } /** * Compute liveness for every observed agent at wall-clock `nowMs`. * The result order mirrors the input order (stable for display). */ export function computeFleetLiveness( observations: readonly HeartbeatObservation[], nowMs: number, policy: LivenessPolicy, ): AgentLiveness[] { return observations.map((o) => { const ageMs = nowMs - o.lastSeenTs; return { slug: o.slug, mxid: o.mxid, status: classifyLiveness(ageMs, policy), lastSeenTs: o.lastSeenTs, ageMs, lastSeq: o.lastSeq, }; }); }