diff --git a/src/infra/restart-handoff.test.ts b/src/infra/restart-handoff.test.ts index cbcdad97e20..ad138720ef9 100644 --- a/src/infra/restart-handoff.test.ts +++ b/src/infra/restart-handoff.test.ts @@ -271,4 +271,26 @@ describe("gateway restart handoff", () => { "Recent restart handoff: full-process via launchd; source=plugin-change; reason=plugin source changed; pid=12345; age=2s; expiresIn=57s", ); }); + + it("formats restart reasons as a single diagnostic line", () => { + expect( + formatGatewayRestartHandoffDiagnostic( + { + kind: GATEWAY_SUPERVISOR_RESTART_HANDOFF_KIND, + version: 1, + intentId: "intent-1", + pid: 12_345, + createdAt: 10_000, + expiresAt: 70_000, + reason: "ok\nFake: bad", + source: "operator-restart", + restartKind: "full-process", + supervisorMode: "external", + }, + 12_500, + ), + ).toBe( + "Recent restart handoff: full-process via external; source=operator-restart; reason=ok Fake: bad; pid=12345; age=2s; expiresIn=57s", + ); + }); }); diff --git a/src/infra/restart-handoff.ts b/src/infra/restart-handoff.ts index 90c7748af04..c3f5d228b3e 100644 --- a/src/infra/restart-handoff.ts +++ b/src/infra/restart-handoff.ts @@ -53,14 +53,22 @@ function formatShortDuration(ms: number): string { return remainingSeconds === 0 ? `${minutes}m` : `${minutes}m ${remainingSeconds}s`; } +function formatDiagnosticValue(value: string): string { + return value + .replace(/[\u0000-\u001f\u007f]+/gu, " ") + .replace(/\s+/gu, " ") + .trim(); +} + export function formatGatewayRestartHandoffDiagnostic( handoff: GatewayRestartHandoff, now = Date.now(), ): string { + const reason = handoff.reason ? formatDiagnosticValue(handoff.reason) : undefined; const detail = [ `${handoff.restartKind} via ${handoff.supervisorMode}`, `source=${handoff.source}`, - handoff.reason ? `reason=${handoff.reason}` : undefined, + reason ? `reason=${reason}` : undefined, `pid=${handoff.pid}`, `age=${formatShortDuration(now - handoff.createdAt)}`, `expiresIn=${formatShortDuration(handoff.expiresAt - now)}`,