121 lines
3.2 KiB
TypeScript
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)}`;
|
|
}
|