Skip to content

Commit ba9a6ec

Browse files
committed
fix(gateway): bound explicit restart deferral
1 parent 762b20c commit ba9a6ec

5 files changed

Lines changed: 32 additions & 1 deletion

File tree

src/agents/tools/gateway-tool.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ describe("gateway tool restart continuation", () => {
138138
);
139139
expect(scheduleGatewaySigusr1RestartMock).toHaveBeenCalledWith({
140140
delayMs: 250,
141+
deferralTimeoutMs: 30_000,
141142
reason: "continue after reboot",
142143
emitHooks: expect.objectContaining({
143144
beforeEmit: expect.any(Function),

src/agents/tools/gateway-tool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { isOpenClawOwnerOnlyCoreToolName } from "./owner-only-tools.js";
2424
const log = createSubsystemLogger("gateway-tool");
2525

2626
const DEFAULT_UPDATE_TIMEOUT_MS = 20 * 60_000;
27+
const EXPLICIT_RESTART_DEFERRAL_TIMEOUT_MS = 30_000;
2728
// Security: the agent-facing `gateway` tool is owner-only, but per SECURITY.md the model/agent
2829
// itself is not a trusted principal. `assertGatewayConfigMutationAllowed` is the explicit
2930
// model -> operator trust-boundary control on `config.apply`/`config.patch`, so the runtime
@@ -411,6 +412,7 @@ export function createGatewayTool(opts?: {
411412
let sentinelPath: string | null = null;
412413
const scheduled = scheduleGatewaySigusr1Restart({
413414
delayMs,
415+
deferralTimeoutMs: EXPLICIT_RESTART_DEFERRAL_TIMEOUT_MS,
414416
reason,
415417
emitHooks: {
416418
beforeEmit: async () => {

src/auto-reply/reply/commands-session.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { persistSessionEntry } from "./commands-session-store.js";
3737
import type { CommandHandler, HandleCommandsParams } from "./commands-types.js";
3838
import { resolveConversationBindingContextFromAcpCommand } from "./conversation-binding-input.js";
3939

40+
const EXPLICIT_RESTART_DEFERRAL_TIMEOUT_MS = 30_000;
41+
4042
const SESSION_COMMAND_PREFIX = "/session";
4143
const SESSION_DURATION_OFF_VALUES = new Set(["off", "disable", "disabled", "none", "0"]);
4244
const SESSION_ACTION_IDLE = "idle";
@@ -680,6 +682,7 @@ export const handleRestartCommand: CommandHandler = async (params, allowTextComm
680682
if (hasSigusr1Listener) {
681683
let sentinelPath: string | null = null;
682684
scheduleGatewaySigusr1Restart({
685+
deferralTimeoutMs: EXPLICIT_RESTART_DEFERRAL_TIMEOUT_MS,
683686
reason: "/restart",
684687
emitHooks: sentinelPayload
685688
? {

src/infra/infra-runtime.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,24 @@ describe("infra runtime", () => {
540540
}
541541
});
542542

543+
it("emits SIGUSR1 after per-request deferral timeout even without runtime timeout", async () => {
544+
const emitSpy = vi.spyOn(process, "emit");
545+
const handler = () => {};
546+
process.on("SIGUSR1", handler);
547+
try {
548+
setPreRestartDeferralCheck(() => 5);
549+
scheduleGatewaySigusr1Restart({ delayMs: 0, deferralTimeoutMs: 1_000 });
550+
551+
await vi.advanceTimersByTimeAsync(0);
552+
expect(emitSpy).not.toHaveBeenCalledWith("SIGUSR1");
553+
554+
await vi.advanceTimersByTimeAsync(1_000);
555+
expect(emitSpy).toHaveBeenCalledWith("SIGUSR1");
556+
} finally {
557+
process.removeListener("SIGUSR1", handler);
558+
}
559+
});
560+
543561
it("emits SIGUSR1 if deferral check throws", async () => {
544562
const emitSpy = vi.spyOn(process, "emit");
545563
const handler = () => {};

src/infra/restart.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ export type ScheduledRestart = {
655655

656656
export function scheduleGatewaySigusr1Restart(opts?: {
657657
delayMs?: number;
658+
deferralTimeoutMs?: number;
658659
reason?: string;
659660
audit?: RestartAuditInfo;
660661
emitHooks?: RestartEmitHooks;
@@ -738,9 +739,15 @@ export function scheduleGatewaySigusr1Restart(opts?: {
738739
return;
739740
}
740741
const cfg = getRuntimeConfig();
742+
const deferralTimeoutMs =
743+
typeof opts?.deferralTimeoutMs === "number" &&
744+
Number.isFinite(opts.deferralTimeoutMs) &&
745+
opts.deferralTimeoutMs > 0
746+
? Math.floor(opts.deferralTimeoutMs)
747+
: cfg.gateway?.reload?.deferralTimeoutMs;
741748
deferGatewayRestartUntilIdle({
742749
getPendingCount: pendingCheck,
743-
maxWaitMs: resolveGatewayRestartDeferralTimeoutMs(cfg.gateway?.reload?.deferralTimeoutMs),
750+
maxWaitMs: resolveGatewayRestartDeferralTimeoutMs(deferralTimeoutMs),
744751
reason: scheduledReason,
745752
});
746753
},

0 commit comments

Comments
 (0)