Repository navigation
fix(windows): retry detached spawns without CREATE_BREAKAWAY_FROM_JOB when denied - #108720
IzzyWizzy666 wants to merge 1 commit into
Conversation
A Windows host that wraps every spawned process in a Job Object without
JOB_OBJECT_LIMIT_BREAKAWAY_OK makes CreateProcess fail outright with
ERROR_ACCESS_DENIED (WinError 5) when CREATE_BREAKAWAY_FROM_JOB is
requested -- the flag is not merely ignored. Task Scheduler does exactly
this for every task it starts, and a Task Scheduler task is how a gateway
normally comes back after an unattended reboot.
Three detached spawns therefore could never start on that auto-start path,
even though the identical spawn works from a plain shell or the Desktop
app:
* the WhatsApp bridge (plugins/platforms/whatsapp/adapter.py)
* the gateway-side /update spawner (gateway/slash_commands.py)
* the dashboard action spawner (hermes_cli/web_server_gateway.py)
Symptom for the bridge: the gateway starts and cron executes, but the
bridge never connects -- `Failed to start bridge: [WinError 5] Access is
denied` on every retry, and port 3000 never listens.
Add popen_detached_with_breakaway_fallback() to _subprocess_compat and
route all three sites through it, mirroring the existing fallback in
gateway_windows._spawn_detached: retry once with
windows_detach_flags_without_breakaway(), which drops only that bit.
POSIX is unaffected -- there is no breakaway concept, so the original
error propagates unmasked.
hermes_cli/gateway.py::_spawn_detached_gateway is deliberately untouched:
it is only reachable from the macOS-26 launchd fallback, where
windows_detach_popen_kwargs() returns start_new_session and the flag is
irrelevant.
Reproduced by registering a scheduled task that attempts both variants
from inside a real Task Scheduler job:
WITH_BREAKAWAY -> PermissionError [WinError 5]
WITHOUT_BREAKAWAY -> OK
Tests: behavioural regression coverage for both branches (patched
IS_WINDOWS, so they run on Linux CI too).
Related: #87777 covers the same WinError 5 breakaway retry for the WhatsApp bridge only; this PR generalizes it (shared helper in |
|
Confirming this still reproduces on current Live evidence from a self-managed Windows install (today) The gateway comes back through a Windows Scheduled Task (
The shared helper proposed here ( Is anything blocking this from merging? Happy to test a build from the branch on the same Task-Scheduler-started setup (Windows 11, gateway auto-start, WhatsApp bridge + |
|
Independent Windows reproduction + a note on shape (shared helper instead of per-site try/except) Environment: hermes-agent 0.21.5 (6590f13), Windows 11, Hermes-managed Node under Observed: WhatsApp permanently "disconnected"; every reconnect attempt (~every 5 min) logs Same failure class in the same lineage: Ruled out before blaming the job object:
That last point matches this patch's diagnosis: it is the inherited Job Object, not the call. On shape - instead of each site growing its own try/except (gateway_windows._spawn_detached, Verified locally against a stubbed Popen that raises PermissionError(winerror=5) when the flags Sites I could not find a fallback in today, same class: gateway/slash_commands.py:140 (the Duplicates worth consolidating: #132463, #125117, #125531 (whatsapp), #129325 (dashboard), |
Summary
Windows hosts that wrap each spawned process in a Job Object without
JOB_OBJECT_LIMIT_BREAKAWAY_OKmakeCreateProcessfail outright withERROR_ACCESS_DENIED(WinError 5) whenCREATE_BREAKAWAY_FROM_JOBisrequested — the flag is not merely ignored.
Windows Task Scheduler does exactly this for every task it starts, and a
Task Scheduler task is how a gateway normally comes back after an unattended
reboot. So three detached spawns could never start on the auto-start path, even
though the identical spawn works fine from a plain shell or the Desktop app.
plugins/platforms/whatsapp/adapter.py— bridgegateway/slash_commands.py—/updatehermes_cli/web_server_gateway.py— dashboard actionsThe asymmetry is the tell: the same code path works when the gateway is started
by hand, and fails only when it is started by the mechanism that exists to keep
it running.
Repro
Register a throwaway scheduled task with the same principal and have it attempt
both variants from inside a real Task Scheduler job:
Bridge symptom on a real install:
retried every 60–300s, identical each time;
curl 127.0.0.1:3000/healthneveranswers.
Fix
Add
popen_detached_with_breakaway_fallback()tohermes_cli/_subprocess_compat.pyand route all three sites through it, mirroring the fallback that already exists
in
gateway_windows._spawn_detached: retry once withwindows_detach_flags_without_breakaway(), which drops only that bit.POSIX is unaffected — there is no breakaway concept there, so the original error
propagates unmasked.
hermes_cli/gateway.py::_spawn_detached_gatewayis deliberately left alone: itis only reachable from the macOS-26 launchd fallback, where
windows_detach_popen_kwargs()returnsstart_new_sessionand the breakawaybit is irrelevant. Fixing an unreachable-on-Windows path would be noise.
Test Plan
pytestacrosstest_windows_native_support.py,test_gateway_windows.py,test_windows_gateway_job_teardown_48820.py,test_whatsapp_connect.py,test_75349_whatsapp_multiplex_secret_scope.py,test_whatsapp_bridge_pidfile.py,test_whatsapp_bridge_dir_resolution.py,test_whatsapp_from_owner.py→ 93 passed, 5 skipped, 1 pre-existing failure
New tests fail against the unfixed helper and pass against the fixed one
(RED → GREEN, verified by reverting the retry branch)
ruff checkclean on all five changed filesLive verification on a Windows install: a gateway restarted by its own
scheduled task now logs
The single failure (
TestWindowlessGatewayRestartSpec::test_noop_on_non_windows)is unrelated and pre-existing on Windows: it asserts a POSIX default
(
cwd == "") and receives a Windows temp directory. It fails identically withthese changes stashed, so it is not a regression from this PR.