mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
fix(relay): accept is_reconnect kwarg in RelayAdapter.connect (#52911)
The gateway reconnect watcher (gateway/run.py) recovers a platform after a
fatal adapter error by building a fresh adapter and calling
connect(is_reconnect=True). Every BasePlatformAdapter implements
connect(*, is_reconnect: bool = False) for this — except RelayAdapter, whose
connect() was bare. So the watcher's recovery path raised:
TypeError: connect() got an unexpected keyword argument 'is_reconnect'
Observed live on a hosted staging agent: after a fatal relay adapter error the
watcher could never re-establish relay, so the shared-bot inbound never reached
the gateway and Discord DMs stopped (dashboard surfaced the TypeError).
Relay deliberately ignores the flag: the #46621 server-side-queue-preservation
concern doesn't apply, because relay's outage buffer is the connector's durable
buffer (replayed on the transport's re-handshake), not a gateway-side queue the
adapter owns. Routine WS drops are already handled by the transport's own
reconnect supervisor (WebSocketRelayTransport, reconnect=True); the watcher path
is fatal-error recovery, and the fatal handler disconnect()s the old adapter
(cancelling its supervisor) before a fresh adapter+transport is built, so there
is no double-dial.
Adds two regression tests (both proven red without the fix): connect(is_reconnect=True)
reaches the same transport-less RuntimeError instead of TypeError, and the
signature matches BasePlatformAdapter.connect.
This commit is contained in:
parent
0f81b0d458
commit
cb7d1f68f8
2 changed files with 52 additions and 1 deletions
|
|
@ -106,7 +106,24 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
return self.descriptor.supports_draft_streaming
|
||||
|
||||
# ── abstract methods (delegated to the transport) ────────────────────
|
||||
async def connect(self) -> bool:
|
||||
async def connect(self, *, is_reconnect: bool = False) -> bool:
|
||||
# ``is_reconnect`` is part of the BasePlatformAdapter.connect contract:
|
||||
# the gateway's reconnect watcher (gateway/run.py) re-establishes a
|
||||
# platform after a fatal adapter error by building a fresh adapter and
|
||||
# calling ``connect(is_reconnect=True)``. Relay MUST accept the kwarg or
|
||||
# that recovery path raises TypeError and the relay platform can never
|
||||
# come back through the watcher.
|
||||
#
|
||||
# Relay deliberately IGNORES the flag. The flag exists so adapters with a
|
||||
# server-side update queue (e.g. Telegram's Bot API) preserve that queue
|
||||
# across an outage instead of dropping it (#46621). Relay has no such
|
||||
# gateway-side queue: messages buffered during a gap live in the
|
||||
# CONNECTOR's durable buffer and are replayed when the transport
|
||||
# re-handshakes. Routine WS drops are handled entirely by the transport's
|
||||
# own reconnect supervisor (WebSocketRelayTransport, reconnect=True);
|
||||
# a watcher-driven reconnect builds a fresh transport from scratch (the
|
||||
# fatal-error handler disconnect()s the old adapter first, cancelling its
|
||||
# supervisor), so there is nothing at the adapter layer to preserve.
|
||||
if self._transport is None:
|
||||
raise RuntimeError("RelayAdapter has no transport configured")
|
||||
self._transport.set_inbound_handler(self._on_inbound)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,40 @@ async def test_connect_without_transport_raises():
|
|||
await a.connect()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_accepts_is_reconnect_kwarg():
|
||||
"""Regression: RelayAdapter.connect must accept the BasePlatformAdapter
|
||||
contract's ``is_reconnect`` kwarg. The gateway reconnect watcher recovers a
|
||||
platform after a fatal adapter error by calling ``connect(is_reconnect=True)``
|
||||
(gateway/run.py); before the fix, RelayAdapter.connect was bare ``connect()``
|
||||
and that recovery path raised ``TypeError: connect() got an unexpected
|
||||
keyword argument 'is_reconnect'`` (observed live: relay never reconnected,
|
||||
no DMs). It must reach the SAME transport-less RuntimeError as connect() —
|
||||
i.e. accept the kwarg, never TypeError on it."""
|
||||
a = _adapter()
|
||||
with pytest.raises(RuntimeError, match="no transport"):
|
||||
await a.connect(is_reconnect=True)
|
||||
|
||||
|
||||
def test_connect_signature_matches_base_contract():
|
||||
"""The is_reconnect parameter must be keyword-accepting and default False,
|
||||
matching BasePlatformAdapter.connect, so the reconnect watcher's
|
||||
``connect(is_reconnect=...)`` call is valid for relay as for every other
|
||||
adapter."""
|
||||
import inspect
|
||||
|
||||
from gateway.platforms.base import BasePlatformAdapter
|
||||
|
||||
sig = inspect.signature(RelayAdapter.connect)
|
||||
base_sig = inspect.signature(BasePlatformAdapter.connect)
|
||||
assert "is_reconnect" in sig.parameters
|
||||
param = sig.parameters["is_reconnect"]
|
||||
base_param = base_sig.parameters["is_reconnect"]
|
||||
# Keyword-acceptable (KEYWORD_ONLY here, matching the base) with a False default.
|
||||
assert param.kind is base_param.kind
|
||||
assert param.default is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_without_transport_returns_failure():
|
||||
a = _adapter()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue