From 60a309d5a43287a4902488be9f8565d8978a3d80 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 21 Jun 2026 23:25:53 +0000 Subject: [PATCH] =?UTF-8?q?fix(fleet):=20heartbeat=20consistency=20?= =?UTF-8?q?=E2=80=94=20MOSAIC=5FHOME=20path=20+=20configurable=20interval?= =?UTF-8?q?=20(#595)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .../tools/fleet/start-agent-session.sh | 2 +- packages/mosaic/src/commands/fleet.spec.ts | 17 +++++++++++++++++ packages/mosaic/src/commands/fleet.ts | 12 +++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/mosaic/framework/tools/fleet/start-agent-session.sh b/packages/mosaic/framework/tools/fleet/start-agent-session.sh index 53782ff..8c3b656 100755 --- a/packages/mosaic/framework/tools/fleet/start-agent-session.sh +++ b/packages/mosaic/framework/tools/fleet/start-agent-session.sh @@ -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 diff --git a/packages/mosaic/src/commands/fleet.spec.ts b/packages/mosaic/src/commands/fleet.spec.ts index 0b641d5..e7fe766 100644 --- a/packages/mosaic/src/commands/fleet.spec.ts +++ b/packages/mosaic/src/commands/fleet.spec.ts @@ -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', () => { diff --git a/packages/mosaic/src/commands/fleet.ts b/packages/mosaic/src/commands/fleet.ts index 54ad1b9..4a166d2 100644 --- a/packages/mosaic/src/commands/fleet.ts +++ b/packages/mosaic/src/commands/fleet.ts @@ -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) {