Files
clawdbot/src/infra/restart-sentinel.ts
2026-01-10 23:14:55 +01:00

121 lines
3.2 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
export type RestartSentinelLog = {
stdoutTail?: string | null;
stderrTail?: string | null;
exitCode?: number | null;
};
export type RestartSentinelStep = {
name: string;
command: string;
cwd?: string | null;
durationMs?: number | null;
log?: RestartSentinelLog | null;
};
export type RestartSentinelStats = {
mode?: string;
root?: string;
before?: Record<string, unknown> | null;
after?: Record<string, unknown> | null;
steps?: RestartSentinelStep[];
reason?: string | null;
durationMs?: number | null;
};
export type RestartSentinelPayload = {
kind: "config-apply" | "update" | "restart";
status: "ok" | "error" | "skipped";
ts: number;
sessionKey?: string;
message?: string | null;
doctorHint?: string | null;
stats?: RestartSentinelStats | null;
};
export type RestartSentinel = {
version: 1;
payload: RestartSentinelPayload;
};
const SENTINEL_FILENAME = "restart-sentinel.json";
export const DOCTOR_NONINTERACTIVE_HINT =
"Run: clawdbot doctor --non-interactive";
export function resolveRestartSentinelPath(
env: NodeJS.ProcessEnv = process.env,
): string {
return path.join(resolveStateDir(env), SENTINEL_FILENAME);
}
export async function writeRestartSentinel(
payload: RestartSentinelPayload,
env: NodeJS.ProcessEnv = process.env,
) {
const filePath = resolveRestartSentinelPath(env);
await fs.mkdir(path.dirname(filePath), { recursive: true });
const data: RestartSentinel = { version: 1, payload };
await fs.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
return filePath;
}
export async function readRestartSentinel(
env: NodeJS.ProcessEnv = process.env,
): Promise<RestartSentinel | null> {
const filePath = resolveRestartSentinelPath(env);
try {
const raw = await fs.readFile(filePath, "utf-8");
let parsed: RestartSentinel | undefined;
try {
parsed = JSON.parse(raw) as RestartSentinel | undefined;
} catch {
await fs.unlink(filePath).catch(() => {});
return null;
}
if (!parsed || parsed.version !== 1 || !parsed.payload) {
await fs.unlink(filePath).catch(() => {});
return null;
}
return parsed;
} catch {
return null;
}
}
export async function consumeRestartSentinel(
env: NodeJS.ProcessEnv = process.env,
): Promise<RestartSentinel | null> {
const filePath = resolveRestartSentinelPath(env);
const parsed = await readRestartSentinel(env);
if (!parsed) return null;
await fs.unlink(filePath).catch(() => {});
return parsed;
}
export function formatRestartSentinelMessage(
payload: RestartSentinelPayload,
): string {
return `GatewayRestart:\n${JSON.stringify(payload, null, 2)}`;
}
export function summarizeRestartSentinel(
payload: RestartSentinelPayload,
): string {
const kind = payload.kind;
const status = payload.status;
const mode = payload.stats?.mode ? ` (${payload.stats.mode})` : "";
return `Gateway restart ${kind} ${status}${mode}`.trim();
}
export function trimLogTail(input?: string | null, maxChars = 8000) {
if (!input) return null;
const text = input.trimEnd();
if (text.length <= maxChars) return text;
return `${text.slice(text.length - maxChars)}`;
}