Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import fs from "node:fs/promises";
import path from "node:path";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveStateDir } from "../config/paths.js";
import { writeJsonAtomic } from "./json-files.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 RestartSentinelContinuation =
| {
kind: "systemEvent";
text: string;
}
| {
kind: "agentTurn";
message: string;
};
export type RestartSentinelPayload = {
kind: "config-apply" | "config-auto-recovery" | "config-patch" | "update" | "restart";
status: "ok" | "error" | "skipped";
ts: number;
sessionKey?: string;
/** Delivery context captured at restart time to ensure channel routing survives restart. */
deliveryContext?: {
channel?: string;
to?: string;
accountId?: string;
};
/** Thread ID for reply threading (e.g., Slack thread_ts). */
threadId?: string;
message?: string | null;
continuation?: RestartSentinelContinuation | null;
doctorHint?: string | null;
stats?: RestartSentinelStats | null;
};
export type RestartSentinel = {
version: 1;
payload: RestartSentinelPayload;
};
export const DEFAULT_RESTART_SUCCESS_CONTINUATION_MESSAGE =
"The gateway restart completed successfully. Tell the user OpenClaw restarted successfully and continue any pending work.";
const SENTINEL_FILENAME = "restart-sentinel.json";
export function formatDoctorNonInteractiveHint(
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
): string {
return `Run: ${formatCliCommand("openclaw doctor --non-interactive", env)}`;
}
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);
const data: RestartSentinel = { version: 1, payload };
await writeJsonAtomic(filePath, data, { trailingNewline: true, ensureDirMode: 0o700 });
return filePath;
}
export async function removeRestartSentinelFile(filePath: string | null | undefined) {
if (!filePath) {
return;
}
await fs.unlink(filePath).catch(() => {});
}
export function buildRestartSuccessContinuation(params: {
sessionKey?: string;
continuationMessage?: string | null;
}): RestartSentinelContinuation | null {
const message = params.continuationMessage?.trim();
if (message) {
return { kind: "agentTurn", message };
}
return params.sessionKey?.trim()
? { kind: "agentTurn", message: DEFAULT_RESTART_SUCCESS_CONTINUATION_MESSAGE }
: null;
}
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 hasRestartSentinel(env: NodeJS.ProcessEnv = process.env): Promise<boolean> {
try {
await fs.access(resolveRestartSentinelPath(env));
return true;
} catch {
return false;
}
}
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 removeRestartSentinelFile(filePath);
return parsed;
}
export function formatRestartSentinelMessage(payload: RestartSentinelPayload): string {
const message = payload.message?.trim();
if (message && (!payload.stats || payload.kind === "config-auto-recovery")) {
return message;
}
const lines: string[] = [summarizeRestartSentinel(payload)];
if (message) {
lines.push(message);
}
const reason = payload.stats?.reason?.trim();
if (reason && reason !== message) {
lines.push(`Reason: ${reason}`);
}
if (payload.doctorHint?.trim()) {
lines.push(payload.doctorHint.trim());
}
return lines.join("\n");
}
export function summarizeRestartSentinel(payload: RestartSentinelPayload): string {
if (payload.kind === "config-auto-recovery") {
return "Gateway auto-recovery";
}
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)}`;
}
¤ Dauer der Verarbeitung: 0.28 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland