mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(cli): a request after the gateway socket closed is reported as not sent
When the keepalive times out, the socket closes and GatewayClient's reader
ends before the dock's next poll. websockets refuses any later send on that
socket without writing it, so the frame never leaves. GatewayClient.rpc now
raises NOT_SENT once the reader has ended, instead of DISCONNECTED ("turn
outcome is unknown; do not resend"). A prompt.submit that hits this path
tells the user nothing was sent, so they know to send it again. A send that
fails while the reader is still alive stays DISCONNECTED, because the frame
may already have left.
NOT_SENT moves next to DISCONNECTED in gateway_client; gateway_connection
imports it, so gateway_queue's `str(exc) == NOT_SENT` check is unchanged.
This commit is contained in:
1 parent
2a5aa2a406
commit
7303b01fd6
3 files changed
+32
-5
No files matched your search
@@ -51,6 +51,8 @@ class GatewayClientError(ValueError):
|
||||
|
||||
DISCONNECTED = ("Gateway disconnected; turn outcome is unknown. "
|
||||
"Resume the printed session ID to check whether it completed or was interrupted; do not resend the work.")
|
||||
# A request refused before it reached any socket: nothing is in doubt, so never DISCONNECTED.
|
||||
NOT_SENT = "Gateway disconnected before the request was sent; nothing was sent."
|
||||
|
||||
|
||||
class GatewayRPCError(GatewayClientError):
|
||||
@@ -214,6 +216,10 @@ class GatewayClient:
|
||||
"""Send one request and await its answer; ``_timeout`` (seconds, never sent on the wire)
|
||||
overrides ``DEFAULT_RPC_TIMEOUT`` for a verb that legitimately takes longer (``math.inf``: no
|
||||
deadline; the answer, a hang-up or the caller's cancel ends the wait)."""
|
||||
if self.reader is not None and self.reader.done():
|
||||
# The reader ended (the socket closed, e.g. a keepalive timeout, or it gave up): no answer
|
||||
# could arrive, so nothing is sent and the caller may safely send again.
|
||||
raise GatewayClientError(NOT_SENT)
|
||||
self.sequence += 1
|
||||
rid = self.sequence
|
||||
future = asyncio.get_running_loop().create_future()
|
||||
|
||||
@@ -16,13 +16,11 @@ import queue
|
||||
import threading
|
||||
import time
|
||||
|
||||
from hermes_cli.gateway_client import (DEFAULT_RPC_TIMEOUT, DISCONNECTED, RESUME_KEY_MISMATCH, GatewayClientError,
|
||||
GatewayRPCError)
|
||||
from hermes_cli.gateway_client import (DEFAULT_RPC_TIMEOUT, DISCONNECTED, NOT_SENT, RESUME_KEY_MISMATCH,
|
||||
GatewayClientError, GatewayRPCError)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# A request refused before it reached any socket: nothing is in doubt, so never DISCONNECTED.
|
||||
NOT_SENT = "Gateway disconnected before the request was sent; nothing was sent."
|
||||
# ``session.resume`` refusals that mean "this viewer may not open that session": the caller keeps the
|
||||
# session it is on instead of adopting the target (startup resume, bare -c fallback).
|
||||
RESUME_REFUSALS = frozenset({"not_found", "permission_denied"})
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.gateway_client import GatewayClient, GatewayClientError, GatewayRPCError
|
||||
from hermes_cli.gateway_client import NOT_SENT, GatewayClient, GatewayClientError, GatewayRPCError
|
||||
|
||||
|
||||
class _Socket:
|
||||
@@ -89,3 +89,26 @@ async def test_send_on_a_dead_socket_is_a_client_error_not_a_raw_websockets_one(
|
||||
with pytest.raises(GatewayClientError, match='turn outcome is unknown'):
|
||||
await client.rpc('subagent.list', session_id='s')
|
||||
assert client.pending == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_request_after_the_reader_ended_is_not_sent():
|
||||
"""The keepalive timeout closes the socket and ends the reader before the dock's next poll: that
|
||||
send is never written (websockets refuses a send on a closed socket), so the caller is told
|
||||
nothing was sent, not that a turn's outcome is unknown."""
|
||||
sent = []
|
||||
|
||||
class _Closed(_Socket):
|
||||
async def send(self, raw):
|
||||
sent.append(raw)
|
||||
|
||||
async def __anext__(self):
|
||||
raise StopAsyncIteration # the socket closed: iteration ends
|
||||
|
||||
async with GatewayClient(_Closed(lambda req: None)) as client:
|
||||
await asyncio.sleep(0) # let the reader see the close
|
||||
assert client.reader.done()
|
||||
with pytest.raises(GatewayClientError) as refused:
|
||||
await client.rpc('prompt.submit', session_id='s')
|
||||
assert str(refused.value) == NOT_SENT
|
||||
assert sent == [] and client.pending == {}
|
||||
Reference in new issue
Block a user