Skip to content

fix(whatsapp): fall back when the parent job forbids CREATE_BREAKAWAY_FROM_JOB - #132463

Open
ConTresillo wants to merge 1 commit into
NousResearch:mainfrom
ConTresillo:fix/whatsapp-bridge-breakaway-fallback
Open

ConTresillo wants to merge 1 commit into
NousResearch:mainfrom
ConTresillo:fix/whatsapp-bridge-breakaway-fallback

Conversation

@ConTresillo

Copy link
Copy Markdown

Problem

On Windows, the WhatsApp (Baileys) bridge could never start when the gateway was launched from Hermes Desktop or from Task Scheduler. The platform showed permanently "Disconnected" and every reconnect attempt failed:

PermissionError: [WinError 5] Access is denied
  ... hermes_plugins.platforms__whatsapp.adapter: [Whatsapp] Failed to start bridge: [WinError 5] Access is denied
INFO gateway.run: Reconnect whatsapp failed, next retry in 300s

Cause

The bridge is spawned with windows_detach_popen_kwargs(), whose default Windows flags include CREATE_BREAKAWAY_FROM_JOB. Per MSDN:

If the calling process is associated with a job, the job must set the JOB_OBJECT_LIMIT_BREAKAWAY_OK limit.
— CreateProcessAsUser fails with error 5 (Access Denied) when using Jobs

Electron (Hermes Desktop) and Task Scheduler both wrap their children in job objects that do not grant breakaway, so CreateProcess is rejected with ERROR_ACCESS_DENIED every time. I confirmed this against a live gateway process: querying its job object returned LimitFlags=0x0, i.e. JOB_OBJECT_LIMIT_BREAKAWAY_OK unset. Spawning the same node bridge.js ... command outside that job succeeded, and node itself was fine — only the flag was rejected.

_subprocess_compat.windows_detach_flags() documents that "a job that forbids breakaway yields PermissionError from Popen — callers catch OSError and fall back to windows_detach_flags_without_breakaway", and that helper already exists. This caller was the one place not doing it.

Fix

Catch PermissionError on the bridge spawn and retry with windows_detach_flags_without_breakaway(), i.e. the same command without CREATE_BREAKAWAY_FROM_JOB. This is the documented fallback; no new helper is introduced.

Verification

On Windows 10, gateway restarted via Hermes_Gateway (Task Scheduler), log:

WARNING [Whatsapp] Bridge spawn denied by parent job (breakaway forbidden); retrying without CREATE_BREAKAWAY_FROM_JOB
INFO gateway.run: ✓ whatsapp connected

bridge.log: ✅ WhatsApp connected! — inbound and outbound messages flowing.

Tradeoff

Without breakaway the bridge stays in the gateway's job, so if the gateway is killed the bridge dies with it instead of surviving. For a long-running gateway this is not meaningful, and the alternative is WhatsApp never connecting at all on Desktop/Task Scheduler launches.

Scope note

This is not WhatsApp-specific. windows_detach_popen_kwargs() is the default detach path repo-wide, so any other adapter that spawns a helper process under Electron or Task Scheduler will hit the same wall. This PR fixes the WhatsApp adapter only; a repo-wide sweep of other subprocess.Popen(..., **windows_detach_popen_kwargs()) call sites may be worth a separate pass.

🤖 Generated with Claude Code

…_FROM_JOB

The Baileys bridge is spawned with windows_detach_popen_kwargs(), whose
default Windows flags include CREATE_BREAKAWAY_FROM_JOB. Per MSDN, that
flag is only valid when the parent's job object sets
JOB_OBJECT_LIMIT_BREAKAWAY_OK; otherwise CreateProcess fails with
ERROR_ACCESS_DENIED (WinError 5).

Electron (Hermes Desktop) and Task Scheduler both wrap children in jobs
that do not grant breakaway, so on Windows the bridge could never start
from either launch path and WhatsApp stayed permanently "Disconnected".
Observed on Windows 10 with Hermes_Gateway (Task Scheduler) and a
Desktop-initiated restart; the live gateway's job reported
LimitFlags=0x0.

Catch PermissionError and retry with
windows_detach_flags_without_breakaway() — the fallback already
documented in _subprocess_compat.windows_detach_flags but not used by
this caller. The retry keeps the bridge in the gateway's job, so it now
dies with the gateway instead of outliving it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have duplicate This issue or pull request already exists comp/plugins Plugin system and bundled plugins platform/whatsapp WhatsApp Business adapter platform/windows sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Oct 3, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

This appears to duplicate #87777, which already implements the same no-breakaway retry and includes regression coverage.

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

Fix direction is right, and I confirmed the premise rather than assuming it: CreateProcess rejecting CREATE_BREAKAWAY_FROM_JOB does surface as PermissionError (measured: winerror=5, errno=13), so the except isn't dead code. Two changes before merge.

1. Narrow the except — PermissionError alone mis-attributes a real failure mode.

hermes_cli/main_desktop.py:1961-1973 already does this fallback and deliberately guards it: if getattr(exc, "winerror", None) != 5: raise, commented "Re-raise every other spawn failure (bad argv/env, missing exe) so it stays a clear, single error instead of being masked by a doomed second attempt."

That guard applies to you. env=self._bridge_env() is evaluated inside your try, and _bridge_env() ends in _cache_dirs() → get_image_cache_dir() → _secure_media_cache_dir() (gateway/platforms/base.py), whose final statement is an unguarded cache_dir.mkdir(parents=True, exist_ok=True) after its outer except Exception. On a read-only or ACL-restricted cache dir that raises PermissionError.

So: unwritable cache dir → _bridge_env() raises → your handler logs "Bridge spawn denied by parent job" → calls _bridge_env() again → same exception propagates. A filesystem permission problem gets logged as a job-object problem, and the retry can't succeed. Nothing crashes (connect()'s outer except Exception catches it), but debugging gets pointed the wrong way:

except PermissionError as exc:
    if getattr(exc, "winerror", None) != 5:
        raise

2. No regression test — and this branch is invisible to CI.

tests/tools/test_windows_native_support.py:546 pins this behaviour well: two spawns, identical argv, same env object, retry drops only 0x01000000. Without an equivalent, narrowing the except, deleting it, or hardcoding the flags all stay green. Your tradeoff — bridge now dies with the gateway — also has no assertion protecting it.

Minor: windows_detach_flags()'s docstring says "callers catch OSError". Nearest unfixed sibling: gateway/slash_commands.py:140 has no try and runs inside the gateway process where you measured LimitFlags=0x0, so /update fails identically under Desktop. On the duplicate (#87777) I'd keep yours — older base, no tests.

Verified against base 8ffb3c65 (blob confirmed). Static review only; no tests run.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins duplicate This issue or pull request already exists os/windows Native Windows behaviour or breakage: PowerShell install, Windows paths, cmd.exe, WSL-vs-native diff P3 Low — cosmetic, nice to have platform/whatsapp WhatsApp Business adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants