From 3e53580d6311b14fc3000c23e3f15bf8e6c2baee Mon Sep 17 00:00:00 2001 From: Shakker Date: Tue, 5 May 2026 07:48:41 +0100 Subject: [PATCH] refactor: format restart handoff diagnostics --- src/infra/restart-handoff.test.ts | 23 +++++++++++++++++++++++ src/infra/restart-handoff.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/infra/restart-handoff.test.ts b/src/infra/restart-handoff.test.ts index f87df4c2ce4..cbcdad97e20 100644 --- a/src/infra/restart-handoff.test.ts +++ b/src/infra/restart-handoff.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { consumeGatewayRestartHandoffForExitedProcessSync, + formatGatewayRestartHandoffDiagnostic, GATEWAY_SUPERVISOR_RESTART_HANDOFF_FILENAME, GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND, readGatewayRestartHandoffSync, @@ -248,4 +249,26 @@ describe("gateway restart handoff", () => { }), ).toMatchObject({ pid: 12_345 }); }); + + it("formats a concise diagnostic line for status surfaces", () => { + expect( + formatGatewayRestartHandoffDiagnostic( + { + kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND, + version: 1, + intentId: "intent-1", + pid: 12_345, + createdAt: 10_000, + expiresAt: 70_000, + reason: "plugin source changed", + source: "plugin-change", + restartKind: "full-process", + supervisorMode: "launchd", + }, + 12_500, + ), + ).toBe( + "Recent restart handoff: full-process via launchd; source=plugin-change; reason=plugin source changed; pid=12345; age=2s; expiresIn=57s", + ); + }); }); diff --git a/src/infra/restart-handoff.ts b/src/infra/restart-handoff.ts index 7d4ea850e81..90c7748af04 100644 --- a/src/infra/restart-handoff.ts +++ b/src/infra/restart-handoff.ts @@ -39,6 +39,35 @@ export type GatewayRestartHandoff = { supervisorMode: GatewayRestartHandoffSupervisorMode; }; +function formatShortDuration(ms: number): string { + const clamped = Math.max(0, Math.floor(ms)); + if (clamped < 1000) { + return `${clamped}ms`; + } + const seconds = Math.floor(clamped / 1000); + if (seconds < 60) { + return `${seconds}s`; + } + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return remainingSeconds === 0 ? `${minutes}m` : `${minutes}m ${remainingSeconds}s`; +} + +export function formatGatewayRestartHandoffDiagnostic( + handoff: GatewayRestartHandoff, + now = Date.now(), +): string { + const detail = [ + `${handoff.restartKind} via ${handoff.supervisorMode}`, + `source=${handoff.source}`, + handoff.reason ? `reason=${handoff.reason}` : undefined, + `pid=${handoff.pid}`, + `age=${formatShortDuration(now - handoff.createdAt)}`, + `expiresIn=${formatShortDuration(handoff.expiresAt - now)}`, + ].filter((value): value is string => Boolean(value)); + return `Recent restart handoff: ${detail.join("; ")}`; +} + function resolveGatewayRestartHandoffPath(env: NodeJS.ProcessEnv = process.env): string { return path.join(resolveStateDir(env), GATEWAY_SUPERVISOR_RESTART_HANDOFF_FILENAME); }