From bda38bddc135b71f1f8683da68fa512388dc5f0e Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 21 Jun 2026 20:17:42 -0500 Subject: [PATCH] feat(fleet): surface self-reported model in fleet ps parseHeartbeat now reads an optional model= line from the heartbeat file (written by native runtime heartbeats) into HeartbeatInfo.model, and fleet ps surfaces it as a MODEL column (table) and in --json (via rows[].heartbeat.model). Legacy/sidecar heartbeats omit the line and report model=null, so the column shows '-'. Closes the model self-report gap end-to-end with the native Pi heartbeat writer (F3-m2): the runtime self-reports its active model and the fleet operator can see it in ps. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EsgTQzV5YUGk1JtCLP4B83 --- packages/mosaic/src/commands/fleet.spec.ts | 11 +++++++++++ packages/mosaic/src/commands/fleet.ts | 13 +++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/mosaic/src/commands/fleet.spec.ts b/packages/mosaic/src/commands/fleet.spec.ts index 401469d..f0c3262 100644 --- a/packages/mosaic/src/commands/fleet.spec.ts +++ b/packages/mosaic/src/commands/fleet.spec.ts @@ -833,6 +833,17 @@ describe('fleet ps — heartbeat parsing', () => { expect(hb.pid).toBe(12345); expect(hb.status).toBe('ok'); expect(hb.ageMs).toBe(10_000); + // No model= line in a legacy/sidecar heartbeat → model is null. + expect(hb.model).toBeNull(); + }); + + it('parses a self-reported model id from a native heartbeat (model= line)', () => { + const ts = new Date(NOW - 5_000).toISOString(); + const content = `ts=${ts}\npid=42\nstatus=busy\nmodel=openai-codex/gpt-5.5:high\n`; + const hb = parseHeartbeat(content, NOW); + expect(hb.model).toBe('openai-codex/gpt-5.5:high'); + expect(hb.status).toBe('busy'); + expect(hb.health).toBe('healthy'); }); it('reports stale when heartbeat is older than 3×interval', () => { diff --git a/packages/mosaic/src/commands/fleet.ts b/packages/mosaic/src/commands/fleet.ts index 8b1aee1..eb0c462 100644 --- a/packages/mosaic/src/commands/fleet.ts +++ b/packages/mosaic/src/commands/fleet.ts @@ -390,6 +390,8 @@ export interface HeartbeatInfo { /** healthy | stale | unknown */ health: 'healthy' | 'stale' | 'unknown'; ageMs: number | null; + /** Model id the runtime self-reported in its heartbeat (native HB only), else null. */ + model: string | null; } export interface AgentPsRow { @@ -490,15 +492,17 @@ export function heartbeatPath(agentName: string, mosaicHome = defaultMosaicHome( * ts= * pid= * status= + * model= (optional — native runtime heartbeats self-report it) */ export function parseHeartbeat(content: string | null, nowMs = Date.now()): HeartbeatInfo { if (content === null) { - return { ts: null, pid: null, status: null, health: 'unknown', ageMs: null }; + return { ts: null, pid: null, status: null, health: 'unknown', ageMs: null, model: null }; } const lines = content.split('\n'); let ts: Date | null = null; let pid: number | null = null; let status: 'ok' | 'busy' | null = null; + let model: string | null = null; for (const line of lines) { const [key, ...rest] = line.split('='); const val = rest.join('=').trim(); @@ -510,6 +514,8 @@ export function parseHeartbeat(content: string | null, nowMs = Date.now()): Hear if (Number.isFinite(n)) pid = n; } else if (key === 'status' && (val === 'ok' || val === 'busy')) { status = val; + } else if (key === 'model' && val) { + model = val; } } const thresholdMs = heartbeatIntervalMs() * HEARTBEAT_HEALTHY_MULTIPLIER; @@ -519,7 +525,7 @@ export function parseHeartbeat(content: string | null, nowMs = Date.now()): Hear ageMs = nowMs - ts.getTime(); health = ageMs <= thresholdMs ? 'healthy' : 'stale'; } - return { ts, pid, status, health, ageMs }; + return { ts, pid, status, health, ageMs, model }; } /** @@ -1123,6 +1129,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps = 'PID'.padEnd(8), 'IDLE'.padEnd(8), 'HB'.padEnd(12), + 'MODEL'.padEnd(22), 'FLAGS', ].join(' '); console.log(header); @@ -1137,6 +1144,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps = row.heartbeat.ageMs !== null ? `${Math.round(row.heartbeat.ageMs / 1000)}s/${row.heartbeat.health}` : `unknown`; + const model = row.heartbeat.model ?? '-'; const flags: string[] = []; if (!row.managed) flags.push('UNMANAGED'); if (row.driftFlag) flags.push('DRIFT'); @@ -1153,6 +1161,7 @@ export function registerFleetCommand(program: Command, deps: FleetCommandDeps = pid.padEnd(8), idle.padEnd(8), hbAge.padEnd(12), + model.padEnd(22), flags.join(','), ].join(' '), );