Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ named rather than smoothed.
dependency of its own.

### Fixed
- Room-scoped spoken approvals now return the approval event's exact
`request_id` under the field name required by the Hermes run API. They no
longer fail with HTTP 400 `approval_request_required` while a pending action
waits for an answer.
- The dashboard cascade relay no longer deadlocks on the servers Hermes
actually runs on. `POST /api/plugins/hermes-talk/cascade-tts` returned
HTTP 200 and then zero bytes of PCM, followed by a `ClientDisconnect` in
Expand Down Expand Up @@ -574,8 +578,9 @@ regression tests:
single-shot upstream (a reconnect 404s); when a run's watcher dies, the
poll loop reconciles one conservative prompt (`once`/`deny`) instead of
letting the approval sit silent until the host's 300s auto-deny. Resolves
also carry the request's own id (`approvalId`) for exact routing on hosts
that support it.
also carry the request's own id for exact routing; this release sent it as
`approvalId`, while current Hermes requires `request_id` (corrected under
[Unreleased](#unreleased)).
- **Stale sidecars are quarantined by attach generation.** A delegated run
outliving its session can no longer speak its approval into — or be
resolved from — the next session.
Expand Down
4 changes: 2 additions & 2 deletions talk_apiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ def respond_to_approval(
:class:`TalkApiServerError` with speakable text on any other failure.

``approval_id`` is the request's own id from the ``approval.request``
event: a host that supports exact routing (the field is ``approvalId``
event: a host that supports exact routing (the field is ``request_id``
on the wire) resolves THAT request instead of FIFO-popping the oldest;
hosts that predate the field ignore it.

Expand All @@ -632,7 +632,7 @@ def respond_to_approval(

body: dict = {"choice": choice}
if approval_id:
body["approvalId"] = approval_id
body["request_id"] = approval_id
try:
response = httpx.post(
f"{talk_config.api_server_url()}{RUNS_PATH}/{run_id}/approval",
Expand Down
2 changes: 1 addition & 1 deletion talk_approvals.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ class _PendingApproval:
request_text: str
choices: tuple[str, ...]
#: The host's approval request id (always present on real SSE events).
#: Sent back as ``approvalId`` so a host that supports exact routing
#: Sent back as ``request_id`` so a host that supports exact routing
#: resolves THIS request instead of FIFO-popping the oldest; ``None``
#: for reconciled prompts, where the id was lost with the stream.
request_id: str | None = None
Expand Down
6 changes: 3 additions & 3 deletions tests/test_approval_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ def test_reconcile_stays_out_while_the_watcher_lives(monkeypatch):


def test_resolve_routes_the_exact_request_when_the_event_carried_one(monkeypatch):
"""Real SSE events always carry request_id; sending it back as approvalId
"""Real SSE events always carry request_id; sending it back as request_id
lets a host with exact routing resolve THIS request instead of
FIFO-popping the oldest. Hosts that predate the field ignore it."""

Expand Down Expand Up @@ -822,7 +822,7 @@ def capture(url, **kwargs):
assert seen["headers"] == {"Authorization": "Bearer k-123"}


def test_respond_to_approval_carries_the_approval_id_when_given(monkeypatch):
def test_respond_to_approval_carries_the_request_id_when_given(monkeypatch):
seen = {}

class Response:
Expand All @@ -841,7 +841,7 @@ def capture(url, **kwargs):

talk_apiserver.respond_to_approval("run_9", "once", approval_id="req-77")

assert seen["json"] == {"choice": "once", "approvalId": "req-77"}
assert seen["json"] == {"choice": "once", "request_id": "req-77"}


def test_respond_to_approval_409_is_the_gone_verdict(monkeypatch):
Expand Down