Repository navigation
Conversation
An app that runs agents without a socket per agent could not say they were online: presence (kind 20001) is WebSocket-only and stored for the connection's own key, so app-hosted agents always read as offline. Kind 20003 is one owner-signed heartbeat for many agents. Content is the status, each `p` tag names an agent (at most 256). Over WebSocket only, the relay keeps the agents the signer owns (users.agent_owner_pubkey, through the existing owner cache, with at most one query per event) and sets or clears their presence in one Redis round trip, exactly as each agent's own kind 20001 would, so the presence snapshot reports them unchanged. It is not fanned out: live presence is matched by author, and the author is the owner. A new kind, rather than p tags on 20001, keeps relays without this from storing the batch as the owner's own presence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
🔐 Codex Security Review
Review SummaryOverall Risk: MEDIUM
Findings[MEDIUM]
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fab3fddb0b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let stored = if status == "offline" { | ||
| state.pubsub.clear_presence_many(tenant, &owned).await | ||
| } else { | ||
| state.pubsub.set_presence_many(tenant, &owned, status).await | ||
| }; |
There was a problem hiding this comment.
Fence stale presence batches before writing
Consecutive kind:20003 events are handled by independently spawned tasks (connection.rs:1244-1247), so an older online batch delayed by this ownership lookup can reach Redis after a newer offline batch has cleared the same agent. The resulting stale online lease survives for up to 180 seconds and can incorrectly block starting another agent instance; serialize updates per agent or use an ordering/generation fence before mutating Redis.
AGENTS.md reference: AGENTS.md:L222-L225
Useful? React with 👍 / 👎.
| let linked = state | ||
| .db | ||
| .list_agents_for_owner(community, &owner_bytes) | ||
| .await |
There was a problem hiding this comment.
Restrict ownership lookup to the requested agents
When any submitted agent is absent from the positive cache, this fetches every agent owned by the signer even though the event can name at most 256. An owner with a large roster—or a client repeatedly including one unowned key, which is deliberately never cached—can therefore force the relay to return and scan the full roster on every admitted heartbeat; query only the requested pubkeys (or otherwise impose a hard bound) instead.
AGENTS.md reference: AGENTS.md:L240-L248
Useful? React with 👍 / 👎.
| let mut pipe = redis::pipe(); | ||
| for pubkey in pubkeys { | ||
| pipe.cmd("SET") |
There was a problem hiding this comment.
Make the batched presence update atomic
A normal Redis pipeline is only a round-trip optimization, not an atomic transaction. If the connection fails after Redis processes a prefix of these SET commands, query_async returns an error and the relay rejects the heartbeat even though some named agents were updated and others were not; use an atomic pipeline or Lua operation so this single batch cannot leave a torn presence snapshot.
AGENTS.md reference: AGENTS.md:L250-L255
Useful? React with 👍 / 👎.
Ask Postgres about just the event's uncached agents rather than the owner's whole roster, and set a batch's presence in one MULTI/EXEC so a dropped connection cannot leave it half applied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
Summary
App-hosted agents (buzz-app agents2) run inside the desktop app without a socket per agent, so they can't send their own kind 20001 presence and always read as offline — including to their owner on another device.
This adds kind 20003, an owner's presence for its agents:
contentis the status (online/away/offline); eachptag names an agent (1–256 distinct).users.agent_owner_pubkey) — cachedtruefromobserver_owner_cache, the rest resolved with onelist_agents_for_ownerquery. Only positive links are cached, so a just-linked agent isn't hidden for the cache TTL and the observer path isn't poisoned.A new kind rather than
ptags on 20001, so a relay without this ignores the batch instead of storing it as the owner's own presence.Testing
agent_presence_request_names_distinct_agents_with_a_known_status(unit).presence_storage_postgres_tests::agent_presence_stores_only_owned_agents(Postgres + Redis): only the owner's agent goes online, another owner's agent doesn't, a pre-cachedfalsedoesn't hide a linked agent, andofflineclears it. All 5 presence Postgres tests pass viascripts/postgres-test-run.sh -p buzz-relay --lib -E 'test(/presence/)'.buzz-pubsubignored Redis testtest_presence_many_sets_with_ttl_and_clearspasses.cargo testfailures locally were environmental only (buzz-media "role buzz does not exist", mesh_demo 504).App side: block/buzz-app#862.
🤖 Generated with Claude Code