relay: add control channel and heartbeat stream
This commit is contained in:
@@ -22,7 +22,7 @@ type HealthConnect = {
|
||||
elapsedMs: number;
|
||||
};
|
||||
|
||||
type HealthSummary = {
|
||||
export type HealthSummary = {
|
||||
ts: number;
|
||||
durationMs: number;
|
||||
web: {
|
||||
@@ -77,10 +77,9 @@ async function probeWebConnect(timeoutMs: number): Promise<HealthConnect> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function healthCommand(
|
||||
opts: { json?: boolean; timeoutMs?: number },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function getHealthSnapshot(
|
||||
timeoutMs?: number,
|
||||
): Promise<HealthSummary> {
|
||||
const cfg = loadConfig();
|
||||
const linked = await webAuthExists();
|
||||
const authAgeMs = getWebAuthAgeMs();
|
||||
@@ -101,8 +100,8 @@ export async function healthCommand(
|
||||
const ipcExists = Boolean(ipcPath) && fs.existsSync(ipcPath);
|
||||
|
||||
const start = Date.now();
|
||||
const timeoutMs = Math.max(1000, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
const connect = linked ? await probeWebConnect(timeoutMs) : undefined;
|
||||
const cappedTimeout = Math.max(1000, timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
||||
const connect = linked ? await probeWebConnect(cappedTimeout) : undefined;
|
||||
|
||||
const summary: HealthSummary = {
|
||||
ts: Date.now(),
|
||||
@@ -117,39 +116,55 @@ export async function healthCommand(
|
||||
ipc: { path: ipcPath, exists: ipcExists },
|
||||
};
|
||||
|
||||
const fatal = !linked || (connect && !connect.ok);
|
||||
return summary;
|
||||
}
|
||||
|
||||
export async function healthCommand(
|
||||
opts: { json?: boolean; timeoutMs?: number },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const summary = await getHealthSnapshot(opts.timeoutMs);
|
||||
const fatal =
|
||||
!summary.web.linked || (summary.web.connect && !summary.web.connect.ok);
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(JSON.stringify(summary, null, 2));
|
||||
} else {
|
||||
runtime.log(
|
||||
linked
|
||||
? `Web: linked (auth age ${authAgeMs ? `${Math.round(authAgeMs / 60000)}m` : "unknown"})`
|
||||
summary.web.linked
|
||||
? `Web: linked (auth age ${summary.web.authAgeMs ? `${Math.round(summary.web.authAgeMs / 60000)}m` : "unknown"})`
|
||||
: "Web: not linked (run clawdis login)",
|
||||
);
|
||||
if (linked) {
|
||||
if (summary.web.linked) {
|
||||
logWebSelfId(runtime, true);
|
||||
}
|
||||
if (connect) {
|
||||
const base = connect.ok
|
||||
? info(`Connect: ok (${connect.elapsedMs}ms)`)
|
||||
: `Connect: failed (${connect.status ?? "unknown"})`;
|
||||
runtime.log(base + (connect.error ? ` - ${connect.error}` : ""));
|
||||
if (summary.web.connect) {
|
||||
const base = summary.web.connect.ok
|
||||
? info(`Connect: ok (${summary.web.connect.elapsedMs}ms)`)
|
||||
: `Connect: failed (${summary.web.connect.status ?? "unknown"})`;
|
||||
runtime.log(
|
||||
base +
|
||||
(summary.web.connect.error ? ` - ${summary.web.connect.error}` : ""),
|
||||
);
|
||||
}
|
||||
runtime.log(info(`Heartbeat interval: ${heartbeatSeconds}s`));
|
||||
runtime.log(info(`Heartbeat interval: ${summary.heartbeatSeconds}s`));
|
||||
runtime.log(
|
||||
info(`Session store: ${storePath} (${sessions.length} entries)`),
|
||||
info(
|
||||
`Session store: ${summary.sessions.path} (${summary.sessions.count} entries)`,
|
||||
),
|
||||
);
|
||||
if (recent.length > 0) {
|
||||
if (summary.sessions.recent.length > 0) {
|
||||
runtime.log("Recent sessions:");
|
||||
for (const r of recent) {
|
||||
for (const r of summary.sessions.recent) {
|
||||
runtime.log(
|
||||
`- ${r.key} (${r.updatedAt ? `${Math.round((Date.now() - r.updatedAt) / 60000)}m ago` : "no activity"})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
runtime.log(
|
||||
info(`IPC socket: ${ipcExists ? "present" : "missing"} (${ipcPath})`),
|
||||
info(
|
||||
`IPC socket: ${summary.ipc.exists ? "present" : "missing"} (${summary.ipc.path})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,21 +9,21 @@ import {
|
||||
webAuthExists,
|
||||
} from "../web/session.js";
|
||||
|
||||
const formatAge = (ms: number | null | undefined) => {
|
||||
if (!ms || ms < 0) return "unknown";
|
||||
const minutes = Math.round(ms / 60_000);
|
||||
if (minutes < 1) return "just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) return `${hours}h ago`;
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
export type StatusSummary = {
|
||||
web: { linked: boolean; authAgeMs: number | null };
|
||||
heartbeatSeconds: number;
|
||||
sessions: {
|
||||
path: string;
|
||||
count: number;
|
||||
recent: Array<{
|
||||
key: string;
|
||||
updatedAt: number | null;
|
||||
age: number | null;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export async function statusCommand(
|
||||
opts: { json?: boolean },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
export async function getStatusSummary(): Promise<StatusSummary> {
|
||||
const cfg = loadConfig();
|
||||
const linked = await webAuthExists();
|
||||
const authAgeMs = getWebAuthAgeMs();
|
||||
@@ -41,18 +41,33 @@ export async function statusCommand(
|
||||
age: s.updatedAt ? Date.now() - s.updatedAt : null,
|
||||
}));
|
||||
|
||||
const summary = {
|
||||
web: {
|
||||
linked,
|
||||
authAgeMs,
|
||||
},
|
||||
return {
|
||||
web: { linked, authAgeMs },
|
||||
heartbeatSeconds,
|
||||
sessions: {
|
||||
path: storePath,
|
||||
count: sessions.length,
|
||||
recent,
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
}
|
||||
|
||||
const formatAge = (ms: number | null | undefined) => {
|
||||
if (!ms || ms < 0) return "unknown";
|
||||
const minutes = Math.round(ms / 60_000);
|
||||
if (minutes < 1) return "just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) return `${hours}h ago`;
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
};
|
||||
|
||||
export async function statusCommand(
|
||||
opts: { json?: boolean },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const summary = await getStatusSummary();
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(JSON.stringify(summary, null, 2));
|
||||
@@ -60,17 +75,17 @@ export async function statusCommand(
|
||||
}
|
||||
|
||||
runtime.log(
|
||||
`Web session: ${linked ? "linked" : "not linked"}${linked ? ` (last refreshed ${formatAge(authAgeMs)})` : ""}`,
|
||||
`Web session: ${summary.web.linked ? "linked" : "not linked"}${summary.web.linked ? ` (last refreshed ${formatAge(summary.web.authAgeMs)})` : ""}`,
|
||||
);
|
||||
if (linked) {
|
||||
if (summary.web.linked) {
|
||||
logWebSelfId(runtime, true);
|
||||
}
|
||||
runtime.log(info(`Heartbeat: ${heartbeatSeconds}s`));
|
||||
runtime.log(info(`Session store: ${storePath}`));
|
||||
runtime.log(info(`Active sessions: ${sessions.length}`));
|
||||
if (recent.length > 0) {
|
||||
runtime.log(info(`Heartbeat: ${summary.heartbeatSeconds}s`));
|
||||
runtime.log(info(`Session store: ${summary.sessions.path}`));
|
||||
runtime.log(info(`Active sessions: ${summary.sessions.count}`));
|
||||
if (summary.sessions.recent.length > 0) {
|
||||
runtime.log("Recent sessions:");
|
||||
for (const r of recent) {
|
||||
for (const r of summary.sessions.recent) {
|
||||
runtime.log(
|
||||
`- ${r.key} (${r.updatedAt ? formatAge(Date.now() - r.updatedAt) : "no activity"})`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user