Files
clawdbot/src/gateway/server-reload-handlers.ts
2026-01-17 04:15:46 +00:00

156 lines
5.5 KiB
TypeScript

import type { CliDeps } from "../cli/deps.js";
import type { loadConfig } from "../config/config.js";
import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
import { resetDirectoryCache } from "../infra/outbound/target-resolver.js";
import { setCommandLaneConcurrency } from "../process/command-queue.js";
import type { ChannelKind, GatewayReloadPlan } from "./config-reload.js";
import { resolveHooksConfig } from "./hooks.js";
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
import { buildGatewayCronService, type GatewayCronState } from "./server-cron.js";
type GatewayHotReloadState = {
hooksConfig: ReturnType<typeof resolveHooksConfig>;
heartbeatRunner: { stop: () => void };
cronState: GatewayCronState;
browserControl: Awaited<ReturnType<typeof startBrowserControlServerIfEnabled>> | null;
};
export function createGatewayReloadHandlers(params: {
deps: CliDeps;
broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void;
getState: () => GatewayHotReloadState;
setState: (state: GatewayHotReloadState) => void;
startChannel: (name: ChannelKind) => Promise<void>;
stopChannel: (name: ChannelKind) => Promise<void>;
logHooks: {
info: (msg: string) => void;
warn: (msg: string) => void;
error: (msg: string) => void;
};
logBrowser: { error: (msg: string) => void };
logChannels: { info: (msg: string) => void; error: (msg: string) => void };
logCron: { error: (msg: string) => void };
logReload: { info: (msg: string) => void; warn: (msg: string) => void };
}) {
const applyHotReload = async (
plan: GatewayReloadPlan,
nextConfig: ReturnType<typeof loadConfig>,
) => {
const state = params.getState();
const nextState = { ...state };
if (plan.reloadHooks) {
try {
nextState.hooksConfig = resolveHooksConfig(nextConfig);
} catch (err) {
params.logHooks.warn(`hooks config reload failed: ${String(err)}`);
}
}
if (plan.restartHeartbeat) {
state.heartbeatRunner.stop();
nextState.heartbeatRunner = startHeartbeatRunner({ cfg: nextConfig });
}
resetDirectoryCache();
if (plan.restartCron) {
state.cronState.cron.stop();
nextState.cronState = buildGatewayCronService({
cfg: nextConfig,
deps: params.deps,
broadcast: params.broadcast,
});
void nextState.cronState.cron
.start()
.catch((err) => params.logCron.error(`failed to start: ${String(err)}`));
}
if (plan.restartBrowserControl) {
if (state.browserControl) {
await state.browserControl.stop().catch(() => {});
}
try {
nextState.browserControl = await startBrowserControlServerIfEnabled();
} catch (err) {
params.logBrowser.error(`server failed to start: ${String(err)}`);
}
}
if (plan.restartGmailWatcher) {
await stopGmailWatcher().catch(() => {});
if (process.env.CLAWDBOT_SKIP_GMAIL_WATCHER !== "1") {
try {
const gmailResult = await startGmailWatcher(nextConfig);
if (gmailResult.started) {
params.logHooks.info("gmail watcher started");
} else if (
gmailResult.reason &&
gmailResult.reason !== "hooks not enabled" &&
gmailResult.reason !== "no gmail account configured"
) {
params.logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`);
}
} catch (err) {
params.logHooks.error(`gmail watcher failed to start: ${String(err)}`);
}
} else {
params.logHooks.info("skipping gmail watcher restart (CLAWDBOT_SKIP_GMAIL_WATCHER=1)");
}
}
if (plan.restartChannels.size > 0) {
if (
process.env.CLAWDBOT_SKIP_CHANNELS === "1" ||
process.env.CLAWDBOT_SKIP_PROVIDERS === "1"
) {
params.logChannels.info(
"skipping channel reload (CLAWDBOT_SKIP_CHANNELS=1 or CLAWDBOT_SKIP_PROVIDERS=1)",
);
} else {
const restartChannel = async (name: ChannelKind) => {
params.logChannels.info(`restarting ${name} channel`);
await params.stopChannel(name);
await params.startChannel(name);
};
for (const channel of plan.restartChannels) {
await restartChannel(channel);
}
}
}
setCommandLaneConcurrency("cron", nextConfig.cron?.maxConcurrentRuns ?? 1);
setCommandLaneConcurrency("main", nextConfig.agents?.defaults?.maxConcurrent ?? 1);
setCommandLaneConcurrency(
"subagent",
nextConfig.agents?.defaults?.subagents?.maxConcurrent ?? 1,
);
if (plan.hotReasons.length > 0) {
params.logReload.info(`config hot reload applied (${plan.hotReasons.join(", ")})`);
} else if (plan.noopPaths.length > 0) {
params.logReload.info(`config change applied (dynamic reads: ${plan.noopPaths.join(", ")})`);
}
params.setState(nextState);
};
const requestGatewayRestart = (
plan: GatewayReloadPlan,
_nextConfig: ReturnType<typeof loadConfig>,
) => {
const reasons = plan.restartReasons.length
? plan.restartReasons.join(", ")
: plan.changedPaths.join(", ");
params.logReload.warn(`config change requires gateway restart (${reasons})`);
if (process.listenerCount("SIGUSR1") === 0) {
params.logReload.warn("no SIGUSR1 listener found; restart skipped");
return;
}
process.emit("SIGUSR1");
};
return { applyHotReload, requestGatewayRestart };
}