acpmux: chat presence, part 2: viewing from _acpmux/view per author
Chat multiplayer, the daemon half: PR B, part 2 of 2. This part covers who has a chat open: viewing from _acpmux/view, per author (hq#4389 §5, §10.4).
Leo, word for word (2026-10-26): "we can still do the regular chat multiplayer" and "we can just send some codexes to do whatever".
The ask, as subsession-card relayed it: "4. Ephemeral presence, never stored in the log: _acpmux/typing {sessionId, on} (at most 2 per second, expires after 8 s); a _acpmux/presence {sessionId, participants:[{id,name,viewing,typing,self?}]} notification, and presence in the attach reply; viewing from _acpmux/view, per author. Person ids are tailnet:<login> until accounts are mapped."
Split (each lands on its own)
- #25344 (merged, 56921eba59a0): subscriptions to a peer's chat are marked
remote. - #25285 (merged, 32ef6c99cd4b): typing and presence; the
_acpmux/presencenotification;presencein the attach reply; the bounds; cleanup; the peer rules. - This PR: viewing from
_acpmux/viewper author, and the owner's visible attach. Before this,viewingwas always false.
What changed
Presence::view(chat, conn, who, visible)(hub/presence.rs). Showing sets the entry'sviewing, and the 32-person bound applies (presence.full). Hiding clears it, and the entry goes unless the person is typing.- A visible
_acpmux/attachis the owner viewing on that connection. A hidden one is not. Re-attaching hidden clears it. This is recorded under the subscriptions lock, only while the connection is open (not a Peer link, not ended). _acpmux/view {sessionId, visible, _meta.acpmux.author?}(server/view.rs):- With an author, only that person's presence changes. The connection's visibility and the chat's
unreadstay the owner's. So a link that carries other people attaches withvisible: falseand names each of them in_acpmux/view. - With no author, it is the owner: visibility and
unreadchange as before, and presence follows. - Another daemon's link makes no entry and its authored view gets
presence.peer_unsupported. That is a link served as Peer, and, after review, also one served as Web (added with the dashboard token), recognized by theclientInfo.nameevery peer link sends ininitialize(peer::CLIENT_NAME, "acpmux-peer"). Without that, such a link's visible attach would leave the owner "viewing" on the peer for as long as the link lived, after the client that sent it had gone. The name only keeps a client out of presence; it grants nothing. - On the forwarding daemon, an authored
_acpmux/viewto a chat on a peer is refused before forwarding, as typing is. - A subscription to a peer's chat is still refused (part 1's
remoteflag).
- With an author, only that person's presence changes. The connection's visibility and the chat's
ViewParams._meta(AuthorMeta),docs/acpmux-schema.json, the README "Chat presence" paragraph, the regeneratedspec/acpmux-protocol.json, and the webview's generated types.- The author rule is unchanged from part 1 and #25268. The unix socket and a peer forwarding for its own socket or app may send an author. Web, LocalApp, and a peer forwarding for Web are refused with
author.origin_refused. An author grants nothing.
Stop conditions
No new timer, observer or loop. A viewing entry goes on hide, detach or connection close (release_all closes presence under the subscriptions lock, as in part 1).
Tests
These fail on part 1's source (merged #25285; run https://github.com/manaflow-ai/cmux/actions/runs/40279602184):
presence::viewing_comes_from_view_per_author_and_leaves_unread_to_the_owner: two authors on one link; an authored view leavesshownalone; hiding is per person; two devices; close; a view needs an attachment.presence::the_attach_reply_carries_presence_and_typing_reaches_attached_clients: a visible attach is the owner viewing.a_peers_link_makes_no_presence_and_typing_through_it_is_refused: an authored view through a peer link is refused.
typing_in_a_chat_on_a_dashboard_token_peer_is_refused_before_forwarding was extended for the review finding. It covers a visible attach and an authorless view through a Web-served link, then the client closes with the link still up; B holds no participants throughout. It failed before the CLIENT_NAME fix (B held owner viewing; run https://github.com/manaflow-ai/cmux/actions/runs/40272738570) and passes now.
Green at this head (run https://github.com/manaflow-ai/cmux/actions/runs/40279602184): peer_integration 42, lib 917, hub_integration 455, remote_guard, local_app_origin and ephemeral_run pass, and clippy -D warnings and fmt are clean.
Size: +175/-38 in 11 files (11 lines of IR and 5 generated).
Review
Codex gpt-6-astra at xhigh, through acpmux, reading only the mirror. Round 1 was BLOCK: a dashboard-token (Web-served) peer link left a phantom owner viewing on the peer. Fixed and regression-tested. Round 2: SHIP at 95ef6c8d25fcea2074be3bb3ba3d86c32341e23c. This head is that patch squashed and rebased onto merged #25285, with the same changed lines.
No capture: daemon only; the webview lane draws it.
Agent session: big-red
🤖 Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.