Co-authored-by: jason.woltje <jason@diversecanvas.com> Co-committed-by: jason.woltje <jason@diversecanvas.com>
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
/**
|
|
* 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<AgentLiveness[]> {
|
|
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<string> {
|
|
const rows = await this.read();
|
|
rows.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
const dot: Record<string, string> = { 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');
|
|
}
|
|
}
|