You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Bug]: onIsolatedAgentSetupTimeout triggers full-process gateway restart on event-loop saturation — destroys in-flight work and re-enters the same saturation on next tick (2026.6.9) #95784
Workspace plugin in play: family-week (amplifier — see "Plugin attach cost" below; this issue is not about the plugin itself)
TL;DR
When a scheduled isolated cron agent fails to complete its setup phase within the configured timeout, onIsolatedAgentSetupTimeout (in server-cron-DfOwaY9F.js:3770) responds by calling requestSafeGatewayRestart. In every observed case the root cause of the setup timeout has been event-loop saturation in the parent gateway process — not corruption of the gateway state, not a stuck child process, not a leaked handle.
A full-process gateway restart is the wrong primitive for that root cause:
It doesn't fix the cause. Event-loop saturation is process-state. After restart, the same main-session embedded runs resume against the same workload, the loop re-saturates, and the next cron tick re-wedges. Observed cycle time ~10 minutes in the 2026-06-22 incident.
"Safe" in requestSafeGatewayRestart is misleading. It preserves pending emit hooks but does not preserve in-flight embedded runs, which is what users actually care about.
The restart is an amplifier, not a circuit breaker. Once systemd's Restart=always is in the loop, the gateway thrashes against the same saturation pattern. In the 2026-06-22 incident the systemd auto-restart counter reached 29 before the loop broke.
Production incident (2026-06-22, UTC)
Sequence reconstructed from gateway journal + stability bundle:
12:11:08 — first cron job stalls on phase runtime-plugins (setup, pre-execution).
12:15–12:21 — eventLoopUtilization sustained at 99–100% with eventLoopDelayP99Ms 8–12 s. Driver: 4 concurrent main-session embedded runs.
12:17:52, 12:17:57 — two cron jobs fail isolated agent setup, both on runtime-plugins phase.
12:22:14 — forced restart requested; skipping active work drain. 3 active embedded runs nuked.
12:22:16 — shutdown timed out; exiting without full cleanup → exit 1. Stability bundle written with reason: gateway.restart_shutdown_timeout. systemd auto-restart counter increments to 29.
12:22:18 — main-session-restart-recovery emits failed=2 recovered=0; the failure cause is gateway timeout after 10000ms on the embedded runs that had been nuked 4 seconds earlier.
Recovery: manual operator intervention to reset systemd state.
Code site
<workspace>/dist/server-cron-DfOwaY9F.js:3770 — onIsolatedAgentSetupTimeout → requestSafeGatewayRestart (with delayMs: 0). The decision is unconditional on setup timeout — no signal disambiguation between "gateway state is corrupt" and "this tick saturated the loop".
Plugin attach cost (why setup blows past the timeout under load)
Per isolated cron agent, runtime-plugin attach runs ~14 sequential register* calls (tools, channel handlers, command handlers, RPC, CLI, HTTP routes) synchronously on the gateway event loop. Under nominal load, this completes in ~3.3 s (journal line: agent runtime plugins pre-warmed in 3300ms). The cron setup timeout is 60 s default (configurable per job). Under sustained event-loop saturation (utilization near 1.0, p99Delay 8–12 s), the same 3.3 s of attached work elongates well past 60 s — not because anything is broken, just because the loop is starved.
This is the amplification path: any process-wide event-loop pressure converts cron setup into a restart trigger, and the restart re-enters the same pressure.
Expected behavior
onIsolatedAgentSetupTimeout should treat a single setup timeout as a tick-level failure, not a process-level one. Concretely:
Default response: cancel the tick, back off this cron job's next fire (exponential), preserve in-flight non-cron work.
Adaptive timeout: raise the cron setup timeout proportional to current event-loop p99Delay so a saturated loop doesn't get punished by a fixed-budget watchdog.
Escalation criteria for a real restart: only restart the gateway when a stronger signal indicates actual plugin-runtime corruption — e.g. N consecutive setup timeouts across distinct cron jobs with the event loop idle between them. A saturated loop is evidence of workload pressure, not corruption.
Single isolated-cron setup timeout → unconditional requestSafeGatewayRestart(delayMs: 0) → forced restart with skipped work drain → in-flight embedded runs killed → recovery-side timeout failure (#95750) → systemd auto-restart → same loop saturation → next cron tick re-fires the same watchdog.
Reproducer
Run the gateway under systemd --user with Restart=always.
Register an isolated cron whose runtime-plugin set includes a moderately sized workspace plugin (the family-week workspace plugin in the observed incident registers ~14 routes synchronously; any plugin with comparable attach surface works).
Schedule it to fire every ~5 minutes.
Independently, start ≥3 concurrent main-session embedded turns with backends that hold the loop busy (large model deliberation, streaming-heavy SSE consumers, etc.) so eventLoopUtilization climbs to ≥0.95 and eventLoopDelayP99Ms exceeds the configured cron setup timeout's safety margin.
Observe: the next cron tick fails setup on phase runtime-plugins; onIsolatedAgentSetupTimeout fires; requestSafeGatewayRestart is called with delayMs: 0; the forced restart skips drain; in-flight embedded runs are killed; restart-recovery fails to recover them; the loop re-enters saturation and the next cron tick re-wedges.
The reproduction is deterministic given sustained event-loop pressure during a cron tick.
Supervisor handoff file: <workspace>/gateway-supervisor-restart-handoff.json — recorded reason: "cron.isolated_agent_setup_timeout" at 12:21:40, ~10 s before SIGUSR1.
A staged remediation. Items (1) and (2) are the operator-facing win; (3) and (4) round out the contract.
Decouple setup-timeout from gateway-restart.onIsolatedAgentSetupTimeout should default to cancel this tick + back off this job's next fire. It should NOT call requestSafeGatewayRestart directly.
Adaptive setup timeout. Compute the effective setup deadline as baseSetupTimeoutMs + k * eventLoopDelayP99Ms (or a saturation-aware variant). Under nominal load this is a no-op; under saturation it prevents the false trigger.
Restart only on a corruption-class signal. If a restart is still needed (e.g. N consecutive setup timeouts across distinct jobs with idle event loop between them), gate it behind that stronger predicate, and use a non-zero drain budget that lets in-flight embedded runs settle.
Happy to scope a PR if maintainers prefer concrete code. The narrow change for (1) is a behavior flip in onIsolatedAgentSetupTimeout; the broader changes are larger.
Is there an existing config knob that biases onIsolatedAgentSetupTimeout toward "cancel tick" instead of "restart gateway"? I didn't find one; checked cron.* and recovery.*. If one exists, this issue collapses to a documentation fix.
Is the requestSafeGatewayRestart name reserved for a future implementation that does preserve in-flight embedded runs? If so, the gap is the implementation, not the naming.
Environment
2026.6.9cliBackends.claude-cli(Anthropic Claude Code CLI)agents.list[main], modelclaude-cli/claude-opus-4-7systemd --user(openclaw-gateway.service,Restart=always,StartLimitBurst=5,StartLimitIntervalSec=60)family-week(amplifier — see "Plugin attach cost" below; this issue is not about the plugin itself)TL;DR
When a scheduled isolated cron agent fails to complete its setup phase within the configured timeout,
onIsolatedAgentSetupTimeout(inserver-cron-DfOwaY9F.js:3770) responds by callingrequestSafeGatewayRestart. In every observed case the root cause of the setup timeout has been event-loop saturation in the parent gateway process — not corruption of the gateway state, not a stuck child process, not a leaked handle.A full-process gateway restart is the wrong primitive for that root cause:
requestSafeGatewayRestartis called withdelayMs: 0. The shutdown path then logsforced restart requested; skipping active work drain(drain timeout 0 ms). In the 2026-06-22 incident this nuked 3 active main-session embedded runs. Restart-recovery then failed for 2 of them withgateway timeout after 10000ms(the cross-boot retry-budget gap is tracked separately at [Bug]: Main-session restart-recovery has no cross-boot retry budget — wedged sessions death-loop the gateway across reboots (2026.6.9) #95750).requestSafeGatewayRestartis misleading. It preserves pending emit hooks but does not preserve in-flight embedded runs, which is what users actually care about.systemd'sRestart=alwaysis in the loop, the gateway thrashes against the same saturation pattern. In the 2026-06-22 incident the systemd auto-restart counter reached 29 before the loop broke.Production incident (2026-06-22, UTC)
Sequence reconstructed from gateway journal + stability bundle:
12:11:08— first cron job stalls on phaseruntime-plugins(setup, pre-execution).12:15–12:21—eventLoopUtilizationsustained at 99–100% witheventLoopDelayP99Ms8–12 s. Driver: 4 concurrent main-session embedded runs.12:17:52,12:17:57— two cron jobs fail isolated agent setup, both onruntime-pluginsphase.12:21:40— supervisor handoff file written:reason: "cron.isolated_agent_setup_timeout".12:21:50— gateway receivesSIGUSR1; restarting.12:22:14—forced restart requested; skipping active work drain. 3 active embedded runs nuked.12:22:16—shutdown timed out; exiting without full cleanup→ exit 1. Stability bundle written withreason: gateway.restart_shutdown_timeout.systemdauto-restart counter increments to 29.12:22:18— main-session-restart-recovery emitsfailed=2 recovered=0; the failure cause isgateway timeout after 10000mson the embedded runs that had been nuked 4 seconds earlier.Recovery: manual operator intervention to reset systemd state.
Code site
<workspace>/dist/server-cron-DfOwaY9F.js:3770—onIsolatedAgentSetupTimeout→requestSafeGatewayRestart(withdelayMs: 0). The decision is unconditional on setup timeout — no signal disambiguation between "gateway state is corrupt" and "this tick saturated the loop".Plugin attach cost (why setup blows past the timeout under load)
Per isolated cron agent, runtime-plugin attach runs ~14 sequential
register*calls (tools, channel handlers, command handlers, RPC, CLI, HTTP routes) synchronously on the gateway event loop. Under nominal load, this completes in ~3.3 s (journal line:agent runtime plugins pre-warmed in 3300ms). The cron setup timeout is 60 s default (configurable per job). Under sustained event-loop saturation (utilizationnear 1.0,p99Delay8–12 s), the same 3.3 s of attached work elongates well past 60 s — not because anything is broken, just because the loop is starved.This is the amplification path: any process-wide event-loop pressure converts cron setup into a restart trigger, and the restart re-enters the same pressure.
Expected behavior
onIsolatedAgentSetupTimeoutshould treat a single setup timeout as a tick-level failure, not a process-level one. Concretely:p99Delayso a saturated loop doesn't get punished by a fixed-budget watchdog.delayMs: 0+ drain-skip on a watchdog-triggered restart: if a restart is unavoidable, the drain budget should at minimum allow currently-streaming embedded runs to settle, or be marked for recovery with a non-trivial deadline (see [Bug]: Main-session restart-recovery has no cross-boot retry budget — wedged sessions death-loop the gateway across reboots (2026.6.9) #95750 for the recovery-side complement).Actual behavior
Single isolated-cron setup timeout → unconditional
requestSafeGatewayRestart(delayMs: 0)→ forced restart with skipped work drain → in-flight embedded runs killed → recovery-side timeout failure (#95750) → systemd auto-restart → same loop saturation → next cron tick re-fires the same watchdog.Reproducer
systemd --userwithRestart=always.family-weekworkspace plugin in the observed incident registers ~14 routes synchronously; any plugin with comparable attach surface works).eventLoopUtilizationclimbs to ≥0.95 andeventLoopDelayP99Msexceeds the configured cron setup timeout's safety margin.runtime-plugins;onIsolatedAgentSetupTimeoutfires;requestSafeGatewayRestartis called withdelayMs: 0; the forced restart skips drain; in-flight embedded runs are killed; restart-recovery fails to recover them; the loop re-enters saturation and the next cron tick re-wedges.The reproduction is deterministic given sustained event-loop pressure during a cron tick.
Evidence
<workspace>/logs/stability/openclaw-stability-2026-06-22T12-22-16-782Z-<pid>-gateway.restart_shutdown_timeout.json(reasongateway.restart_shutdown_timeout).<workspace>/gateway-supervisor-restart-handoff.json— recordedreason: "cron.isolated_agent_setup_timeout"at12:21:40, ~10 s before SIGUSR1.<workspace>/dist/server-cron-DfOwaY9F.js:3770—onIsolatedAgentSetupTimeout→requestSafeGatewayRestart.agent runtime plugins pre-warmed in 3300ms(nominal load).eventLoopUtilization99–100%,eventLoopDelayP99Ms8–12 s, 4 concurrent main-session embedded runs.Suggested fix
A staged remediation. Items (1) and (2) are the operator-facing win; (3) and (4) round out the contract.
onIsolatedAgentSetupTimeoutshould default to cancel this tick + back off this job's next fire. It should NOT callrequestSafeGatewayRestartdirectly.baseSetupTimeoutMs + k * eventLoopDelayP99Ms(or a saturation-aware variant). Under nominal load this is a no-op; under saturation it prevents the false trigger.requestSafeGatewayRestartsemantics. Either (a) rename to clarify it does not preserve in-flight embedded runs, or (b) extend it to actually preserve them via a longer drain plus the recovery-side fix at [Bug]: Main-session restart-recovery has no cross-boot retry budget — wedged sessions death-loop the gateway across reboots (2026.6.9) #95750. Today the name implies a guarantee the implementation doesn't provide.Happy to scope a PR if maintainers prefer concrete code. The narrow change for (1) is a behavior flip in
onIsolatedAgentSetupTimeout; the broader changes are larger.Related upstream issues
restart gateway after isolated cron setup timeout. This issue argues the inverse direction — that the restart-on-setup-timeout decision is itself wrong under the dominant failure mode. Worth coordinating before fix: restart gateway after isolated cron setup timeout #89055 merges.runtime-pluginsphase (the symptom this issue's restart trigger fires on).Sanity questions
onIsolatedAgentSetupTimeouttoward "cancel tick" instead of "restart gateway"? I didn't find one; checkedcron.*andrecovery.*. If one exists, this issue collapses to a documentation fix.requestSafeGatewayRestartname reserved for a future implementation that does preserve in-flight embedded runs? If so, the gap is the implementation, not the naming.