Skip to content

fix(browser): bound CDP reconnect loop to prevent infinite retry on 502/503 - #44460

Open
plcunha wants to merge 2 commits into
NousResearch:mainfrom
plcunha:fix/browser-max-reconnect-attempts
Open

fix(browser): bound CDP reconnect loop to prevent infinite retry on 502/503#44460
plcunha wants to merge 2 commits into
NousResearch:mainfrom
plcunha:fix/browser-max-reconnect-attempts

Conversation

@plcunha

@plcunha plcunha commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Problem

The CDP supervisor's _run() reconnect loop had no upper bound — it retried WebSocket connections forever, even when the upstream service (Browserbase) returned HTTP 502/503 (infrastructure failures).

This caused multi-minute stalls when a browser session died:

CDP supervisor 20260611_155917: connect failed (attempt 12): server rejected WebSocket connection: HTTP 502
CDP supervisor 20260611_155917: connect failed (attempt 13): server rejected WebSocket connection: HTTP 502
... (19 attempts total)
  • 19 retries × ~11s each = ~3.5 min wasted
  • Context grew until the compressor also timed out (cascade failure)
  • User experienced 15 minutes of dead time waiting for a response

Root cause

_run() at line 614 used while not self._stop_requested with no max retries, no infrastructure failure detection, and no circuit breaker. This was designed to survive transient WebSocket drops (Browserbase tearing down CDP sockets between commands) — but it treated persistent infrastructure failures identically to transient ones.

Fix

  1. Added max_reconnect_attempts (default 10) to CDPSupervisor
  2. HTTP 502/503 treated as immediately fatal — retrying won't bring the upstream service back
  3. Reset consecutive_failures on successful connection (so transient drops still work)
  4. Log level ERROR (not WARNING) when giving up, so it's surfaced in monitoring
  5. Improved log message to show attempt count vs max (attempt 12/10 instead of just attempt 12)

Testing

Manual verification on a live instance that hit this bug:

  • Before: 19 retries, 3.5 min, cascade failure
  • After: HTTP 502 detected → immediate bailout with ERROR log
  • After: 10 consecutive transient failures → bailout with ERROR log
  • After: Successful reconnect resets counter (transient recovery still works)

@liuhao1024

Copy link
Copy Markdown
Contributor

Verification: CDP reconnect bound looks solid

Reviewed the full diff. The implementation is well-structured:

  • consecutive_failures counter correctly resets on successful reconnection
  • Exponential backoff is preserved (capped at 10s)
  • max_reconnect_attempts is configurable via constructor (defaults to 10)
  • The _run method's early-return on 502/503 prevents futile retries against infrastructure failures
  • Test coverage validates the task-cancel-before-close ordering

No issues found. The bound prevents the infinite reconnect loop documented in the PR title.

@AIalliAI

Copy link
Copy Markdown
Contributor

Verified against main (8505e9d) — bug is real; fix direction is right; the 502 detection has demonstrable false positives

Confirmed:

  • _run()'s reconnect loop (tools/browser_supervisor.py L614) is bounded only by _stop_requested. After first ready, connect failures retry forever with backoff capped at 10s, and the quoted log matches the connect failed (attempt %s) line exactly.
  • Giving up via return is systemically safe: _thread_main's finally clears _active, callers fail soft ("supervisor is not active"), and _SupervisorRegistry.get_or_start() health-checks thread.is_alive() / loop.is_running() and recreates a dead supervisor on the next browser command. So bailout self-heals rather than bricking the session.
  • On websockets 15.0.1 (what the repo runs), the HTTP-rejection message is exactly server rejected WebSocket connection: HTTP 502, so the substring does catch the reported case.

Issues:

  1. Substring matching on str(e) misfires. I ran this PR's exact predicate against realistic connect exceptions in the live venv:

    "[Errno 61] Connect call failed ('127.0.0.1', 50253)"            -> FATAL  (ephemeral port contains "502")
    "received 1011 (internal error) ...; sent 503 bytes"             -> FATAL  ("503 bytes")
    "server rejected WebSocket connection: HTTP 502"                 -> FATAL  (intended)
    

    A plain connection-refused can be classified as a fatal infrastructure failure on the first attempt. websockets exposes the structured status:

    from websockets.exceptions import InvalidStatus
    
    if isinstance(e, InvalidStatus) and e.response.status_code in (502, 503):
  2. One 502 → immediately fatal is aggressive. 502/503 from an LB/edge are often one-off blips. The registry self-heal bounds the damage (next command recreates the supervisor), but dialog watching is dead until then. Requiring 2–3 consecutive infra failures before bailing would still fix the reported 19-retry / ~3.5-min stall.

  3. attempt is now dead code — still incremented and reset, but no longer read after the log switched to consecutive_failures.

  4. (scope note) The attach-failure path (websockets.connect succeeds, _attach_initial_page raises → "session dropped" → reconnect) remains unbounded. Fine to leave out of this PR, but worth a code comment if intentional.

  5. No tests in this diff — the only file changed is tools/browser_supervisor.py (for the record, since an earlier comment mentions test coverage: this PR adds none, and the module currently has no test file). A small unit test faking websockets.connect to raise InvalidStatus(502) vs. OSError would lock in the fatal-vs-retry split.

  6. CI: the check-attribution failure just wants "jvsantos.cunha@gmail.com": "plcunha" added to AUTHOR_MAP in scripts/release.py.

@alt-glitch alt-glitch added type/bug Something isn't working tool/browser Browser automation (CDP, Playwright) P2 Medium — degraded but workaround exists labels Jun 11, 2026
AIalliAI added a commit to AIalliAI/Hermes that referenced this pull request Jun 14, 2026
check-attribution flagged emails introduced by the bugfixes rollup:
AIalliAI's three commit emails and plcunha's (cherry-picked PR NousResearch#44460).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for targeting a real stall: current main still has an unbounded post-ready reconnect path in tools/browser_supervisor.py:652-671.

Problems

  • The PR logs raw e in its new fatal and retry paths. Current main requires _redact_cdp_error_text() for every supervisor error egress (tools/browser_supervisor.py:37-53), and the existing reconnect warning uses it at :665-668. A salvage must retain that credential-redaction boundary.
  • The str(e) substring classifier is not limited to an HTTP handshake status, so unrelated exception text can be treated as fatal.
  • The counter only increments for websockets.connect() errors. _attach_initial_page() errors flow through the separate session-drop handler (tools/browser_supervisor.py:685,694-734) and would still retry indefinitely.
  • The diff contains no regression tests for these paths.

Suggested changes

  • Bound failed complete connection cycles, including attach failures, and reset only after a successful attach.
  • Preserve error redaction and classify 502/503 from handshake/status metadata rather than arbitrary message text.
  • Add focused mocked reconnect/attach tests.

This is an automated hermes-sweeper review.

@alt-glitch alt-glitch added comp/tools Tool registry, model_tools, toolsets needs-decision Awaiting maintainer decision before any implementation labels Jul 14, 2026
@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
@alt-glitch alt-glitch removed sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Jul 14, 2026
@plcunha

plcunha commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — I agree with the concerns and addressed them in 6a2a6350e.

The status-substring classifier has been removed. The reconnect budget now covers every failed reconnect cycle after initial readiness:

  • WebSocket connect failures;
  • post-connect CDP attach failures; and
  • quick post-attach reader/session drops.

A successful socket connection no longer resets the counter. The streak resets only after the session has remained attached for a 30-second stability window; the subsequent drop starts a new streak at 1. Endpoint/error redaction is preserved on all reconnect logs, and the supervisor thread's crash log now uses the same redaction helper as well.

I added regression tests for bounded attach failures, bounded quick session drops, stable-session reset behavior, positive budget validation, and secret redaction. Local verification: 25 tests passed across the reconnect-budget, health-check, and secret-exfiltration suites; the new test also passes ty, and ruff/git diff --check are clean.

Would appreciate a re-review when convenient.

@alt-glitch alt-glitch removed the needs-decision Awaiting maintainer decision before any implementation label Jul 22, 2026
The CDP supervisor's _run() reconnect loop had no upper bound —
it retried WebSocket connections forever, even when the upstream
service (Browserbase) returned HTTP 502/503 (infrastructure failures).

This caused multi-minute stalls when the browser session died:
- 19 retries × ~11s each = ~3.5 min wasted
- Context grew until compressor also timed out (cascade failure)

Changes:
- Added max_reconnect_attempts (default 10) to CDPSupervisor
- Treat HTTP 502/503/Service Unavailable/Bad Gateway as immediately fatal
- Reset consecutive_failures after successful connection
- Log level ERROR (not WARNING) when giving up
- Improved log message to show attempt count vs max
@plcunha
plcunha force-pushed the fix/browser-max-reconnect-attempts branch 2 times, most recently from 1e18717 to c088f0e Compare August 9, 2026 18:41
@plcunha
plcunha requested a review from a team August 9, 2026 18:41
@alt-glitch alt-glitch removed the comp/tools Tool registry, model_tools, toolsets label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform tool/browser Browser automation (CDP, Playwright) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants