Skip to content

fix(acp): wait for the relay to accept offline presence before closing - #8385

Draft
anthony-tarantini wants to merge 2 commits into
block:mainfrom
anthony-tarantini:fix/acp-offline-presence-ack
Draft

anthony-tarantini wants to merge 2 commits into
block:mainfrom
anthony-tarantini:fix/acp-offline-presence-ack

Conversation

@anthony-tarantini

@anthony-tarantini anthony-tarantini commented Oct 16, 2026 •

Copy link
Copy Markdown

Summary

After !shutdown, other clients keep showing a remote agent online until they re-query presence. The agent publishes offline, but the relay never delivers it.

buzz-acp queued the offline presence event and closed the socket about 1 ms later. The relay handles each EVENT on its own task, which checks the sender's auth when it runs. The close is processed first and ends the session, so the presence is refused as unauthenticated and never fanned out. The relay then deletes the agent's presence on disconnect without telling any client. On a live relay, the pod logged "presence set to offline" 0.7 ms before exiting, and the desktop never received an update.

This adds RelayEventPublisher::publish_event_acked, which resolves only on the relay's OK for that event id. Shutdown sends offline through it within the existing 2 s limit, and only then closes the socket. Online heartbeats are unchanged.

  • If the event can't be sent (no connection), or the connection drops before its OK, the waiter is dropped, and the caller sees non-delivery at once rather than success or a 2 s stall.
  • It is sent even while rate-gated: the caller waits on that one event, and a gated relay still answers with OK.
  • An auth… refusal reaches the waiter before the existing auth branch returns early to reconnect.
  • Waiters that gave up are pruned on each insert.
  • The log now tells "presence set to offline" (accepted) apart from "relay refused offline presence: …".

Presence can't go over submit_event instead: the HTTP bridge rejects kind:20001.

Related issue

Part of #4730 and #7724; #8386 is the desktop half. Possibly related: #7946 (a remote agent stays green in the DM sidebar after it goes offline).

Testing

  • New tests drive the real background-task code against a test WebSocket:
    • acked_publish_resolves_only_on_its_own_ok
    • acked_publish_reports_an_auth_refusal
    • acked_publish_without_a_socket_reports_non_delivery
    • acked_publish_reports_non_delivery_when_the_connection_drops (through wait_for_reconnect)
    • acked_publish_is_sent_while_rate_gated
  • offline_presence_waits_for_the_relay_verdict covers the shutdown path's publish, through RelayEventPublisher::test_pair.
  • Removing any of these fixes (the OK resolution, its order before the auth branch, clearing waiters on reconnect, sending through the gate) fails a test.
  • Clippy (-D warnings) is clean, and all buzz-acp tests pass.
  • Live, against a self-hosted relay with this buzz-acp in the agent image: after !shutdown the pod logs "presence set to offline" about 5 ms later, once the relay accepts it, and the desktop shows offline within seconds without a refresh. Before, it stayed online until a refresh.

Manual test: deploy a remote agent, watch it from another client, and send !shutdown. It should go offline within a couple of seconds, and the agent log should end with "presence set to offline".

Not in this PR

The relay clears presence on disconnect without a live update, so agents that crash or are killed still look online until clients re-query. That belongs in the relay.

On shutdown the harness queued the `offline` presence event and closed
the socket ~1ms later. The relay handles each EVENT on a spawned task
that snapshots the connection's auth when it runs; the close frame
terminalizes auth first, so the presence was refused as unauthenticated
and never fanned out. The relay then cleared the Redis entry on
disconnect without any live event, leaving watchers (the desktop's
Shutdown/Deploy control) showing the agent online until they re-queried.

Add `RelayEventPublisher::publish_event_acked`, which resolves only on
the relay's OK for that event id. A rate-gated, unsent, or disconnected
publish drops the waiter so callers observe non-delivery, never success,
and an `auth…` refusal reaches the waiter before the reconnect branch.
Shutdown publishes offline through it under the existing 2s timeout and
only then closes the socket; online heartbeats stay fire-and-forget.

Signed-off-by: Tony <tony@tarantini.io>
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 93ffc37cbdb7ced6a07eca11744931cdffb82e20...3247ee82621e22b39a240e7770b1abb11cc7bb2a.
A new review must complete for this exact range. When manual authorization
is required, a user with write access must comment exactly
@buzz-security-review 3247ee82621e22b39a240e7770b1abb11cc7bb2a to authorize a new review.
Any previous review applies only to its recorded range.

…ough the rate gate

Entering reconnect drops pending acknowledged-publish waiters, since the
socket that carried them can no longer deliver an OK. The acknowledged
publish is sent even while rate-gated: its caller waits on that one event,
and a gated relay still answers with OK. Covers the shutdown path's switch
to the acknowledged publish with a test through RelayEventPublisher::test_pair.

Signed-off-by: Tony <tony@tarantini.io>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant