openclaw - ✅(Solved) Fix [BUG] config.patch still sends SIGUSR1 for hot-reloadable paths (browser.profiles.*), bypassing reload mode [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#43803Fetched 2026-04-08 00:17:55
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
cross-referenced ×2commented ×1renamed ×1unsubscribed ×1

We are building a sandbox skill (novita-sandbox) that runs browser operations and untrusted code in isolated Firecracker microVMs. For browser use cases, the ideal workflow is:

  1. Create a browser-chromium sandbox (Chromium + CDP on port 9223)
  2. Use config.patch to set browser.profiles.sandbox.cdpUrl pointing to the sandbox's CDP endpoint
  3. Use OpenClaw's native browser tool with the sandbox profile — full interactive capabilities (snapshot, click, fill, navigate) running safely inside the isolated VM

This pattern is powerful because it gives OpenClaw's browser tool full functionality while completely isolating it from the user's local network (preventing SSRF, local file access, etc.).

Root Cause

In gateway-cli-C2ZZYgwu.js, the config.patch RPC handler unconditionally calls scheduleGatewaySigusr1Restart() after writing the config:

// Line 12912-12922 in gateway-cli-C2ZZYgwu.js
const sentinelPath = await tryWriteRestartSentinelPayload(payload);
const restart = scheduleGatewaySigusr1Restart({
    delayMs: restartDelayMs,
    reason: "config.patch",
    audit: {
        actor: actor.actor,
        deviceId: actor.deviceId,
        clientIp: actor.clientIp,
        changedPaths
    }
});

This triggers the following chain in restart-C7ane9OU.js:

scheduleGatewaySigusr1Restart()
  → authorizeGatewaySigusr1Restart()    // sets sigusr1AuthorizedCount += 1
  → (delay ~8-10s)
  → emitGatewayRestart()
    → process.emit("SIGUSR1")

onSigusr1 handler:
  → consumeGatewaySigusr1RestartAuthorization()  // finds authorized count > 0, returns true
  → request("restart", "SIGUSR1")                // gateway dies

This path is completely independent of the reload mode system. The reload logic (BASE_RELOAD_RULES) correctly classifies browser.* as kind: "hot" with action restart-browser-control — and in fact the hot reload succeeds before the SIGUSR1 kills the process:

14:55:52 [reload] config hot reload applied (browser.profiles.sandbox.cdpUrl)   ← SUCCESS
14:56:00 [gateway] signal SIGUSR1 received                                       ← KILLED
14:56:01 [gateway] restart mode: full process restart (supervisor restart)

Fix Action

Workaround

Our sandbox skill (novita-sandbox) currently disables CDP mode entirely and uses Exec mode only — running curl/puppeteer/playwright inside the sandbox VM and returning results as text. This works but loses the native browser tool's interactive capabilities (snapshot, click, fill, navigate).

PR fix notes

PR #45624: Skip config.patch restart for hot-reload browser changes

Description (problem / solution / changelog)

Skip config.patch restart for hot-reload browser changes

Summary

This teaches config.patch to evaluate the patched config's reload mode and changed paths before scheduling SIGUSR1. Hot-reloadable and no-op changes now return without writing a restart sentinel or queueing a restart, while restart-required paths and gateway.reload.mode=off|restart keep the existing restart behavior.

Problem

  • Expected: config.patch for hot-reloadable browser fields should apply through the config watcher without bouncing the gateway, and changing gateway.reload.mode in the same patch should take effect immediately.
  • Actual: config.patch in src/gateway/server-methods/config.ts:425 always schedules scheduleGatewaySigusr1Restart() after writing the config, even when the reload planner classifies the changed paths as hot or no-op.
  • Impact: browser-profile patches bounce the gateway, kill foreground/no-supervisor runs, and disrupt active sessions.

Reproduction

  1. Start the gateway with config reload enabled.
  2. Call config.patch with a payload like { "gateway": { "reload": { "mode": "hybrid" } }, "browser": { "profiles": { "sandbox": { "cdpUrl": "http://127.0.0.1:9222", "color": "#0066CC" } } } }.
  3. Observe the returned restart payload and the later SIGUSR1.
  • Expected result: the patch succeeds, the watcher hot-reloads browser state, and no restart is queued.
  • Actual result: config.patch returns a scheduled restart and writes a restart sentinel.

Issues Found

Severity: high Confidence: high Status: fixed

IDSeverityConfidenceAreaSummaryEvidenceStatus
PR-45624-BUG-01highhighsrc/gateway/server-methods/config.tsconfig.patch schedules SIGUSR1 even for hot/no-op changessrc/gateway/server-methods/config.ts:425 and src/gateway/server-methods/config.restart.test.ts:147fixed

Fix Approach

  • Add shouldConfigPatchTriggerGatewayRestart(...) in src/gateway/config-reload.ts so config.patch reuses the same reload classification logic as the file watcher and respects the patched gateway.reload.mode.
  • In src/gateway/server-methods/config.ts, only write the restart sentinel and schedule SIGUSR1 when that helper says a restart is required.
  • Add regression coverage for no-op, hot-reload, and restart-required paths plus a handler-level test for switching from restart to hybrid in the same patch.
  • Update the gateway tool/system prompt copy so agents no longer claim config.patch always restarts.

Testing

  • pnpm test -- src/gateway/config-reload.test.ts src/gateway/server.config-patch.test.ts src/gateway/server-methods/config.restart.test.ts src/agents/system-prompt.test.ts src/agents/openclaw-gateway-tool.test.ts (pass)
  • pnpm build (pass)
  • pnpm check (fails: pre-existing unrelated TypeScript errors in src/infra/outbound/deliver.lifecycle.test.ts, src/infra/outbound/deliver.test-helpers.ts, and src/infra/outbound/deliver.test.ts after syncing latest main)

Risk / Notes

  • config.apply still follows the restart path; this PR intentionally scopes behavior changes to config.patch.
  • Closes #43803

Changed files

  • src/agents/system-prompt.ts (modified, +1/-1)
  • src/agents/tools/gateway-tool.ts (modified, +1/-1)
  • src/gateway/config-reload.test.ts (modified, +56/-0)
  • src/gateway/config-reload.ts (modified, +22/-0)
  • src/gateway/server-methods/config.restart.test.ts (added, +186/-0)
  • src/gateway/server-methods/config.ts (modified, +41/-29)

Code Example

// Line 12912-12922 in gateway-cli-C2ZZYgwu.js
const sentinelPath = await tryWriteRestartSentinelPayload(payload);
const restart = scheduleGatewaySigusr1Restart({
    delayMs: restartDelayMs,
    reason: "config.patch",
    audit: {
        actor: actor.actor,
        deviceId: actor.deviceId,
        clientIp: actor.clientIp,
        changedPaths
    }
});

---

scheduleGatewaySigusr1Restart()
authorizeGatewaySigusr1Restart()    // sets sigusr1AuthorizedCount += 1
   (delay ~8-10s)
emitGatewayRestart()
    → process.emit("SIGUSR1")

onSigusr1 handler:
consumeGatewaySigusr1RestartAuthorization()  // finds authorized count > 0, returns true
request("restart", "SIGUSR1")                // gateway dies

---

14:55:52 [reload] config hot reload applied (browser.profiles.sandbox.cdpUrl)SUCCESS
14:56:00 [gateway] signal SIGUSR1 received                                       ← KILLED
14:56:01 [gateway] restart mode: full process restart (supervisor restart)

---

// After writing config, evaluate if restart is actually needed
const plan = evaluateReloadPlan(changedPaths); // uses BASE_RELOAD_RULES
if (plan.restartGateway) {
    scheduleGatewaySigusr1Restart({ ... });
} else {
    // Hot reload already handled by the config watcher / reload system
    // No SIGUSR1 needed
}

---

const hotReloadablePrefixes = ['browser.', 'meta.'];  // from BASE_RELOAD_RULES
const allHot = changedPaths.every(p => hotReloadablePrefixes.some(prefix => p.startsWith(prefix)));
if (!allHot) {
    scheduleGatewaySigusr1Restart({ ... });
}
RAW_BUFFERClick to expand / collapse

Related Issues

This is a continuation of a known problem that has been partially fixed but not fully resolved:

  • Issue #5533 (Jan 2026) — Reported that config modifications unconditionally trigger SIGUSR1 even when restart is disabled. Fixed by PR #13408 (fix(gateway): skip SIGUSR1 restart in config.patch for noop reload rules), which skipped SIGUSR1 for no-op changes.
  • Issue #20245 (Feb 2026) — Reported that agent-initiated config.patch calls bypass restart protection. Fixed by PR #20355 (fix(gateway): respect commands.restart guard in config.patch and config.apply), which added guard checks.

Despite these fixes, the March 2026 release still crashes the gateway when config.patch modifies paths that are explicitly hot-reloadable per BASE_RELOAD_RULES (e.g., browser.profiles.*). The previous PRs addressed no-op changes and commands.restart guards, but did not address the case where changedPaths match hot-reload rules — the RPC handler still unconditionally calls scheduleGatewaySigusr1Restart() regardless of whether the reload system can handle the change without a restart.

Context

We are building a sandbox skill (novita-sandbox) that runs browser operations and untrusted code in isolated Firecracker microVMs. For browser use cases, the ideal workflow is:

  1. Create a browser-chromium sandbox (Chromium + CDP on port 9223)
  2. Use config.patch to set browser.profiles.sandbox.cdpUrl pointing to the sandbox's CDP endpoint
  3. Use OpenClaw's native browser tool with the sandbox profile — full interactive capabilities (snapshot, click, fill, navigate) running safely inside the isolated VM

This pattern is powerful because it gives OpenClaw's browser tool full functionality while completely isolating it from the user's local network (preventing SSRF, local file access, etc.).

The Problem

Every config.patch call crashes the gateway, regardless of what config path is changed and regardless of the gateway.reload setting.

Root Cause

In gateway-cli-C2ZZYgwu.js, the config.patch RPC handler unconditionally calls scheduleGatewaySigusr1Restart() after writing the config:

// Line 12912-12922 in gateway-cli-C2ZZYgwu.js
const sentinelPath = await tryWriteRestartSentinelPayload(payload);
const restart = scheduleGatewaySigusr1Restart({
    delayMs: restartDelayMs,
    reason: "config.patch",
    audit: {
        actor: actor.actor,
        deviceId: actor.deviceId,
        clientIp: actor.clientIp,
        changedPaths
    }
});

This triggers the following chain in restart-C7ane9OU.js:

scheduleGatewaySigusr1Restart()
  → authorizeGatewaySigusr1Restart()    // sets sigusr1AuthorizedCount += 1
  → (delay ~8-10s)
  → emitGatewayRestart()
    → process.emit("SIGUSR1")

onSigusr1 handler:
  → consumeGatewaySigusr1RestartAuthorization()  // finds authorized count > 0, returns true
  → request("restart", "SIGUSR1")                // gateway dies

This path is completely independent of the reload mode system. The reload logic (BASE_RELOAD_RULES) correctly classifies browser.* as kind: "hot" with action restart-browser-control — and in fact the hot reload succeeds before the SIGUSR1 kills the process:

14:55:52 [reload] config hot reload applied (browser.profiles.sandbox.cdpUrl)   ← SUCCESS
14:56:00 [gateway] signal SIGUSR1 received                                       ← KILLED
14:56:01 [gateway] restart mode: full process restart (supervisor restart)

What we tried

We initially assumed gateway.reload: "hot" would prevent the restart (since the reload rules correctly handle browser.* as hot-reloadable). Through multiple iterations we discovered:

  1. Default "hybrid" mode — gateway crashes after config.patch ❌
  2. Setting gateway.reload: "hot" via config.patch — the config.patch itself crashes the gateway before the setting takes effect ❌
  3. Manually editing openclaw.json to set gateway.reload: "hot", then using config.patch for browser.profiles.* — hot reload succeeds, but SIGUSR1 still fires 8 seconds later and kills the process ❌

The SIGUSR1 restart is a completely separate code path from the reload mode evaluation. gateway.reload: "hot" only affects the config file watcher's reload plan (plan.restartGateway is logged but ignored in hot mode) — it does NOT affect the scheduleGatewaySigusr1Restart called explicitly by the config.patch handler.

Impact

  • Any skill or tool that uses config.patch will crash the gateway when run without a supervisor (e.g., openclaw gateway run in foreground/nohup)
  • Even with a supervisor, every config.patch causes an unnecessary full restart, disrupting active agent sessions
  • This makes it impossible for skills to dynamically configure browser profiles, which is a key use case for sandbox-based browsing

Proposal

PR #13408 skipped SIGUSR1 for no-op changes, and PR #20355 added commands.restart guards. The missing piece is: config.patch should also skip SIGUSR1 when all changed paths are hot-reloadable per BASE_RELOAD_RULES. The reload system already handles these changes correctly — the SIGUSR1 is redundant and destructive.

Suggested approaches:

Option A: Check reload rules before scheduling restart

// After writing config, evaluate if restart is actually needed
const plan = evaluateReloadPlan(changedPaths); // uses BASE_RELOAD_RULES
if (plan.restartGateway) {
    scheduleGatewaySigusr1Restart({ ... });
} else {
    // Hot reload already handled by the config watcher / reload system
    // No SIGUSR1 needed
}

Option B: Skip SIGUSR1 for hot-reloadable paths

const hotReloadablePrefixes = ['browser.', 'meta.'];  // from BASE_RELOAD_RULES
const allHot = changedPaths.every(p => hotReloadablePrefixes.some(prefix => p.startsWith(prefix)));
if (!allHot) {
    scheduleGatewaySigusr1Restart({ ... });
}

Option C: Honor gateway.reload mode in the SIGUSR1 path

If gateway.reload is "hot", the scheduleGatewaySigusr1Restart should check whether the changed paths actually require a restart (per BASE_RELOAD_RULES) and skip the SIGUSR1 if all changes are hot-reloadable.

Environment

  • OpenClaw version: installed via npm install -g openclaw (March 2026)
  • Gateway file: gateway-cli-C2ZZYgwu.js
  • Restart module: restart-C7ane9OU.js
  • OS: macOS Darwin 25.2.0
  • Run mode: openclaw gateway run --token xxx (no supervisor)

Workaround

Our sandbox skill (novita-sandbox) currently disables CDP mode entirely and uses Exec mode only — running curl/puppeteer/playwright inside the sandbox VM and returning results as text. This works but loses the native browser tool's interactive capabilities (snapshot, click, fill, navigate).

extent analysis

Problem Summary

config.patch always calls scheduleGatewaySigusr1Restart().
Even when the changed keys are hot‑reloadable (e.g. browser.profiles.*) the gateway receives a SIGUSR1 ≈ 8 s later and exits, because the restart path is completely independent of the reload‑rules engine.

Root Cause

The RPC handler in gateway-cli‑C2ZZYgwu.js unconditionally schedules a SIGUSR1 restart after writing the config, without checking the reload plan that BASE_RELOAD_RULES already produces.
Therefore hot‑reloadable changes are applied twice: once by the hot‑reload system (which succeeds) and a second time by the forced SIGUSR1, which kills the process.

Fix Plan

1. Add a guard before scheduling the restart

Use the existing reload‑plan helper (evaluateReloadPlan) to decide whether a full restart is really required.

// gateway-cli-C2ZZYgwu.js  (around line 12912)
const sentinelPath = await tryWriteRestartSentinelPayload(payload);

// ---- NEW CODE -------------------------------------------------
/**
 * Decide if a full SIGUSR1 restart is needed.
 * `changedPaths` is the array passed to the RPC handler.
 * `evaluateReloadPlan` returns an object like:
 *   { restartGateway: true|false, actions: [...] }
 */
const reloadPlan = evaluateReloadPlan(changedPaths);   // <-- existing helper
if (reloadPlan.restartGateway) {
    // At least one changed path cannot be hot‑reloaded → schedule restart
    scheduleGatewaySigusr1Restart({
        delayMs: restartDelayMs,
        reason: "config.patch",
        audit: {
            actor: actor.actor,
            deviceId: actor.deviceId,
            clientIp: actor.clientIp,
            changedPaths,
        },
    });
} else {
    // All changes were handled by the hot‑reload system → no SIGUSR1
    logger.info(
        "config.patch: hot‑reloadable changes only – skipping SIGUSR1 restart"
    );
}
// ----------------------------------------------------------------

If evaluateReloadPlan is not exported, add a small wrapper in reload.js that re‑uses BASE_RELOAD_RULES.

2. Export the helper (if needed)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [BUG] config.patch still sends SIGUSR1 for hot-reloadable paths (browser.profiles.*), bypassing reload mode [1 pull requests, 1 comments, 2 participants]