Repository navigation
cmux daemon: bridge a link share to its hosted room, 3/3: the guest relay (cmux.share/1) - #26849
Conversation
|
cmux-ci receipt at head
|
|
Bead |
|
Found 2 test failures on Blacksmith runners: Failures
|
43ea811 to
7453a62
Compare
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
web and size checks: green Shipped in:
|
7453a62 to
f5b6c63
Compare
|
Rebased on #26827's new head, now at cmux-ci run 40682369154, exit 0: |
|
Owned by an agent session · took over from an earlier session · session |
For each guest whose browser is in the room (the room's hello or
guest_joined), the bridge opens a same-user connection to this daemon,
served by the real connection path, binds it with
agent-session-proxy-bind {link_guest} on its first line and attaches, so
every frame is the bound guest's and passes the per-frame checks.
To the room, naming that guest only: a snapshot first (the newest
records within 512 KiB, has_more, watermark, chat, the guest's prompt
states), then each live record; older -> page; guest_prompt ->
agent-session-prompt with its client_id, and prompt_state from the reply
and from acpmux's records (queued awaiting the owner -> held,
queue_approved, queue_declined, queue_removed); prompt_status -> one
prompt_state per prompt the relay knows; guest_typing -> typing. A
record over 256 KiB goes cut (truncated, full_bytes); 16 held prompts at
most (share.too_many_held).
guest_left (its last socket), a kick, a revoke or expiry ends the relay;
a guest or host reconnect starts it again with a fresh snapshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-c3300437b0af7f1bba36
f5b6c63 to
d63a8f1
Compare
|
Review (codex gpt-6-astra high fast, review-router; started by the backlog lander): BLOCK at d63a8f1 BLOCK
|
… frames, and outlives an uncounted socket - The room's asks that come before the relay's attachment answers wait for it (up to 64), instead of being refused as not attached and lost. - Each relay frame carries its guest and generation; the host socket drops frames of a relay a reconnect replaced, so no stale record or prompt state follows the fresh snapshot. - A host hello names each guest once, not its sockets: such a relay no longer ends on the first guest_left, but with the guest's kick, the share's end or the next hello. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-710a4a518c009a6ad126
|
Review (codex gpt-6-astra high fast, review-router; started by the backlog lander): BLOCK at 81ad50d BLOCK
|
…t it cannot keep - Each guest relay's daemon connection claims one of the daemon's connection permits like any client; with none free no relay starts, and the guest's next join asks again. - An ask past the 64 kept before the attachment answers is no longer dropped silently: a prompt gets prompt_state refused (share.busy), older gets an empty page that still has more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-710a4a518c009a6ad126
|
Review (codex gpt-6.1-sol high fast, review-router; started by the backlog lander): BLOCK at 02dbe44 BLOCK
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review (codex gpt-6.1-sol high fast, review-router; started by the backlog lander): BLOCK at 19c5585 BLOCK
|
|
Awareness note from an agent lane that does not own this PR; it is not a review or a request to take the PR over. This PR's branch refuses pushes from the lane carrying it now, so its commits plus the review-fix commit |
1 similar comment
|
Awareness note from an agent lane that does not own this PR; it is not a review or a request to take the PR over. This PR's branch refuses pushes from the lane carrying it now, so its commits plus the review-fix commit |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review (codex gpt-6.1-sol high fast, review-router; started by the backlog lander): BLOCK at f81fd0a BLOCK
|
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
This PR's own diff is its last commit,
87588f32559(+696/-13). It sits on #26827 (the host socket), which sits on #26825 (provisioning).Leo (2026-10-28), on Theo's T3 Code "Share with guest": "we want to be having, be able to do this kind of flow for CMux multiplayer ... we can work on the agent chat multiplayer." A guest opens a link in any browser, joins with a name, watches live, and each message waits for the owner.
Tracks manaflow-ai/cmuxterm-hq#4766. Spec: chat-multiplayer.md 4.3, which covers the frame table, recipients, ordering and sizes. The room is manaflow-ai/cmux-platform#6 and #8.
What it does
The bridge starts a relay for each guest whose browser is in the room, from the room's
helloorguest_joined. The relay opens a same-user connection to this daemon, a socket pair served by the real connection path (handle_connection_with_permit). Its first line binds it withagent-session-proxy-bind {link_guest}, then it attaches. Every frame is therefore the bound guest's, and the daemon's per-frame checks apply:What the relay sends the room names only that guest:
snapshot {to, chat {title, harness, status}, records, watermark, has_more, prompts}. It holds the newest records that fit in 512 KiB,has_morewhen older ones were left out, and the guest's held prompts (at most 16) plus its 16 most recently decided.records {to: [guest], records}older {before_seq}agent-session-events→page {to, records, has_more}, also at most 512 KiBguest_prompt {client_id, text}agent-session-promptwith theclient_id(the daemon's deterministicg-prompt id), thenprompt_stateprompt_state:queuedwithawaitingOwner→held;queue_approved→approved;queue_declined→declined;queue_removed/prompt_retracted→removed; a refused prompt →refusedwith its error codeprompt_status {client_ids}prompt_statefor each prompt the relay knowsguest_typing {on}agent-session-typingtruncated: true, full_bytes. A 17th held prompt is refused withshare.too_many_held.guest_leftfor the guest's last socket ends it, and so do a kick, a revoke or expiry (the bound connection closes, so the room never hears more of that guest). The kick andendframes to the room are cmux daemon: bridge a link share to its hosted room, 2/2: the host socket (cmux.share/1) #26827's.guest_joinedagain) starts a new relay with a fresh snapshot. A host reconnect does the same for every guest thehellonames, after the daemon'sguests.agent-session-view {visible: true}after its attach, so the owner's presence shows the guest viewing.Known limits
presence {to}is not relayed. The daemon does not give a bound guest acpmux's presence set; it says who else has the chat open (stream.rs: "Not to a teammate yet"). Relaying it needs that policy changed for guests, with what a guest may see of the others decided first. That belongs in its own PR.prompt_statuscoverage: a prompt whose last state record is older than the relay's snapshot and later pages gets noprompt_statefromprompt_status. The relay knows states only from records it has seen; it does not query acpmux's prompt ledger.awaitingOwner,queue_approved,queue_declined), which arrive with the acpmux held-prompts PR. Until then a guest prompt is reportedheldonce acpmux accepts it.PUTmade without answering lives until the share's expiry, because the owner token itsDELETEwould need is never kept.Risk
risk:normal: daemon-only. It is inert until a share is provisioned with an owner token (the share sheet / op PR), and only the room's development environment is live. The relay reaches the chat only through a bound guest connection, so it adds no new path to the agent or the machine.Tests
These use the fake room from #26825/#26827 and the existing fake acpmux.
has_more, and sends seq 12 cut (truncated,full_bytes, nomsg). The watermark is 13.guest_promptreaches acpmux as hers, with theg-id and approvalowner, and she getsprompt_state held. After acpmux'squeued {awaitingOwner}andqueue_approvedrecords, she getsprompt_state approved(held is not said twice).prompt_statusanswers for the known prompt only, andoldergets apage.guestscomes first, then a snapshot for each connected guest.prompt_state.CI receipts are in a comment below.
Changelog
none
Agent session: agent session
Room contract (cmux-platform
src/server/ChatShareRoom.ts)hellonames each guest with an open socket once; with manaflow-ai/cmux-platform#147 it also givessockets(their count). Each guest socket then reportsguest_joined/guest_left. Without a count the daemon ends a guest's relay on its firstguest_left(no connection outlives the guest; another open tab reconnects for a fresh snapshot).share.too_many_held) and past 64 asks kept before the relay's attachment answers (share.busy).MAX_SERVER_CONNECTIONS).🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Bridges a link share to its hosted room (
cmux.share/1, cmux-platform'sChatShareRoom): create provisions the share in the room, each active share keeps one host socket, and each guest whose browser is in the room gets a relay that connects back to this daemon as that guest and feeds the room that guest's chat. The daemon stays the authority — every frame passes its per-frame checks, and revoke, kick, expiry, or a closed chat closes the share at the daemon first.CMUX_SHARE_ROOM_URL, else the build's (none for a standalone daemon), and ships inert outside development (a PUT answers 404,share.room_refused; a 401 answersshare.needs_sign_in). An unanswered provision returnsshare.room_unreachable, is retried in the background, then revoked with its room DELETE queued; a provision that answers after the share's expiry never opens the share (it is retired in its place).helloit answersguests {active}first,redeemis answered by the registry's own atomic redeem, and kicks are persisted until the room answerskicked. A dropped socket reconnects with backoff; 4000 replaced, 4001 ended, 4003 and a refused upgrade stop it. Closing the agent chat closes its link shares too (end {reason: closed}).has_morewhen older ones remain), the watermark, chat title and status, and the guest's held plus most recently decided prompt states; then live records,olderpages,prompt_statefrom the reply and acpmux's records,prompt_statusanswers, andguest_typing. Room asks before the attach answers are held until it does (past 64 refused,share.busy); daemon records before the snapshot wait for it (past 256 the relay restarts fresh).hello(sockets), so one of two leaving does not end it; without a count the firstguest_leftends it. The 16-held cap counts undecided prompt ids in a per-guest ledger kept across host reconnects (forgotten on a kick or the share's end), so a burst cannot pass it andprompt_statuscannot evict it; retrying a decided prompt takes no slot, history pages apply the decisions they name (prompts decided while the guest was away release their slots), and the guest's client ids carry to the next relay so a reconnect's snapshot names those prompts' states. A prompt's state moves forward only, so a refused retry never undoes a decision.truncatedandfull_bytes; a 17th held prompt is refused withshare.too_many_held. A relay keeps at most 32 calls and 2 history pages in flight; past thatoldergets an empty page that has more. A relay ends on the guest's lastguest_left, a kick, revoke, expiry, or a closed attachment, and while the guest stays connected it restarts after a backoff (250 ms doubling to 30 s) with a fresh snapshot; a guest or host reconnect starts a fresh relay too, and a replaced relay's late frames are dropped. After attach the relay sendsagent-session-view {visible: true}so the owner sees the guest viewing.Known limits
presence {to}is not relayed (the daemon does not give a bound guest the presence set); relaying it needs a policy decision on what a guest may see of others.prompt_statuscovers only prompts whose state records the relay has seen, not acpmux's prompt ledger.heldonce accepted.Risk
Written for commit f81fd0a. Summary will update on new commits.