fix: add gateway stop/restart commands

This commit is contained in:
Peter Steinberger
2026-01-06 03:25:21 +01:00
parent cc0ef4d012
commit 0398f684e7
18 changed files with 339 additions and 28 deletions

View File

@@ -13,6 +13,7 @@
- Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step. - Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step.
- Onboarding: when OpenAI Codex OAuth is used, default to `openai-codex/gpt-5.2` and warn if the selected model lacks auth. - Onboarding: when OpenAI Codex OAuth is used, default to `openai-codex/gpt-5.2` and warn if the selected model lacks auth.
- CLI: auto-migrate legacy config entries on command start (same behavior as gateway startup). - CLI: auto-migrate legacy config entries on command start (same behavior as gateway startup).
- Gateway: add `gateway stop|restart` helpers and surface launchd/systemd/schtasks stop hints when the gateway is already running.
- Auth: prioritize OAuth profiles but fall back to API keys when refresh fails; stored profiles now load without explicit auth order. - Auth: prioritize OAuth profiles but fall back to API keys when refresh fails; stored profiles now load without explicit auth order.
- Control UI: harden config Form view with schema normalization, map editing, and guardrails to prevent data loss on save. - Control UI: harden config Form view with schema normalization, map editing, and guardrails to prevent data loss on save.
- Cron: normalize cron.add/update inputs, align channel enums/status fields across gateway/CLI/UI/macOS, and add protocol conformance tests. Thanks @mneves75 for PR #256. - Cron: normalize cron.add/update inputs, align channel enums/status fields across gateway/CLI/UI/macOS, and add protocol conformance tests. Thanks @mneves75 for PR #256.

View File

@@ -492,6 +492,9 @@ The gateway runs under a supervisor that auto-restarts it. You need to stop the
# Check if running # Check if running
launchctl list | grep clawdbot launchctl list | grep clawdbot
# Stop (disable does NOT stop a running job)
clawdbot gateway stop
# Stop and disable # Stop and disable
launchctl disable gui/$UID/com.clawdbot.gateway launchctl disable gui/$UID/com.clawdbot.gateway
launchctl bootout gui/$UID/com.clawdbot.gateway launchctl bootout gui/$UID/com.clawdbot.gateway
@@ -499,6 +502,9 @@ launchctl bootout gui/$UID/com.clawdbot.gateway
# Re-enable later # Re-enable later
launchctl enable gui/$UID/com.clawdbot.gateway launchctl enable gui/$UID/com.clawdbot.gateway
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.clawdbot.gateway.plist launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.clawdbot.gateway.plist
# Or just restart
clawdbot gateway restart
``` ```
**Linux (systemd)** **Linux (systemd)**
@@ -508,7 +514,11 @@ launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.clawdbot.gateway.plist
systemctl list-units | grep -i clawdbot systemctl list-units | grep -i clawdbot
# Stop and disable # Stop and disable
sudo systemctl disable --now clawdbot clawdbot gateway stop
systemctl --user disable --now clawdbot-gateway.service
# Or just restart
clawdbot gateway restart
``` ```
**pm2 (if used)** **pm2 (if used)**

View File

@@ -159,6 +159,8 @@ See also: `docs/presence.md` for how presence is produced/deduped and why `insta
Bundled mac app: Bundled mac app:
- Clawdbot.app can bundle a bun-compiled gateway binary and install a per-user LaunchAgent labeled `com.clawdbot.gateway`. - Clawdbot.app can bundle a bun-compiled gateway binary and install a per-user LaunchAgent labeled `com.clawdbot.gateway`.
- To stop it cleanly, use `clawdbot gateway stop` (or `launchctl bootout gui/$UID/com.clawdbot.gateway`).
- To restart, use `clawdbot gateway restart` (or `launchctl kickstart -k gui/$UID/com.clawdbot.gateway`).
## Supervision (systemd user unit) ## Supervision (systemd user unit)
Create `~/.config/systemd/user/clawdbot-gateway.service`: Create `~/.config/systemd/user/clawdbot-gateway.service`:
@@ -217,6 +219,7 @@ sudo systemctl enable --now clawdbot-gateway.service
- `clawdbot gateway send --to <num> --message "hi" [--media-url ...]` — send via Gateway (idempotent). - `clawdbot gateway send --to <num> --message "hi" [--media-url ...]` — send via Gateway (idempotent).
- `clawdbot gateway agent --message "hi" [--to ...]` — run an agent turn (waits for final by default). - `clawdbot gateway agent --message "hi" [--to ...]` — run an agent turn (waits for final by default).
- `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging. - `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
- `clawdbot gateway stop|restart` — stop/restart the supervised gateway service (launchd/systemd/schtasks).
- Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one. - Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one.
## Migration guidance ## Migration guidance

View File

@@ -160,6 +160,13 @@ lsof -nP -i :18789
kill -9 <PID> kill -9 <PID>
``` ```
If the gateway is supervised by launchd, killing the PID will just respawn it.
Stop the supervisor instead:
```bash
clawdbot gateway stop
# Or: launchctl bootout gui/$UID/com.clawdbot.gateway
```
**Fix 2: Check embedded gateway** **Fix 2: Check embedded gateway**
Ensure the gateway relay was properly bundled. Run `./scripts/package-mac-app.sh` and ensure `bun` is installed. Ensure the gateway relay was properly bundled. Run `./scripts/package-mac-app.sh` and ensure `bun` is installed.

View File

@@ -1,12 +1,11 @@
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
import { CronAddParamsSchema } from "../../gateway/protocol/schema.js";
import { import {
normalizeCronJobCreate, normalizeCronJobCreate,
normalizeCronJobPatch, normalizeCronJobPatch,
} from "../../cron/normalize.js"; } from "../../cron/normalize.js";
import { CronAddParamsSchema } from "../../gateway/protocol/schema.js";
import { type AnyAgentTool, jsonResult, readStringParam } from "./common.js";
import { callGatewayTool, type GatewayCallOptions } from "./gateway.js";
const CronJobPatchSchema = Type.Partial(CronAddParamsSchema); const CronJobPatchSchema = Type.Partial(CronAddParamsSchema);

View File

@@ -4,7 +4,6 @@ import { defaultRuntime } from "../runtime.js";
import type { GatewayRpcOpts } from "./gateway-rpc.js"; import type { GatewayRpcOpts } from "./gateway-rpc.js";
import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js"; import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js";
async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) { async function warnIfCronSchedulerDisabled(opts: GatewayRpcOpts) {
try { try {
const res = (await callGatewayFromCli("cron.status", opts, {})) as { const res = (await callGatewayFromCli("cron.status", opts, {})) as {

View File

@@ -13,6 +13,9 @@ const forceFreePortAndWait = vi.fn(async () => ({
waitedMs: 0, waitedMs: 0,
escalatedToSigkill: false, escalatedToSigkill: false,
})); }));
const serviceStop = vi.fn().mockResolvedValue(undefined);
const serviceRestart = vi.fn().mockResolvedValue(undefined);
const serviceIsLoaded = vi.fn().mockResolvedValue(true);
const runtimeLogs: string[] = []; const runtimeLogs: string[] = [];
const runtimeErrors: string[] = []; const runtimeErrors: string[] = [];
@@ -74,6 +77,20 @@ vi.mock("./ports.js", () => ({
forceFreePortAndWait: (port: number) => forceFreePortAndWait(port), forceFreePortAndWait: (port: number) => forceFreePortAndWait(port),
})); }));
vi.mock("../daemon/service.js", () => ({
resolveGatewayService: () => ({
label: "LaunchAgent",
loadedText: "loaded",
notLoadedText: "not loaded",
install: vi.fn(),
uninstall: vi.fn(),
stop: serviceStop,
restart: serviceRestart,
isLoaded: serviceIsLoaded,
readCommand: vi.fn(),
}),
}));
describe("gateway-cli coverage", () => { describe("gateway-cli coverage", () => {
it("registers call/health/status/send/agent commands and routes to callGateway", async () => { it("registers call/health/status/send/agent commands and routes to callGateway", async () => {
runtimeLogs.length = 0; runtimeLogs.length = 0;
@@ -228,6 +245,51 @@ describe("gateway-cli coverage", () => {
} }
}); });
it("supports gateway stop/restart via service helper", async () => {
runtimeLogs.length = 0;
runtimeErrors.length = 0;
serviceStop.mockClear();
serviceRestart.mockClear();
serviceIsLoaded.mockResolvedValue(true);
const { registerGatewayCli } = await import("./gateway-cli.js");
const program = new Command();
program.exitOverride();
registerGatewayCli(program);
await program.parseAsync(["gateway", "stop"], { from: "user" });
await program.parseAsync(["gateway", "restart"], { from: "user" });
expect(serviceStop).toHaveBeenCalledTimes(1);
expect(serviceRestart).toHaveBeenCalledTimes(1);
});
it("prints stop hints on GatewayLockError when service is loaded", async () => {
runtimeLogs.length = 0;
runtimeErrors.length = 0;
serviceIsLoaded.mockResolvedValue(true);
const { GatewayLockError } = await import("../infra/gateway-lock.js");
startGatewayServer.mockRejectedValueOnce(
new GatewayLockError("another gateway instance is already listening"),
);
const { registerGatewayCli } = await import("./gateway-cli.js");
const program = new Command();
program.exitOverride();
registerGatewayCli(program);
await expect(
program.parseAsync(["gateway", "--allow-unconfigured"], {
from: "user",
}),
).rejects.toThrow("__exit__:1");
expect(startGatewayServer).toHaveBeenCalled();
expect(runtimeErrors.join("\n")).toContain("Gateway failed to start:");
expect(runtimeErrors.join("\n")).toContain("clawdbot gateway stop");
});
it("uses env/config port when --port is omitted", async () => { it("uses env/config port when --port is omitted", async () => {
await withEnvOverride({ CLAWDBOT_GATEWAY_PORT: "19001" }, async () => { await withEnvOverride({ CLAWDBOT_GATEWAY_PORT: "19001" }, async () => {
runtimeLogs.length = 0; runtimeLogs.length = 0;

View File

@@ -6,6 +6,12 @@ import {
loadConfig, loadConfig,
resolveGatewayPort, resolveGatewayPort,
} from "../config/config.js"; } from "../config/config.js";
import {
GATEWAY_LAUNCH_AGENT_LABEL,
GATEWAY_SYSTEMD_SERVICE_NAME,
GATEWAY_WINDOWS_TASK_NAME,
} from "../daemon/constants.js";
import { resolveGatewayService } from "../daemon/service.js";
import { callGateway, randomIdempotencyKey } from "../gateway/call.js"; import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
import { startGatewayServer } from "../gateway/server.js"; import { startGatewayServer } from "../gateway/server.js";
import { import {
@@ -45,6 +51,62 @@ function parsePort(raw: unknown): number | null {
return parsed; return parsed;
} }
function renderGatewayServiceStopHints(): string[] {
switch (process.platform) {
case "darwin":
return [
"Tip: clawdbot gateway stop",
`Or: launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}`,
];
case "linux":
return [
"Tip: clawdbot gateway stop",
`Or: systemctl --user stop ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`,
];
case "win32":
return [
"Tip: clawdbot gateway stop",
`Or: schtasks /End /TN "${GATEWAY_WINDOWS_TASK_NAME}"`,
];
default:
return ["Tip: clawdbot gateway stop"];
}
}
function renderGatewayServiceStartHints(): string[] {
switch (process.platform) {
case "darwin":
return [
`launchctl bootstrap gui/$UID ~/Library/LaunchAgents/${GATEWAY_LAUNCH_AGENT_LABEL}.plist`,
];
case "linux":
return [`systemctl --user start ${GATEWAY_SYSTEMD_SERVICE_NAME}.service`];
case "win32":
return [`schtasks /Run /TN "${GATEWAY_WINDOWS_TASK_NAME}"`];
default:
return [];
}
}
async function maybeExplainGatewayServiceStop() {
const service = resolveGatewayService();
let loaded: boolean | null = null;
try {
loaded = await service.isLoaded({ env: process.env });
} catch {
loaded = null;
}
if (loaded === false) return;
defaultRuntime.error(
loaded
? `Gateway service appears ${service.loadedText}. Stop it first.`
: "Gateway service status unknown; if supervised, stop it first.",
);
for (const hint of renderGatewayServiceStopHints()) {
defaultRuntime.error(hint);
}
}
async function runGatewayLoop(params: { async function runGatewayLoop(params: {
start: () => Promise<Awaited<ReturnType<typeof startGatewayServer>>>; start: () => Promise<Awaited<ReturnType<typeof startGatewayServer>>>;
runtime: typeof defaultRuntime; runtime: typeof defaultRuntime;
@@ -285,8 +347,22 @@ export function registerGatewayCli(program: Command) {
}), }),
}); });
} catch (err) { } catch (err) {
if (err instanceof GatewayLockError) { if (
defaultRuntime.error(`Gateway failed to start: ${err.message}`); err instanceof GatewayLockError ||
(err &&
typeof err === "object" &&
(err as { name?: string }).name === "GatewayLockError")
) {
const errMessage =
err instanceof Error
? err.message
: typeof err === "object" && err !== null && "message" in err
? String((err as { message?: unknown }).message ?? "")
: String(err);
defaultRuntime.error(
`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot gateway stop`,
);
await maybeExplainGatewayServiceStop();
defaultRuntime.exit(1); defaultRuntime.exit(1);
return; return;
} }
@@ -486,8 +562,22 @@ export function registerGatewayCli(program: Command) {
}), }),
}); });
} catch (err) { } catch (err) {
if (err instanceof GatewayLockError) { if (
defaultRuntime.error(`Gateway failed to start: ${err.message}`); err instanceof GatewayLockError ||
(err &&
typeof err === "object" &&
(err as { name?: string }).name === "GatewayLockError")
) {
const errMessage =
err instanceof Error
? err.message
: typeof err === "object" && err !== null && "message" in err
? String((err as { message?: unknown }).message ?? "")
: String(err);
defaultRuntime.error(
`Gateway failed to start: ${errMessage}\nIf the gateway is supervised, stop it with: clawdbot gateway stop`,
);
await maybeExplainGatewayServiceStop();
defaultRuntime.exit(1); defaultRuntime.exit(1);
return; return;
} }
@@ -635,6 +725,59 @@ export function registerGatewayCli(program: Command) {
}), }),
); );
gateway
.command("stop")
.description("Stop the Gateway service (launchd/systemd/schtasks)")
.action(async () => {
const service = resolveGatewayService();
let loaded = false;
try {
loaded = await service.isLoaded({ env: process.env });
} catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1);
return;
}
if (!loaded) {
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
return;
}
try {
await service.stop({ stdout: process.stdout });
} catch (err) {
defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
defaultRuntime.exit(1);
}
});
gateway
.command("restart")
.description("Restart the Gateway service (launchd/systemd/schtasks)")
.action(async () => {
const service = resolveGatewayService();
let loaded = false;
try {
loaded = await service.isLoaded({ env: process.env });
} catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1);
return;
}
if (!loaded) {
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
for (const hint of renderGatewayServiceStartHints()) {
defaultRuntime.log(`Start with: ${hint}`);
}
return;
}
try {
await service.restart({ stdout: process.stdout });
} catch (err) {
defaultRuntime.error(`Gateway restart failed: ${String(err)}`);
defaultRuntime.exit(1);
}
});
// Build default deps (keeps parity with other commands; future-proofing). // Build default deps (keeps parity with other commands; future-proofing).
void createDefaultDeps(); void createDefaultDeps();
} }

View File

@@ -38,6 +38,7 @@ const resolveGatewayProgramArguments = vi.fn().mockResolvedValue({
}); });
const serviceInstall = vi.fn().mockResolvedValue(undefined); const serviceInstall = vi.fn().mockResolvedValue(undefined);
const serviceIsLoaded = vi.fn().mockResolvedValue(false); const serviceIsLoaded = vi.fn().mockResolvedValue(false);
const serviceStop = vi.fn().mockResolvedValue(undefined);
const serviceRestart = vi.fn().mockResolvedValue(undefined); const serviceRestart = vi.fn().mockResolvedValue(undefined);
const serviceUninstall = vi.fn().mockResolvedValue(undefined); const serviceUninstall = vi.fn().mockResolvedValue(undefined);
@@ -85,6 +86,7 @@ vi.mock("../daemon/service.js", () => ({
notLoadedText: "not loaded", notLoadedText: "not loaded",
install: serviceInstall, install: serviceInstall,
uninstall: serviceUninstall, uninstall: serviceUninstall,
stop: serviceStop,
restart: serviceRestart, restart: serviceRestart,
isLoaded: serviceIsLoaded, isLoaded: serviceIsLoaded,
readCommand: vi.fn(), readCommand: vi.fn(),

View File

@@ -660,6 +660,12 @@ export async function doctorCommand(runtime: RuntimeEnv = defaultRuntime) {
if (!loaded) { if (!loaded) {
note("Gateway daemon not installed.", "Gateway"); note("Gateway daemon not installed.", "Gateway");
} else { } else {
if (process.platform === "darwin") {
note(
`LaunchAgent loaded; stopping requires "clawdbot gateway stop" or launchctl bootout gui/$UID/${GATEWAY_LAUNCH_AGENT_LABEL}.`,
"Gateway",
);
}
const restart = guardCancel( const restart = guardCancel(
await confirm({ await confirm({
message: "Restart gateway daemon now?", message: "Restart gateway daemon now?",

View File

@@ -33,9 +33,7 @@ const UI_FILES = [
"ui/src/ui/views/cron.ts", "ui/src/ui/views/cron.ts",
]; ];
const SWIFT_FILES = [ const SWIFT_FILES = ["apps/macos/Sources/Clawdbot/GatewayConnection.swift"];
"apps/macos/Sources/Clawdbot/GatewayConnection.swift",
];
describe("cron protocol conformance", () => { describe("cron protocol conformance", () => {
it("ui + swift include all cron channels from gateway schema", async () => { it("ui + swift include all cron channels from gateway schema", async () => {

View File

@@ -60,7 +60,8 @@ export function normalizeCronJobInput(
if (options.applyDefaults) { if (options.applyDefaults) {
if (!next.wakeMode) next.wakeMode = "next-heartbeat"; if (!next.wakeMode) next.wakeMode = "next-heartbeat";
if (!next.sessionTarget && isRecord(next.payload)) { if (!next.sessionTarget && isRecord(next.payload)) {
const kind = typeof next.payload.kind === "string" ? next.payload.kind : ""; const kind =
typeof next.payload.kind === "string" ? next.payload.kind : "";
if (kind === "systemEvent") next.sessionTarget = "main"; if (kind === "systemEvent") next.sessionTarget = "main";
if (kind === "agentTurn") next.sessionTarget = "isolated"; if (kind === "agentTurn") next.sessionTarget = "isolated";
} }
@@ -73,16 +74,18 @@ export function normalizeCronJobCreate(
raw: unknown, raw: unknown,
options?: NormalizeOptions, options?: NormalizeOptions,
): CronJobCreate | null { ): CronJobCreate | null {
return normalizeCronJobInput(raw, { applyDefaults: true, ...options }) as return normalizeCronJobInput(raw, {
| CronJobCreate applyDefaults: true,
| null; ...options,
}) as CronJobCreate | null;
} }
export function normalizeCronJobPatch( export function normalizeCronJobPatch(
raw: unknown, raw: unknown,
options?: NormalizeOptions, options?: NormalizeOptions,
): CronJobPatch | null { ): CronJobPatch | null {
return normalizeCronJobInput(raw, { applyDefaults: false, ...options }) as return normalizeCronJobInput(raw, {
| CronJobPatch applyDefaults: false,
| null; ...options,
}) as CronJobPatch | null;
} }

View File

@@ -307,6 +307,35 @@ export async function uninstallLaunchAgent({
} }
} }
function isLaunchctlNotLoaded(res: {
stdout: string;
stderr: string;
code: number;
}): boolean {
const detail = `${res.stderr || res.stdout}`.toLowerCase();
return (
detail.includes("no such process") ||
detail.includes("could not find service") ||
detail.includes("not found")
);
}
export async function stopLaunchAgent({
stdout,
}: {
stdout: NodeJS.WritableStream;
}): Promise<void> {
const domain = resolveGuiDomain();
const label = GATEWAY_LAUNCH_AGENT_LABEL;
const res = await execLaunchctl(["bootout", `${domain}/${label}`]);
if (res.code !== 0 && !isLaunchctlNotLoaded(res)) {
throw new Error(
`launchctl bootout failed: ${res.stderr || res.stdout}`.trim(),
);
}
stdout.write(`Stopped LaunchAgent: ${domain}/${label}\n`);
}
export async function installLaunchAgent({ export async function installLaunchAgent({
env, env,
stdout, stdout,

View File

@@ -233,6 +233,28 @@ export async function uninstallScheduledTask({
} }
} }
function isTaskNotRunning(res: {
stdout: string;
stderr: string;
code: number;
}): boolean {
const detail = `${res.stderr || res.stdout}`.toLowerCase();
return detail.includes("not running");
}
export async function stopScheduledTask({
stdout,
}: {
stdout: NodeJS.WritableStream;
}): Promise<void> {
await assertSchtasksAvailable();
const res = await execSchtasks(["/End", "/TN", GATEWAY_WINDOWS_TASK_NAME]);
if (res.code !== 0 && !isTaskNotRunning(res)) {
throw new Error(`schtasks end failed: ${res.stderr || res.stdout}`.trim());
}
stdout.write(`Stopped Scheduled Task: ${GATEWAY_WINDOWS_TASK_NAME}\n`);
}
export async function restartScheduledTask({ export async function restartScheduledTask({
stdout, stdout,
}: { }: {

View File

@@ -3,6 +3,7 @@ import {
isLaunchAgentLoaded, isLaunchAgentLoaded,
readLaunchAgentProgramArguments, readLaunchAgentProgramArguments,
restartLaunchAgent, restartLaunchAgent,
stopLaunchAgent,
uninstallLaunchAgent, uninstallLaunchAgent,
} from "./launchd.js"; } from "./launchd.js";
import { import {
@@ -10,6 +11,7 @@ import {
isScheduledTaskInstalled, isScheduledTaskInstalled,
readScheduledTaskCommand, readScheduledTaskCommand,
restartScheduledTask, restartScheduledTask,
stopScheduledTask,
uninstallScheduledTask, uninstallScheduledTask,
} from "./schtasks.js"; } from "./schtasks.js";
import { import {
@@ -17,6 +19,7 @@ import {
isSystemdServiceEnabled, isSystemdServiceEnabled,
readSystemdServiceExecStart, readSystemdServiceExecStart,
restartSystemdService, restartSystemdService,
stopSystemdService,
uninstallSystemdService, uninstallSystemdService,
} from "./systemd.js"; } from "./systemd.js";
@@ -37,6 +40,7 @@ export type GatewayService = {
env: Record<string, string | undefined>; env: Record<string, string | undefined>;
stdout: NodeJS.WritableStream; stdout: NodeJS.WritableStream;
}) => Promise<void>; }) => Promise<void>;
stop: (args: { stdout: NodeJS.WritableStream }) => Promise<void>;
restart: (args: { stdout: NodeJS.WritableStream }) => Promise<void>; restart: (args: { stdout: NodeJS.WritableStream }) => Promise<void>;
isLoaded: (args: { isLoaded: (args: {
env: Record<string, string | undefined>; env: Record<string, string | undefined>;
@@ -59,6 +63,9 @@ export function resolveGatewayService(): GatewayService {
uninstall: async (args) => { uninstall: async (args) => {
await uninstallLaunchAgent(args); await uninstallLaunchAgent(args);
}, },
stop: async (args) => {
await stopLaunchAgent(args);
},
restart: async (args) => { restart: async (args) => {
await restartLaunchAgent(args); await restartLaunchAgent(args);
}, },
@@ -78,6 +85,9 @@ export function resolveGatewayService(): GatewayService {
uninstall: async (args) => { uninstall: async (args) => {
await uninstallSystemdService(args); await uninstallSystemdService(args);
}, },
stop: async (args) => {
await stopSystemdService(args);
},
restart: async (args) => { restart: async (args) => {
await restartSystemdService(args); await restartSystemdService(args);
}, },
@@ -97,6 +107,9 @@ export function resolveGatewayService(): GatewayService {
uninstall: async (args) => { uninstall: async (args) => {
await uninstallScheduledTask(args); await uninstallScheduledTask(args);
}, },
stop: async (args) => {
await stopScheduledTask(args);
},
restart: async (args) => { restart: async (args) => {
await restartScheduledTask(args); await restartScheduledTask(args);
}, },

View File

@@ -331,6 +331,22 @@ export async function uninstallSystemdService({
} }
} }
export async function stopSystemdService({
stdout,
}: {
stdout: NodeJS.WritableStream;
}): Promise<void> {
await assertSystemdAvailable();
const unitName = `${GATEWAY_SYSTEMD_SERVICE_NAME}.service`;
const res = await execSystemctl(["--user", "stop", unitName]);
if (res.code !== 0) {
throw new Error(
`systemctl stop failed: ${res.stderr || res.stdout}`.trim(),
);
}
stdout.write(`Stopped systemd service: ${unitName}\n`);
}
export async function restartSystemdService({ export async function restartSystemdService({
stdout, stdout,
}: { }: {

View File

@@ -1,3 +1,7 @@
import {
normalizeCronJobCreate,
normalizeCronJobPatch,
} from "../../cron/normalize.js";
import { import {
readCronRunLogEntries, readCronRunLogEntries,
resolveCronRunLogPath, resolveCronRunLogPath,
@@ -17,10 +21,6 @@ import {
validateWakeParams, validateWakeParams,
} from "../protocol/index.js"; } from "../protocol/index.js";
import type { GatewayRequestHandlers } from "./types.js"; import type { GatewayRequestHandlers } from "./types.js";
import {
normalizeCronJobCreate,
normalizeCronJobPatch,
} from "../../cron/normalize.js";
export const cronHandlers: GatewayRequestHandlers = { export const cronHandlers: GatewayRequestHandlers = {
wake: ({ params, respond, context }) => { wake: ({ params, respond, context }) => {
@@ -88,9 +88,7 @@ export const cronHandlers: GatewayRequestHandlers = {
); );
return; return;
} }
const job = await context.cron.add( const job = await context.cron.add(normalized as unknown as CronJobCreate);
normalized as unknown as CronJobCreate,
);
respond(true, job, undefined); respond(true, job, undefined);
}, },
"cron.update": async ({ params, respond, context }) => { "cron.update": async ({ params, respond, context }) => {

View File

@@ -60,6 +60,7 @@ import {
resolveGatewayPort, resolveGatewayPort,
writeConfigFile, writeConfigFile,
} from "../config/config.js"; } from "../config/config.js";
import type { AgentModelListConfig } from "../config/types.js";
import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js"; import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
import { resolveGatewayProgramArguments } from "../daemon/program-args.js"; import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
import { resolveGatewayService } from "../daemon/service.js"; import { resolveGatewayService } from "../daemon/service.js";
@@ -68,7 +69,6 @@ import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js"; import { defaultRuntime } from "../runtime.js";
import { resolveUserPath, sleep } from "../utils.js"; import { resolveUserPath, sleep } from "../utils.js";
import type { WizardPrompter } from "./prompts.js"; import type { WizardPrompter } from "./prompts.js";
import type { AgentModelListConfig } from "../config/types.js";
const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.2"; const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.2";