fix(fleet): heartbeat consistency — MOSAIC_HOME path + configurable interval #595

Merged
jason.woltje merged 1 commits from feat/f3-pi-harness-hb into main 2026-06-21 23:25:54 +00:00
3 changed files with 29 additions and 2 deletions

View File

@@ -6,7 +6,7 @@ MOSAIC_TMUX_SOCKET=${MOSAIC_TMUX_SOCKET:-mosaic-factory}
MOSAIC_AGENT_RUNTIME=${MOSAIC_AGENT_RUNTIME:-pi}
MOSAIC_AGENT_WORKDIR=${MOSAIC_AGENT_WORKDIR:-$HOME}
MOSAIC_AGENT_COMMAND=${MOSAIC_AGENT_COMMAND:-}
MOSAIC_HEARTBEAT_RUN_DIR=${MOSAIC_HEARTBEAT_RUN_DIR:-$HOME/.config/mosaic/fleet/run}
MOSAIC_HEARTBEAT_RUN_DIR=${MOSAIC_HEARTBEAT_RUN_DIR:-${MOSAIC_HOME:-$HOME/.config/mosaic}/fleet/run}
MOSAIC_HEARTBEAT_INTERVAL=${MOSAIC_HEARTBEAT_INTERVAL:-15}
if [ -z "$AGENT_NAME" ]; then

View File

@@ -851,6 +851,23 @@ describe('fleet ps — heartbeat parsing', () => {
expect(hb.health).toBe('unknown');
expect(hb.ts).toBeNull();
});
it('honors MOSAIC_HEARTBEAT_INTERVAL for the freshness threshold', () => {
const prev = process.env.MOSAIC_HEARTBEAT_INTERVAL;
try {
// A 60s-old beat is STALE at the default 15s interval (3x15 = 45s)...
const ts = new Date(NOW - 60_000).toISOString();
const content = `ts=${ts}\npid=1\nstatus=ok\n`;
delete process.env.MOSAIC_HEARTBEAT_INTERVAL;
expect(parseHeartbeat(content, NOW).health).toBe('stale');
// ...but HEALTHY when the operator widened the interval to 30s (3x30 = 90s).
process.env.MOSAIC_HEARTBEAT_INTERVAL = '30';
expect(parseHeartbeat(content, NOW).health).toBe('healthy');
} finally {
if (prev === undefined) delete process.env.MOSAIC_HEARTBEAT_INTERVAL;
else process.env.MOSAIC_HEARTBEAT_INTERVAL = prev;
}
});
});
describe('fleet ps — systemd show parsing', () => {

View File

@@ -368,6 +368,16 @@ export function buildAgentTailCommand(
// ---------------------------------------------------------------------------
export const HEARTBEAT_INTERVAL_MS = 15_000;
/**
* Heartbeat interval in ms, honoring MOSAIC_HEARTBEAT_INTERVAL (seconds) so the
* `fleet ps` freshness threshold matches the writer sidecar's actual cadence
* (start-agent-session.sh). Falls back to HEARTBEAT_INTERVAL_MS (15s).
*/
export function heartbeatIntervalMs(): number {
const sec = Number.parseInt(process.env.MOSAIC_HEARTBEAT_INTERVAL ?? '', 10);
return Number.isFinite(sec) && sec > 0 ? sec * 1000 : HEARTBEAT_INTERVAL_MS;
}
export const HEARTBEAT_HEALTHY_MULTIPLIER = 3;
export interface HeartbeatInfo {
@@ -496,7 +506,7 @@ export function parseHeartbeat(content: string | null, nowMs = Date.now()): Hear
status = val;
}
}
const thresholdMs = HEARTBEAT_INTERVAL_MS * HEARTBEAT_HEALTHY_MULTIPLIER;
const thresholdMs = heartbeatIntervalMs() * HEARTBEAT_HEALTHY_MULTIPLIER;
let health: 'healthy' | 'stale' | 'unknown' = 'unknown';
let ageMs: number | null = null;
if (ts !== null) {