Skip to content

fix(gateway): spawn platform reconnect watcher with task-level supervision - #71867

Closed
ygd58 wants to merge 1 commit into
NousResearch:mainfrom
ygd58:fix/gateway-fatal-adapter-revive-probe
Closed

fix(gateway): spawn platform reconnect watcher with task-level supervision#71867
ygd58 wants to merge 1 commit into
NousResearch:mainfrom
ygd58:fix/gateway-fatal-adapter-revive-probe

Conversation

@ygd58

@ygd58 ygd58 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Context

Fixes #71758.

Problem

A platform adapter that dies on a transient upstream failure (marked retryable=True) is correctly queued for background reconnection. But the reconnect watcher task itself was spawned via a bare asyncio.create_task -- if an exception ever escaped its outer while-loop, the watcher died silently: no log, no restart.

_ensure_reconnect_watcher_running() already existed to respawn a dead watcher, but it's only called when a NEW platform's fatal error arrives. If the watcher dies while a platform is already queued and no other platform ever fails afterward, nothing notices -- matching the reported symptom of a platform staying dead for 17.5h after a transient outage recovered.

Fix

Spawn the reconnect watcher via the existing _spawn_supervised() task-level supervisor (already used for other long-lived background tasks) instead of a bare asyncio.create_task, at both the initial startup spawn and the manual-respawn path. This catches and logs any exception escaping the task, and auto-restarts with capped exponential backoff -- self-healing independent of any new fatal-error event.

Also hardened a related race: a platform removed from _failed_platforms concurrently between the watcher's keys-snapshot and its lookup could raise an uncaught KeyError. Changed to .get() with a skip-if-missing guard.

Verification

6 new tests pass (spawn paths use _spawn_supervised, the core self-heals-without-new-fatal-error regression, and the race-guard scenario); 52/52 in the full tests/gateway/test_platform_reconnect.py file (no regression to existing respawn behavior).

…ision

Fixes NousResearch#71758.

A platform adapter that dies on a transient upstream failure (marked
retryable=True, e.g. photon's sidecar exiting when its upstream
gRPC/CDN returns errors) is correctly queued into _failed_platforms
for background reconnection. But the reconnect watcher task itself
was spawned via a bare asyncio.create_task -- if an exception ever
escaped its OUTER while-loop (not just the per-platform inner
try/except), the watcher died silently: no log, no restart.

_ensure_reconnect_watcher_running() already existed to respawn a dead
watcher, but it's only called from _handle_adapter_fatal_error_impl()
when a NEW platform's fatal error arrives. If the watcher dies while a
platform is already sitting in the queue and no OTHER platform ever
fails afterward, nothing ever notices the watcher is dead -- exactly
matching the reported symptom: photon queued for reconnect, the
gateway itself healthy (other platforms kept working, so nothing
re-triggered the ensure-alive check), and the platform stayed dead for
17.5h until a manual restart, well after the transient upstream outage
had recovered.

Fix: spawn the reconnect watcher via the existing _spawn_supervised()
task-level supervisor (already used for kanban_dispatcher_watcher,
handoff_watcher, etc.) instead of a bare asyncio.create_task, at both
the initial startup spawn and the manual-respawn path in
_ensure_reconnect_watcher_running(). _spawn_supervised already
provides exactly what's missing here: catches and logs any exception
escaping the task, and auto-restarts with capped exponential backoff
(healthy-run counter resets so a daemon that crashes occasionally over
days is never permanently abandoned) -- self-healing independent of
any new fatal-error event.

Also hardened a related race: the watcher's per-platform loop looked
up self._failed_platforms[platform] via direct indexing after
snapshotting the keys with list(...). A platform removed concurrently
between the snapshot and the lookup (e.g. a manual /platform resume,
or a reconnect that succeeded via a different path) would raise an
uncaught KeyError -- exactly the class of bug this fix's supervision
now catches, but avoiding the crash-and-restart cycle entirely is
better than relying on it. Changed to .get() with a skip-if-missing
guard.

6 new tests pass (initial spawn uses _spawn_supervised, manual respawn
uses _spawn_supervised, the core regression -- watcher self-heals
after an uncaught exception with no new fatal-error event -- and the
race-guard scenario); 52/52 in the full
tests/gateway/test_platform_reconnect.py file (including the 4
pre-existing _ensure_reconnect_watcher_running tests, confirming no
regression to that respawn-when-dead-or-missing behavior).
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #71177: both repair reconnect-watcher liveness. This focused patch additionally guards the failed-platform snapshot/lookup race; please choose or consolidate the overlapping watcher-supervision changes.

teknium1 added a commit that referenced this pull request Jul 27, 2026
…er a supervised respawn

Follow-up to the salvaged #71867 supervision fix. _spawn_supervised's own
backoff respawn created a new task without updating the external handle
self._reconnect_watcher_task, so after the reconnect watcher crashed and
self-restarted, _ensure_reconnect_watcher_running() saw the stale handle as
done() and spawned a SECOND concurrent watcher (double reconnect attempts).

Add an optional on_spawn callback to _spawn_supervised, fired with the live
task on every spawn INCLUDING internal respawns, and pass it at both reconnect-
watcher spawn sites so the tracked handle always advances. The two supervision
mechanisms (supervisor auto-restart + ensure-respawn) now compose instead of
racing. Regression test sabotage-verified.
teknium1 added a commit that referenced this pull request Jul 27, 2026
…er a supervised respawn

Follow-up to the salvaged #71867 supervision fix. _spawn_supervised's own
backoff respawn created a new task without updating the external handle
self._reconnect_watcher_task, so after the reconnect watcher crashed and
self-restarted, _ensure_reconnect_watcher_running() saw the stale handle as
done() and spawned a SECOND concurrent watcher (double reconnect attempts).

Add an optional on_spawn callback to _spawn_supervised, fired with the live
task on every spawn INCLUDING internal respawns, and pass it at both reconnect-
watcher spawn sites so the tracked handle always advances. The two supervision
mechanisms (supervisor auto-restart + ensure-respawn) now compose instead of
racing. Regression test sabotage-verified.
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #72366 with your commit cherry-picked onto current main — authorship preserved. The reconnect watcher is spawned with task-level supervision again, plus a follow-up fix so a supervised respawn keeps _reconnect_watcher_task pointing at the live task (prevents a duplicate watcher). Closes #71758. Thanks!

@teknium1 teknium1 closed this Jul 27, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…er a supervised respawn

Follow-up to the salvaged NousResearch#71867 supervision fix. _spawn_supervised's own
backoff respawn created a new task without updating the external handle
self._reconnect_watcher_task, so after the reconnect watcher crashed and
self-restarted, _ensure_reconnect_watcher_running() saw the stale handle as
done() and spawned a SECOND concurrent watcher (double reconnect attempts).

Add an optional on_spawn callback to _spawn_supervised, fired with the live
task on every spawn INCLUDING internal respawns, and pass it at both reconnect-
watcher spawn sites so the tracked handle always advances. The two supervision
mechanisms (supervisor auto-restart + ensure-respawn) now compose instead of
racing. Regression test sabotage-verified.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation P1 High — major feature broken, no workaround sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gateway marks a crashed platform adapter fatal and never retries — transient upstream outage permanently kills the platform

3 participants