acpmux: chat presence, part 2: viewing from _acpmux/view per author

Merged
TE
11d ago
+175-38

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)

  1. #25344 (merged, 56921eba59a0): subscriptions to a peer's chat are marked remote.
  2. #25285 (merged, 32ef6c99cd4b): typing and presence; the _acpmux/presence notification; presence in the attach reply; the bounds; cleanup; the peer rules.
  3. This PR: viewing from _acpmux/view per author, and the owner's visible attach. Before this, viewing was always false.

What changed

  • Presence::view(chat, conn, who, visible) (hub/presence.rs). Showing sets the entry's viewing, and the 32-person bound applies (presence.full). Hiding clears it, and the entry goes unless the person is typing.
  • A visible _acpmux/attach is 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 unread stay the owner's. So a link that carries other people attaches with visible: false and names each of them in _acpmux/view.
    • With no author, it is the owner: visibility and unread change 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 the clientInfo.name every peer link sends in initialize (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/view to 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 remote flag).
  • ViewParams._meta (AuthorMeta), docs/acpmux-schema.json, the README "Chat presence" paragraph, the regenerated spec/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 leaves shown alone; 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 Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.