Skip to content

Browser collaboration

The Emacs host owns session state and execution. Browsers receive a live projection through a content-blind relay and submit only the typed actions their bearer links permit. Visible prompts, paths, source, and tool results can contain secrets; the projection does not redact them.

flowchart TD
    Host["Emacs host<br/>State, permissions, execution"]
    Relay["Relay<br/>Routes ciphertext"]
    Guest["Browser<br/>Decrypted view and local navigation"]
    Host -->|Sealed projection| Relay
    Relay -->|Sealed projection| Guest
    Guest -->|Sealed typed input| Relay
    Relay -->|Sealed typed input| Host

Only the host interprets guest input and performs permitted session actions. The relay serves the viewer but never receives the room key.

Start and stop sharing

/collab starts a room for the current session, dialing the relay at mevedel-collaboration-relay-url as a WebSocket client, and reports its bearer links (the full-control link is copied to the kill ring). /collab status reports the room, relay connectivity, and guest names without printing any secret, and /collab stop ends that session's room. /collab stop all (or M-x mevedel-collaboration-stop-all) ends every session's room and every lobby, and forgets lobbies recorded to restart. /collab lobby shares a workspace's session list instead; see the lobby. The session menu's S Share… surface offers the same actions on the menu's session and workspace, and lists every active share and lobby of this Emacs; see the view manual. Sessions may be shared concurrently; each has an independent room, key, and guest set. Killing the owning data buffer, ending its session, explicitly stopping the share, or exiting Emacs tears its room down. Otherwise the room remains available for the host share's lifetime, including when no guest is connected. The share surface is a singleton QR panel for the selected room.

When the host moves a shared session's work into a new session, such as a plan accepted with Worktree, the room hands off instead of just ending: the host shares the new session without asking again, since its guests were already following this work, and tells each guest "Continued in NAME" with an Open action linking the new room at the tier that guest already holds. A successor not yet titled, such as a fresh fork known only by its ID, is "a new session". The browser also keeps the room under the project menu's Rooms, without a second dock notice for it. When the new session cannot be shared, the host gets a warning and guests are told the work continues in Emacs; the host's own handoff is never held up.

An open share or lobby requests that the host stay awake while a guest is connected, and for two hours after it starts or the last guest leaves, whichever is later. mevedel-collaboration-keep-awake-idle-time configures this empty-share interval in seconds; nil disables the cutoff, while zero keeps the host awake only while guests are connected. This prevents a forgotten empty share from blocking sleep indefinitely. A connected browser tab still counts even when nobody is interacting with it.

Temporary relay outages leave the share open. Losing connected guests starts the idle interval; repeated unsuccessful reconnects do not extend it. Rejoining guests request keep-awake again. The cutoff does not stop sharing or invalidate links, but a sleeping host cannot serve guests until it wakes and reconnects. Active mevedel requests independently prevent sleep, even after this cutoff. /collab status reports the keep-awake request and its idle expiry, not proof that the OS inhibitor succeeded. These requests inhibit sleep only while mevedel-sleep-inhibit is enabled and its command is available.

The relay (the Go binary in relay/, which also serves the static viewer) is content-blind: every frame is sealed with AES-256-GCM under a room key that travels only in the links' URL fragments. A view link carries the bare key and grants live read access. A full link appends a 16-byte write token; its holder can additionally queue prompts and interrupt the running request. An owner link appends a further 16-byte owner token. Authority follows possession of the link, and each tier is a prefix of the next, so the secret's length alone tells the viewer which one it holds.

The owner link exists for the case the other two do not cover: the host is not at the keyboard and something needs granting. It is full control plus the following authorities, delivered as typed frames rather than through the command allowlist:

  • set-mode changes the session permission mode, including to full-auto. The mode slash command stays in mevedel-collaboration-unsafe-guest-commands for every guest: that refusal protects the allowlist from becoming an escalation path, and says nothing about a credential the host handed out for this purpose. The viewer offers the mode in its session settings, and keeps showing the mode the session is actually in until the host's status frame confirms the change.
  • new-session creates a session. The request itself needs only write authority -- every full-control guest gets the button -- and the owner token decides what happens next: an owner link is granted it outright, any other full-control guest has the request put to Emacs and to the room's owner-link guests. Approving someone else's request is no new authority for an owner, who can create a session outright. The sheet says which of the two it is doing before the guest presses anything.

  • recovery exposes selection among the offered models, reasoning efforts and registered presets, session rename, native-history recovery, retained-input retry, reviewed input requeue/discard, provider login and, in Claude Code sessions, runtime update checks. Login challenges go only to owner peers viewing that provider's credential store; each owner keeps the provider it last selected. Codex uses device authorization (URL and user code); Claude runs the CLI's login, shows its authorization URL, takes the full returned code and then verifies the subscription. Codex tokens also refresh asynchronously without a challenge. Tokens and subprocess output never enter shared transcript or recovery state. Owners see each recovery issue once, with the host's details; other readers see readiness and runtime issues only by category, since host diagnostics can name local paths. Signing in changes host-wide provider credentials; cancelling discards the challenge. Model/effort/preset/history/input changes require idle turns and current session mutation authority. Preset replacement preserves the session's and data buffer's permission and sandbox modes. The frame names the current model as the same BACKEND:MODEL label the offered models use. Its efforts array lists the current model's declared effort names, and effort is the selected name or null for Default. Owners choose an offered name or Default through the effort action; the host validates against the current model, persists the choice and refreshes every owner tab. Model changes retain supported effort or reset it to Default. Only providers with a browser login (Codex and Claude Code) are offered for sign-in, defaulting to the session's own when it supports browser login. Account remains available when the current session uses an API key but other login providers exist. A refused sign-in action leaves the owner's previous selection unchanged. Queue retry is offered only while a failed turn paused delivery, and native-history recovery only for a history Claude cannot or did not resume: one edited after Claude received it, one another machine or installation retained, or one whose resume failed to start. The host rechecks that the scope is still offered before detaching native history. Choices refresh when session status or retained input changes and are sent only when they changed; unchanged updates preserve an in-progress login code and the composer draft.

The viewer gathers these choices and the permission mode in a Session settings sheet, opened from the status strip's Settings button or its model and mode, which only owner links can do. The button reads Needs attention while an issue, paused queue, input awaiting review or recoverable history is pending; those actions head the sheet. Model, reasoning effort and mode apply on pick, a preset after confirmation. The effort selector includes Default and hides when the model declares no supported effort choices. Its displayed selection changes only after host confirmation, and refreshed model choices discard stale efforts. Every control carries a tooltip and, since phones have no hover, a visible explanation. An active Plan restriction is shown separately from permission mode: changing permission mode does not lift Plan. Rename uses the native readable-Unicode naming rules and updates the room heading, browser title and lobby listings.

Owner authority is never granted alone: the owner link contains the write token, so a peer claiming the owner token without it is a forgery and is refused. The viewer's own knowledge of its tier is cosmetic; the host re-checks the token on every owner frame.

Narrowed interactions

A mirrored interaction reaches every writable guest by default. The closed :audience contract on its mevedel--remote descriptor is either nil for that default or the symbol owner to restrict it to owner-link guests. Both the creation broadcast and the replay a joining guest receives apply the filter, and so does the response handler -- seeing a narrowed interaction and answering it are the same authority, and a request id is guessable while an audience is not. The push that wakes a sleeping browser follows the same audience, because waking someone for a decision they are not shown is noise.

An audience turns on the link a peer holds, never on who is holding it. Guest identity is per browser, not per tab: a person with the writer link open beside the owner link is one guest id in two tabs, and an audience keyed on identity would blank the owner tab for a request the writer tab made. Keying on the link is also sufficient here, because only a non-owner request ever becomes a question -- an owner's is granted outright -- so restricting to owners already excludes the requester. An audience that matches nobody is not an error: it is a decision only Emacs can take.

Because each tier's secret is a prefix of the next, a viewer can derive every tier at or below its own by truncating the secret it already holds: an owner can offer all three links, a full-control guest two, a view guest one, and no one can express a link stronger than their own. Inviting therefore involves neither the host nor the relay -- the Invite sheet builds the links in the page and copies them, or shows one as a QR code for a phone's camera. A code shows only when its tier's QR code button is pressed, because a code on screen hands its access to any camera that sees it. That is also why the tiers are a prefix chain rather than three unrelated tokens.

The room header and the tab title name the shared session. The host's status frame carries the session's display name, which a joining guest receives at once and every guest again after a rename, manual or automatic; until it arrives the header shows the start of the room ID. A lobby's header names its project instead. An artifact's own conversation is named by the artifact's title (see the artifact store), so its room is too, and the browser lists the room under that name.

Invite sits in the room header and remains available to read-only guests. New session is in In this room. Invite disappears when the room ends, because the link goes with it.

Every page has the same header: the room, the editor tab, and the agent and artifact views. It shows the wordmark, a back button when the page has somewhere to go back to, a small line over the page's title, and the page's actions; the Appearance menu follows whichever header is on screen. See navigation and history for the back button and the small line. The header keeps one line at every width: actions fold into its ⋯ menu, the least needed first, while they would squeeze the title below 120 pixels or cut it off while it has less than half the header, and come back when there is room again. The back button and presence never folds. The bell is an icon, struck through while the browser blocks notifications, and a labelled row in the ⋯ menu.

The header's small line names where the page is. It starts with the project, proj ▾, which opens a menu of the places this tab can go; over an artifact or sub-agent transcript it continues › Feature work, the room the panel covers, and clicking that closes the panel. A phone keeps this line and hides only the page's details, such as an artifact's size.

The project menu lists, in a room, the Lobby when there is one to go to and Project artifacts, the store sheet; in the lobby, its Sessions, Artifacts and, for a full or owner link, Files tabs. Under Rooms it lists the other rooms of the workspace this browser holds, capped to the tab's tier, and Manage rooms…, the sheet that copies and forgets them. The menu replaced the dock's Rooms and Project artifacts rows.

Navigation destinations are real links: the menu's lobby, rooms and project views, the header's lobby link and parent-room breadcrumbs, artifact previews and versions, project files, shared editors and agent transcripts. Ordinary clicks keep their in-page behavior; middle-click, Ctrl/Cmd-click and the browser's link context menu open the destination independently without changing the source tab or its draft. Management and mutation controls remain buttons. Opening a saved session or an artifact's conversation still requests a link from the host, which may first need to resume or share that session.

Every page but the lobby has a back button before its title. In a room it returns to the lobby; over a panel it closes the panel. Its label names where it leads, such as "Back to the project lobby". An editor tab's back button shows the session in that tab.

Moves are browser history. Opening a room from the lobby, a menu or the back button pushes an entry, and so does opening an artifact or transcript panel; browser Back closes the panel, then returns to the page before, and Forward reopens it. A reload keeps an open panel: its entry names the artifact record, project file or agent row, which is opened again once the host welcomes the tab. The lobby records its tab on its entry, so a reload or Back returns to the tab that was shown.

A room's key never enters history. Each entry names its room id only, and the tab keeps each room's link it used in sessionStorage, per tab and gone with it; going back finds the link there and reloads into that room. A room that ends is dropped from the tab's links, so Back cannot rejoin it.

Fresh-tab destinations use a private suffix on the bearer fragment, for example #ROOM.SECRET?view=artifacts or #ROOM.SECRET?file=PATH. The closed selectors are view=artifacts, tab=sessions|artifacts|files, artifact=STORE-ID (with optional positive version), record=RECORD-ID, file=PATH, and agent=AGENT-PATH (with optional positive segment). The viewer imports the destination into history state and scrubs the whole fragment before connecting. Neither credentials nor these content selectors enter relay request URLs. Reload restores the selected view from history state. Shared editors retain their existing ?shared=ITEM-ID#ROOM.SECRET URL format. Destination links never inherit a source editor's query and never grant a stronger tier than the tab already holds; the host still authorizes every data request.

Store links name their primary artifact. Transcript-card links instead retain the exact record ID, including cards for secondary files in a store directory. Agent-owned cards add agent=AGENT-PATH and, for an archive, segment=N to the record route. The new tab first fetches that agent snapshot, then opens the matching published record; it cannot substitute a filename or another store artifact when the record is unavailable. This preserves the host's per-guest publication boundary on reload as well as first opening.

Which lobby a room leads back to comes from the browser, and from the host for an owner. The browser keeps every lobby it opens with its rooms, marked as a lobby. A room's welcome carries the project's name and a lobby field: for an owner-link guest, the owner link of the workspace's running lobby; for any other guest, true while one runs; and false when none does. A lobby-state frame carries the same field to every guest of the workspace's rooms when the lobby starts or stops. The browser keeps an owner's link like one it opened. A workspace has one lobby, so a new lobby link for it, such as after its links were rotated, replaces the one kept before. With false, a room offers no way to a lobby even when the browser still holds its link, and the project stays only a menu. The host never starts a lobby to answer, and a view or full room link never reveals the workspace's other sessions.

Room navigation and appearance

In this room groups Shared work, artifacts, progress, skills and commands, and room actions. Shared work and artifacts start expanded; skills and commands start collapsed and include a search field. Skills + beside the composer opens and focuses that search. Each section reports its counts through the shared summarize seam and hides when it has nothing to offer.

Tapping a chip selects it while keeping the menu and keyboard focus in place. Skills combine on one message (up to six); a slash command is a single action and replaces the skill selection. Selecting a skill replaces a slash command. Selected chips stay highlighted and appear above the composer, each with a remove button. Removing a selection preserves the message draft.

At viewport widths of 1100px and above, the navigation occupies an independently scrolling right column at the window's right edge, 304px wide and 360px from 1600px. The conversation takes the rest, up to a 1100px column: paragraphs, lists, quotes and headings keep a 72-character reading measure, while code, tables, tool rows, cards, the composer and the status line use the column's width. From 1800px, an opened whiteboard or document docks beside the chat instead (see shared editing), and the navigation becomes a drawer opened by the header's In this room button. At smaller widths and increased browser zoom, it becomes a disclosure above the composer. On a phone (640px or narrower) that disclosure is one row with its counts, and it opens as a sheet over the room, closed by × or a tap outside it. On a phone shorter than 700px, such as an iPhone mini in Safari, the row leaves the dock for an In this room button in the header, and the status line under the composer hides while it only says that the room is connected. The composer starts at one line, grows with its message up to 40% of the viewport height or 18rem, and shrinks when cleared. On a phone it keeps a 44px minimum and a 30% viewport-height cap. Conversation text and the composer share a bounded width. The message and name fields have visible labels; keyboard users can skip the header directly to the transcript.

At the bottom of the conversation, the viewer follows new messages and streamed responses, including content that grows after rendering. Sending a message keeps that following state. Even a small upward scroll pauses following and preserves the reading position; sending while reading earlier content does not jump to the bottom. Scrolling downward back within 40px of the bottom resumes following. A compact floating down-arrow button above the dock also jumps to the latest content and resumes following. It overlays the conversation without reserving a row or hiding chat behind a full-width bar; its accessible label and tooltip are Jump to latest.

Room and discussion message boxes use Enter for a newline and Ctrl+Enter or ⌘+Enter to send. This applies to the room composer and new discussions and replies on HTML artifacts, whiteboards and documents. The sending shortcut also submits the feedback field on a pending interaction. The sending button's tooltip shows the platform's shortcut.

A guest's own chip leads the header's actions on every page. It opens a form for the name others see; the composer's as … beside Attach opens the same form. A guest is called by the name they chose, kept in that browser's storage per relay origin, or else by a random alliterative adjective and animal, such as Curious Capybara. A random name lasts while any of the browser's tabs on the relay is open: a reload keeps the tab's own, kept in its tab storage, and a new tab, the editor tab included, asks the open ones for theirs over a BroadcastChannel and waits up to 150 ms before picking a new one. Once every tab is closed, the next visit is someone new. Use a random name drops a chosen name and gives every open tab the same new random one; saving an empty name returns to the random one. Each change sends a sealed set-name frame to update the host's registered guest, and an open editor labels its laser pointer with it. Names are labels, not identities or unique handles. The guest's independent browser ID continues to identify its queued work.

Who else is here

The header, the artifact panel and the editor tab's header show initials for the other people on the same page, with their names on hover, each in a colour of its own derived from the name. A page is the store artifact a tab has open -- a whiteboard, document, HTML page or image -- or otherwise the tab's room. An artifact page spans every room and the lobby of the workspace, because one whiteboard can be open from a session's room and from its own conversation's; a room page is that room's alone. A browser counts once per page however many of its tabs are there, and is faded while all of them are hidden. Lobby session rows show how many people are in each live session, and artifact rows how many have the artifact open. Only browsers count; the host at Emacs does not appear.

Each hello carries the tab's page and visibility, and a sealed viewing frame reports every later change. mevedel-collaboration-presence recomputes the workspace on each change, admission, departure, rename or room stop, and sends a guest a presence frame only when its content changed. Frames carry display names only; browser IDs stay on the host. Whiteboard cursors remain scoped to one room.

Room and editor chrome share color tokens. Appearance selects a complete cool (default) or warm palette, system/light/dark appearance, and selective or minimal accent detail. Selective accents color participant attribution and the sidebar scrollbar; minimal accents make those details neutral. Errors, diffs, discussions, presence and authored drawing colors retain their meaning. Choices persist in browser storage per relay origin and follow other open room/editor tabs. Editors receive them through their bound item port without replacing the frame, document, selection or question draft. The same Appearance menu is available in the editor tab's header.

Pending decisions use a quiet card with an accent edge. Host-provided options retain their labels and response IDs, with no implicit preferred answer. Suggest a change expands the feedback field when the host allows feedback. Repeated unchanged requests preserve the open card and draft feedback.

Guest-requested sessions

A guest supplies an optional name and an optional first prompt; the workspace and working directory come from the room's own session, never from the guest. The browser creation path limits the name to 48 display columns, replaces characters outside [A-Za-z0-9_-] with underscores, and requires at least one ASCII letter or digit in a typed name. This is narrower than Emacs session renaming. The new session still gets an independent ID; its display name does not name its directory. A matching name among live root sessions in that workspace is refused; this is not a uniqueness check across all saved display names.

Without a name, or with one that sanitizes to no letter or digit, the session is created unnamed, like one started in Emacs without a name: it shows its ID until its first prompt, the guest's or a later one, gives it an automatic title (see session naming). Outcomes and offers carry the name the session starts with, its ID, and the approval prompt says it will be titled from the first prompt.

A guest may also pick the session's model. The welcome frame lists every model registered with gptel, as exact BACKEND:MODEL labels, to writable guests. Opening the request sheet refreshes that catalog with session-models, so models discovered during provider startup are offered without reconnecting. An existing pick is retained if still offered. The sheet offers models after a Default entry and sends the pick as the new-session frame's optional model. A label that no registered model resolves refuses the request instead of falling back to the default. The new session gets the default chat preset first and the picked model second, because a preset may name a model of its own that would otherwise replace the guest's choice. Only the lead model changes: the preset's model tiers and workloads, and so its agents, stay as configured. The choice is stored like /model's and survives resume.

session-models also carries efforts, mapping each offered label whose model declares reasoning efforts to their names. Once a model with efforts is picked, the sheet offers its efforts after a Default entry and sends the pick as the frame's optional effort; changing the model refreshes the list and drops a pick the new model lacks. The Default model offers none, because its model is only known once the host applies the default preset. The host refuses an effort without a model, or one the picked model does not declare, and otherwise applies it after the model, so it replaces the effort the preset brought. Default keeps that effort. The approval prompt shows a requested effort.

An owner's session settings also permit registered preset replacement; it preserves permission and sandbox modes because a preset carries tools, agents and arbitrary settings, not just a model. An approval prompt shows the requested model, or default.

One creation request per guest waits at a time; another is refused while the first awaits approval. Outcomes repeat the request ID and sanitized name so the viewer can settle the matching request. Each completed request has its own dock notice carrying its link or refusal and a Dismiss action. Connection loss marks waiting notices interrupted and dismissible, not refused or replayed: the request may already have reached the host. Check the lobby before asking again. A late outcome for that request can still settle its notice.

The room reaches more than the requester. Its other owner-link guests are offered an owner link to it in a room frame -- its own frame rather than a reply, because pairing it with a request the receiver did not make is how one guest's approval lands on another guest's pending card. An owner can already reach a session it approved; being told is the difference between reaching it and knowing it exists. Offers and replies land in one dock strip as notices, and every reachable one is kept in browser storage per relay origin and listed under the project menu's Rooms.

One relay serves every host and project, so the store is grouped by workspace. Every welcome and lobby listing carries an opaque workspace key, a hash of the host's system name and the workspace's type and id, and a room is kept under the key of the tab that was handed it; a session requested from a room or lobby is created in that same workspace. The project menu lists only rooms under the current tab's key.

The two surfaces answer different questions. A notice says what just happened -- approved, refused, someone handed you a room -- and Dismiss dismisses the news. The project menu answers which rooms this browser can get back to, and only Forget, under Manage rooms, takes one out of it. A refusal is never stored: it is news and nothing else. Rooms die with the host's share regardless of what the browser remembers.

A room is stored by its id and secret rather than as a whole link, because one origin can hold several tiers at once: two tabs of one browser on the full and owner links share that store. The browser keeps the strongest secret it was handed, and a tab presents that room capped to its own tier -- truncation again, the same prefix property the Invite sheet uses. Writes merge by room ID instead of replacing the list, preserving offers received by other tabs. A weaker tab cannot expose a stronger stored bearer. The room a tab is currently in is kept but not listed: it is not somewhere to go. It is kept under the name its host currently gives it, so a session renamed or titled after its first prompt is listed by that name.

The new session gets its own room, and the requester is handed the tier it already holds -- an owner requester an owner link, a full-control requester a full-control one -- so asking for a session can never be a way to gain authority. The first prompt enters the new session's pending-input queue, which drains on idle: the host read that text when approving, so submitting it is what the approval was for. Because a browser will not let an approval that arrives later open a tab by itself, the link arrives in the request sheet as something to tap. Creating a session is not idempotent, so a repeat request inside the duplicate-prompt window is dropped.

Questions nobody can answer

A guest may be the only one there, as on an Emacs daemon, so nothing a guest does waits for an answer in the minibuffer. Room frames run with inhibit-interaction, as lobby frames do, and so does the drain step that starts a guest's queued message as a turn. A step that would ask in Emacs signals instead:

  • a frame is refused to its sender with a notice, "This needs a decision in Emacs on the host first", and the room stays up;
  • a queued message is dropped with its attachments, as on a retraction; its sender gets the same notice, and the host a warning naming the guest.

Either way the host's warning quotes the question when the refusing API supplies it; some Emacs input functions signal without question data. Guests see only the notice, since prompts can name hosts and paths.

A queued message the host refuses for another reason, such as /compact in a Claude session, is dropped the same way rather than blocking the queue. Its sender is told it was not sent, and the host sees the error. A settling turn or running compaction only delays the drain, so the message waits in the queue. An attempt that may already have reached the transcript stays queued for review instead, and its sender is told so. None of these refusals blocks the host's own requests.

A refusal that leaves a blocking recovery issue, such as an expired Codex login, holds the message in the queue instead. Its sender sees the issue's public reason ("Provider sign-in is required") and that the message will be sent once it is resolved; an owner signs in from Settings, and the queue resumes when readiness clears the issue.

Owners can choose a replacement provider or preset, complete provider login, recover a root or child native history, or explicitly retry retained input. Undelivered steering or an interrupted queued delivery pauses delivery and must be reviewed first. Recovery never resends a submitted prompt. Queue acceptance, removal and pause state are persisted with the session.

A failing observer or projection preserves the room and its last good snapshot. The host gets one warning with the error and guests one reconnect notice until a publication succeeds again. A fault while handling one guest's frame warns the host and notifies only that guest.

Later steps of a turn run from timers and process callbacks, outside any binding. An Emacs daemon without a client frame reads the minibuffer on its invisible initial terminal, where a question waits forever while guests keep the event loop running inside it. So while a room or lobby is live, a minibuffer-setup-hook refuses any minibuffer question on that terminal before it waits, with the same inhibited-interaction signal and its prompt; sit-for and other event reads are untouched. This covers TRAMP passwords, one-time codes and host-key questions when a connection reopens, taking over a portable session lease another client let expire, and GPG passphrases read for auth-source through loopback pinentry. The failing step reports an error instead of hanging. A question on a client frame reaches the person there as usual. Before its first write to a remote target, mevedel asks once per target and Emacs process whether to keep the project's state there; a daemon nobody sits at sets mevedel-session-durability-accept-target-storage to accept in advance. Questions that read single keys instead of the minibuffer are reached only from host commands.

mevedel-busy-p tells whether any buffer in the Emacs is running or settling a turn, including retained agents awaiting admission or terminal publication. A host can wait for it to return nil before restarting a daemon whose rooms are idle.

Questions that are only offers are skipped where nobody can be asked: a directive turn's offer to save modified file buffers leaves the host's buffers alone. The cockpit refuses to open, so a bare /goal from a guest is refused rather than opening a menu on the host's screen. A guest can scope a message only to a directive bound to the room's own session, since another session's directive would run, and maybe restore, that session.

Steps that run without the guard never ask either. Tool calls are confirmed by mevedel's permission pipeline, whose prompts room participants can answer, so session and agent buffers turn off gptel's own confirmation. A session save whose visited file changed on disk fails with an error instead of asking whether to save anyway. Tool file visits skip unsafe local variables, the large-file warning and the version-control symlink question, and session segments skip the project's unsafe directory-local variables.

The lobby

A lobby is a room bound to a workspace instead of a session: one bookmarkable link that lists the workspace's sessions and opens or creates them. /collab lobby starts the current workspace's lobby and presents its links in the share panel; /collab lobby stop stops it, and /collab lobby rotate replaces its credentials. A host without a keyboard, such as an Emacs daemon, starts one with (mevedel-collaboration-lobby-start DIRECTORY), which returns the lobby with its links under :link-view, :link-full and :link-owner. /collab status reports running lobbies beside rooms.

Unlike a room, a lobby's credentials persist, and so does the lobby itself. The credentials are generated once and stored in .mevedel/lobby in the workspace state directory, readable only by the user. A lobby runs until it is stopped: starting one records its workspace root in lobbies.el under mevedel-user-dir, and exiting Emacs leaves that record in place. When the next Emacs runs mevedel-install, it restarts every recorded lobby, after startup has finished when it is installed from the init file, so the relay settings that follow it apply. The restarted lobby dials the same relay room, its links keep working, and browser tabs still within their reconnect window rejoin on their own. A lobby that cannot restart is reported as a warning and stays recorded for the next start. Automatic restore never prompts, and a failed workspace probe does not prevent other lobbies from restarting. /collab lobby stop in that workspace removes the record. A recorded workspace whose directory no longer exists is forgotten with a warning.

/collab lobby stop stops the lobby and removes the record, so it stays stopped across restarts; starting it again revives the same links. Stopping leaves the credentials in place. Rotating them is the revocation operation: every earlier lobby link stops working. The rooms a lobby hands out are ordinary rooms with the ordinary share lifetime (ADR 0114).

Link tiers keep their meaning. A view link lists sessions and the workspace's artifacts, and opens artifacts; a full link can also open sessions, change artifacts, and use the project files; an owner link can also create, rename and delete sessions. Opening a session resumes it when it is not live, shares it when it is not already shared, and returns its room's link at the requester's own tier, so the lobby never grants more than the link that reached it. The session id in an open request only selects among the sessions the listing shows; it never becomes a path. Creation uses the guest-requested session path with the lobby's workspace and root; a non-owner request is refused, because a lobby has no session in which to ask the host. For the same reason only an owner's listing carries the models it may create a session on.

Deleting removes a saved session's directory for good, under the safety rules of expired-session cleanup without its age cap. A session live in this Emacs is closed first, since its buffer would save it back; its room ends, so guests there see "Session deleted". One still running a turn is refused until the turn stops, and so is one whose lock or lease another client may still hold or whose journal captures are pending. After a deletion every lobby guest receives a fresh listing.

Each row carries the session id, display name, last save time, a prompt preview of at most 160 characters, and whether the session is live in Emacs and shared. An artifact's dedicated session is not listed; it is reached from its artifact. Lobby open, rename and delete reject those sessions even if a caller supplies their ID. Live sessions that were never saved come first, then saved ones, newest first; a listing holds at most 200 rows and reports how many it left out. The listing is sent on join and on request; the browser asks again on Refresh and whenever the lobby tab returns to the foreground.

Guests act while nobody may be at the keyboard, so lobby frames run with inhibit-interaction. Cold session restoration does not initialize optional view power monitoring or its battery/D-Bus backend. Opening a file-workspace session automatically reclaims a same-host PID lock when the host proves its PID is dead or its process identity proves PID reuse. Replacement checks that the observed holder has not changed; a competing replacement refuses the open. Live, foreign-host, malformed or unprovable locks still need a decision in Emacs, as do portable lease takeovers requiring confirmation. Other steps that would prompt refuse the request instead of waiting. A clean exit releases session locks. Rooms follow the same no-prompt rule; see questions nobody can answer.

PID-reuse proof uses Linux's boot ID and process start ticks, not a wall-clock timestamp. A live PID with missing or unavailable identity evidence remains protected, including locks written without that evidence. See session locking.

In the browser, a lobby link renders the session list in place of the conversation and composer. Open shows the session's room in the same tab, as a new history entry; an owner's rows also carry Rename and Delete. Rename asks for the new display name and uses the native naming path (including readable Unicode); it preserves the session ID and room. Renaming a saved, closed session restores it into Emacs first, with the same lock checks and no-prompt rules as Open; it does not share the session. It refreshes all connected lobby listings, including when Emacs or a room's Settings performs the rename. Delete asks for confirmation first. The lobby stores itself in the browser's room list, marked as the lobby, so every room it opens leads back to it (navigation and history). An Artifacts tab beside Sessions lists the artifact store for every link; a full or owner link adds a Files tab: the project files. Refresh and a return to the foreground reload whichever tab is shown.

Project files

A lobby is the project's view, so it also shows the project's files: the material every session in the workspace can read, which in a whiteboard or chat project may be nothing but uploaded reference data. Browsing, reading, uploading and removing all need a full or owner link; a view link gets no Files tab and its requests are refused, so it still reveals only the session list. The host owns this in mevedel-collaboration-files.el.

The project's own listing is the authority. It is project-files for the lobby's workspace root -- the VC backend's tracked and untracked files minus ignored ones in a repository, the transient project's ignores elsewhere -- with workspace state under every .mevedel/ component removed, including nested projects and submodules, because it holds lobby credentials, permissions and memory. Private directories are not descended into for reference clones. A Git repository cloned inside a Git project without being tracked or ignored there, such as a reference repository, contributes the same listing of its own root by its own rules; Git reports it as a single directory entry, which project-files drops although the model's search tools read its files. Registered submodules come from project.el itself (project-vc-merge-submodules), and with project-vc-include-untracked off no untracked clone is listed. A guest names files and folders by their path relative to the root, but a path is accepted only when the listing shows it: a file must be listed, and a folder must hold a listed file. The resolved name is then re-verified beneath the root with mevedel-resource-within-root-p before any I/O, which also refuses a symlinked file or folder. An ignored, private or linked file is therefore neither readable nor writable from a browser.

The browser lists one folder at a time (files), folders first; a listing holds at most 1000 entries and reports how many it left out. Opening a file (file-get) uses the artifact panel and its transfer: bytes on demand, at most 16 MiB, in base64 chunk frames under the wire bound, rendered by the same sandboxed and XSS-safe renderers. A file with no type of its own previews as plain text when its content is UTF-8, so source and configuration files read in place. A project file has no discussions and no Delete there; for an owner link, Ask opens the new-session dialog with the file's path in the first prompt.

Upload adds files to the folder on screen, from the picker or dropped on the tree. A file travels in acknowledged chunks of 512 KiB (file-upload): the first announces folder, name and size, and the browser sends the next only after the host took the previous one, so a refusal stops the transfer and the socket never queues a whole file. The host keeps one unfinished upload per guest, and a new request id replaces it. An upload is at most 16 MiB. The name must be one path component that is neither hidden nor a ~ reference, and the file must not exist: an upload never replaces one. The file is written with an exclusive create. If the project's ignore rules would hide the new file, it is deleted again and the upload refused, since the browser could neither list nor remove it.

Remove (file-remove) asks for confirmation, then moves the file to the trash with move-file-to-trash; on a TRAMP workspace that is this Emacs's trash, which still keeps the bytes. A file with unsaved changes in an Emacs buffer is refused, because the buffer would save it straight back. Only files are removed, never folders. After an upload or removal the host tells every writable guest of that lobby which folder changed (files-changed), and a tree showing that folder reloads. These are direct changes by the link holder, outside the model's permission checks and patch review; the trash is their safety net.

Scoped prompts and attachments

Directives and shared items are the room's discussions. Records inside a directive turn carry its id and records inside a shared-item turn carry the item's id, so the filter strip lists Main chat, each directive (◆) and each whiteboard, document or artifact with turns (◇), with All at its far end. Main chat is the default and hides every discussion; a tab whose discussion disappears falls back to it. All appears only when it shows more than Main chat. A turn's discussion chip selects that discussion too. A deleted item has no tab: its turns stay under All with a disabled chip marked deleted (see deleting).

Selecting a directive scopes ordinary guest text to discussion of that directive; the host rechecks its workspace identity on receipt. A missing or invalid directive selection falls back to ordinary chat. Explicit command/skill invocations use their own route instead of inheriting the directive scope.

Selecting a shared item sends the composer's text to the assistant in that item's general discussion, the same way the item's own composer does. A room message has no reviewed snapshot, so the helper captures the item as currently committed. An artifact's message names its file instead (see discussing an HTML artifact). Commands and skills stay main-chat features; the viewer refuses them in an item discussion and says so. The composer placeholder and scope line name the discussion a message will reach.

Every composer that asks the model takes attachments: the room composer in any discussion, the whiteboard and document discussion composer, and the new-session first prompt. Attach, pasting into the message and dropping files onto the composer all add them. Replies and the HTML artifact discussion composer stay text-only. A prompt can carry up to three attachments totaling 1.25 MiB decoded: JPEG, PNG, WebP, PDF, plain text, Markdown, CSV, JSON, or patch text, and any other UTF-8 text such as source code or HTML, which reaches the model as text under its own extension. An extension Read treats as binary becomes .txt; content with NUL bytes or invalid UTF-8 is not text. The host generates filenames under workspace media storage and queues them through the normal file-mention path; a whiteboard question's own board snapshot rides beside them. The viewer can downscale images; non-image files cannot be resampled to fit. In a room's composer, each attachment chip has a + project toggle. A marked file is also uploaded to the project root through the project file upload, as its original bytes rather than the prompt's downscaled copy, after the prompt is sent. The prompt keeps its own copy either way. A taken name is numbered instead of refused (image.png becomes image-2.png), because pasted images share a name; a room accepts uploads but no other project file request. On a main-chat prompt an invalid attachment set is omitted as a whole and its text may still be queued; an item, artifact or new-session question with an invalid set is refused, so it never reaches the model without the files its sender chose. A storage error refuses that prompt without ending the room. Failed enqueue removes the files just created for it.

Discussions

Everything said about an HTML artifact, a whiteboard or a document is a discussion kept with the item: a target and its messages. The target is part of the item -- an element, word, passage or box of an HTML artifact, objects or an area of a whiteboard, a passage of a document -- or the whole item. Every item has one general discussion about the whole item. It always exists, stays open and has no marker; the room's discussion tab for the item shows it, and it is the first row of the item's discussions sidebar. A discussion about part of an item starts with its first message. Participants with edit authority resolve and reopen it; resolving hides its marker for everyone and folds the discussion into the sidebar's Resolved row. Reopen a resolved discussion before writing to it. View participants read discussions but cannot write.

Whiteboards, documents and HTML artifacts share one discussions sidebar (relay/viewer/viewer-discussions.js). It shows either the list of the item's discussions or one discussion. The list puts the whole item first, then the open discussions with their marker number, latest message or answer and any state that needs attention (including queued and running assistant work), then the resolved ones folded into one row. Its foot offers Discuss for the current selection. A discussion fills the sidebar: its subject and a link that shows the target, its messages with the assistant's answers beneath the messages they answer, and one box at the foot that replies to that discussion and nothing else. Its ⋯ menu holds Resolve or Reopen, Ask assistant again in whiteboards and documents, and Show in chat for an artifact. Discuss opens a new discussion that looks the same before its first message, with the quoted target on top; going back to the list drops it.

A message or answer that arrives in a discussion not on screen marks it instead of switching to it: its row turns bold with New answer or New message, the back link counts the discussions with news, its marker carries a dot, and the sidebar's toggle shows one while any news is unread. Opening the discussion clears the mark. What the item opens with is not news. In the discussion on screen, new messages and answers simply appear. Drafts belong to their discussion, so typing in one is never lost to another.

Every place that discusses an item works the same way. Discuss picks the part to discuss and opens a new discussion about it: discuss mode in the artifact panel, and the Discuss button, tool (M) and Ctrl/Command+Alt+M in the whiteboard and document editor. Ask assistant, checked by default, also sends the message to the assistant, which answers beneath it; unchecked, the message is a note between people and starts no model turn. Replies carry the same choice, which the next message keeps, and Ask assistant again sends a discussion as it stands, without a new message. A message to the general discussion needs text. With Ask assistant checked, a message about part of an item may be empty: what is written in that part is then the request (see sending a selection without a message). A room message with an item selected in the strip is a message to the assistant in that item's general discussion, so everything said about the item is kept with it.

A message to the assistant goes to the session the link speaks for:

  • written in a chat's room, to that chat, which becomes attached to the item, also when another session answered the discussion before: a room's links never prompt another session;
  • written from the lobby, to the session that answered the discussion, or else to the item's dedicated session, created on first use.

The discussion records its answering session; the artifact panel shows it as Answered in NAME. A selection sent alone is a task rather than a conversation: once the turn that answered it completes, the discussion resolves itself, attributed to the assistant. A failed or cancelled turn leaves it open, and a discussion with a written message is never resolved automatically.

The model receives the message, then the target as it now stands behind the shared-content heading, framed as user-provided data, then the discussion so far, newest messages first kept within 32,000 characters. Earlier turns about the same item are included; other room work is not, and history://root reaches the rest of the conversation. The request's audit data names the discussion and the message it answers, so every view places the answer beneath that message.

The HTML sidebar reads canonical item history, including archived answers, through the read-only history discussion action. A room reads only that session's turns about the item. In the lobby, the selected discussion chooses its recorded answering session, or the item's existing dedicated session; reading never creates or resumes a session. It does not combine project chats. History keeps newest complete turns within 128,000 characters, reports omitted older turns and unavailable archives, and places answers by their recorded discussion/message identities. Live copies replace preserved compaction tails. If a transcript segment cannot be read, this history action fails explicitly instead of returning partial history. Its browser error identifies only an unavailable logical segment (with its number when known at the read boundary); the complete condition and private archive path stay in the host's local diagnostic log, not the discussion response, for every link tier. An open artifact polls every five seconds; source identities cache up to four bounded projections per room, and a matching digest returns only an unchanged reply rather than reading and projecting archives again. Source checks stay constant-size: the live buffer tick/current segment identity, portable publication head and session-directory timestamp cover immutable archives and their atomic replacement, addition or deletion. Closing the panel stops polling. View links can read this history just as they can open the artifact.

If a lobby discussion's dedicated conversation is waiting for an Ask answer or approval, its sidebar says Waiting for a decision in NAME. The history snapshot carries only that pending flag and the bounded conversation title, not the question, approval body or a room link. Interaction identities also participate in the history digest independently of transcript edits, without reprojecting its records. Nothing shares or opens the dedicated conversation automatically. Full and owner links can explicitly Open conversation, using the store's existing Conversation action and the requester's existing link tier; view links see the waiting notice without a new action or decision authority.

Each guest action is a discussion request with an action of list, history, post, reply, resolve or ask naming its item as artifact:ID or by a card's record id. The artifact panel sends it as a room frame; the editor sends it through its editing channel, which also carries the message's attachments. Client-generated discussion and message identities make a retry neither store nor queue twice. The host refuses writes from view links and unknown or deleted items, and answers every refusal to the sender. Each item has at most 200 discussions besides the general one, each discussion at most 200 messages, and each message at most 10,000 characters.

An HTML artifact's discussions live in the store's protected .state/ID/discussions.json. A whiteboard's or document's live in the item on its editing host, which checks every target against the content, keeps targets current as the content changes, and lists them for models at shared://ID/discussions. Every room and the lobby see the same discussions, deleting the item deletes them, and exports leave them out.

Transcript, agent, and task projection

Compaction and clearing model context retain earlier conversation segments. The browser groups expandable Earlier conversation segment rows under the canonical chat number and title above the live transcript. /clear starts a new chat; compaction segments stay within their chat. The current chat is marked Live conversation · New messages go here. Opening a row loads the host's canonical archived projection on demand; it does not restore that segment into model context or change where new prompts are sent. Loaded disclosures remain open across live updates, and unavailable archives show a retry action. Rejoining a room reconstructs the segment index from the session, rather than relying on browser memory. An expanded segment has a collapse rail along its full left edge; closing it brings its heading back into view and returns keyboard focus there. Archived and agent transcripts use the live conversation's compact grouping: consecutive assistant text and tool calls share one visible speaker label.

The Assistant working… indicator sits above the message composer. The host publishes status immediately after request admission and teardown, including interruption and failure, rather than inferring activity from the arrival of transcript text. A lost connection clears the working indicator.

The shared browser renderer owns disclosure continuity when rebuilding a record. Main transcript updates, reconnect snapshots, and polled agent transcripts pass the previous record element to it, so explicit expansion and collapse survive while new nested disclosures use the host's defaults. Shared questions use this same renderer in the room and editor sidebar: the question stays visible above a closed Shared context disclosure containing the sent snapshot and attachment links. Its summary identifies the item, scope, and revision. Host-edited or mismatched prompts stay fully visible; attribution alone does not hide arbitrary text.

The browser is an observer of the canonical data buffer plus, for full links, a remote input source. It receives visible user and assistant text and tool records whose start and settlement state are explicitly published, never raw hidden audit or internal render data or arbitrary mutation commands. Each projection builds one tool-boundary index, avoiding repeated searches through historical tool results as new text arrives. A room also retains each transcript segment's records between publishes. A segment is reprojected only when its characters or text properties, the session context including plan mode, or a live input it read -- a running execution, pending completion evidence, an artifact's file stat, the tool registry -- has changed, so the result always equals a full projection. A room's first projection fills this cache. Later classifications compare a source snapshot, including text properties, and resume control and tool searches at the last closed control before the first change. Tool recovery keeps the final tool's lookahead live; unresolved openers, orphan tool properties and enclosing payloads move the restart back. This also handles producers that inhibit modification hooks. Raw property classification, prefix validation, neighbor repairs and record reconciliation still grow with transcript length; this is not a constant-time publish. Streamed updates therefore coalesce for at least 0.1 s and four times the previous publish's cost, up to one second, keeping a streaming room to about a fifth of the Emacs thread that every room shares without letting one slow publish stall the stream. Nested tool events update invocation facts synchronously and share that same publication timer, rather than rebuilding the entire pending tree for each child event. Projection refreshes dirty trees before returning records, including direct projection reads before the timer fires. Nested approvals reach guests at the coalesced boundary. Top-level tool starts, approvals, and settlement still publish immediately; root settlement flushes final status and child-scoped approval facts without waiting for the next stream tick. Canonical result insertion replaces the transient tree. A tool record keeps one stable identity from running through its settled canonical result. A guest prompt enters the ordinary pending-input queue as a queued follow-up; a hidden guest-prompt transcript audit record attributes the inserted prompt durably, renders as a badge, and never enters model-visible context. Every string a guest sends -- a prompt, a questionnaire answer, interaction feedback -- crosses the same per-string byte budget, because each of them reaches model-visible context and the transcript the same way. Outbound, every frame is bounded by the wire limit at the transport itself, and a snapshot record too large to travel in a frame of its own is dropped rather than sent: the relay refuses an oversized frame by closing the connection, and for the host connection it collects the room with it.

Every link tier receives the unified Background work panel: active agents, yielded Bash commands, and Monitors. The host's background-work frame uses the native panel's collector, canonical agent activity and stable ordering; the browser does not reconstruct process state or sort by mutable status. Rows carry stable keys, owner and execution ID or agent path, native start time and order, status labels, descriptions, effective provider/model/known effort, and the tool ID needed to find the originating call. Waiting is not itself a request for intervention; blocked agents are distinct. Retained-agent roster metadata still supports transcript inspection, including settled agents, without a second agent-chip or finished-agent presentation.

An execution preview is exactly three bounded output lines, with an explicit availability label, preview/output truncation facts, and output counts. Preview rows are capped at 512 characters; browsers fit each to one visible row. Full output uses the existing scoped execution-result request, never a browser-sent host filesystem path. While work is running, Full output shows an explicitly labelled snapshot at request time, not an automatically updating terminal. Passive execution events refresh the same coalesced publication without consuming output. The host retains at most 128 terminal snapshots for 30 seconds so completion survives registry removal and short reconnects; effective child model metadata stays with the completed row. Each terminal row includes terminalAgeSeconds, measured from its original host cache timestamp. A newly joining browser subtracts that age from its ten-second visible grace instead of reviving old completions; no host/browser clock synchronization is needed. Duplicate evidence and frame reads never restart the age, and age changes alone do not broadcast another frame. Active and terminal rows pass through the same native ordering. Frames admit live work before terminal evidence, cap at 128 rows and 64 KiB of encoded JSON, and signal any omitted rows explicitly. Changed frames broadcast once; joining guests receive the current frame directly. Expansion, hover/focus freezing and the ten-second visible finished-row grace belong to each browser, not the host view.

The panel replaces the separate agent chips and finished-agent list. It shows up to five host-ordered rows, the selected inspector and recent finished rows; Show all expands that limit in place. Its own scroll area is capped at 34% of the viewport (300px maximum), keeping work from growing over the composer on a narrow phone. Row names and metadata fit one line; execution inspectors reserve three preview lines and the same action slots through completion. Only one inspector opens at a time. Expanded completed executions remain until collapsed; collapse starts a fresh ten-second grace. Mouse hover or keyboard focus freezes the currently shown row set and holds its finished rows; new work is counted until the pointer and focus leave, which restarts their grace. Touch does not acquire a synthetic hover hold: expansion and focus provide the hold, and tapping elsewhere releases focus. In narrow-screen reading mode, focusing work controls keeps the dock compact rather than expanding the composer and moving the tap target; other dock controls retain their ordinary focus behavior. The panel's collapse control hides its rows without discarding durable transcript completion notices.

Open transcript reads an active retained agent in its existing sheet. Show in conversation asks the host to locate the original call in its owner's current or archived conversation, then loads exactly that segment. It opens the tool row, including a nested ToolCall child and its enclosing activity disclosure; child-owned rows open that child's transcript sheet. Source scrolling uses the unobscured transcript viewport between the sticky header and dock, rather than the whole window. Oversized tools start below the header; short sources center in the available space. Child-sheet navigation scrolls only its own transcript and leaves the parent's scroll position alone. The browser neither scans archives sequentially nor guesses a nearby row. Only a host-reported unavailable source falls back to retained execution output; connection, loading and projection failures report a retryable notice instead. Inspecting work never edits the parent's draft or changes its workspace. Finished agent controls become inert in place; the final report's durable transcript link remains available afterwards.

Source navigation is read-only for every link tier. A typed background-locate request names an execution owner/ID or a known child's path; the host obtains the tool ID from current scoped execution facts, retained evidence, or the registered child's launch invocation. Native audit lookup then identifies the actual current or archived receiving transcript, including nested ToolCall sources. The background-source reply carries only its owner, tool ID, current flag and optional segment number, or an explicit unavailable message. Lookup and cold read-only child hydration run only on click, outside the guest filter, with room/guest and remote transport readiness rechecked before reading. No archive scan or I/O is added to background publication. A child's fetch-agent request may name the located segment number; the host resolves only that child's canonical compact archive and returns uniquely identified records, never the parent's history or a browser-supplied filesystem path.

Full and owner links may submit typed background-stop actions: an execution's owner/ID tuple or a retained child's canonical path, optionally with its row key. The host checks the guest's current writable tier and resolves the target again in that room's live execution registry or retained-agent registry before using the native user-authority stop/interrupt API. View links inspect only; forged, stale, cross-session, ambiguous and root-agent targets are refused. Interrupting a child ends its current turn without deleting its retained conversation.

The same publication cycle sends the session task list to either link: identity, subject, status, optional owner, running agent activity, owner status notes, and unresolved dependency IDs. Each task displays its ID so dependency references can be resolved; each active owner's note appears once. The host uses the native task activity and priority helpers rather than inferring them in JavaScript. Long owner paths and notes wrap on narrow screens. The frame is latched on change and sent directly to each joining guest so an unchanged list cannot disappear on reconnect. The viewer keeps it collapsed to a done/total summary, orders in-progress work first, and strikes completed rows. Task projection is capped at 64 KiB of encoded JSON: it admits in-progress tasks first, then ready pending tasks, then blocked pending tasks, then the most recently completed tasks. The frame carries total, completed, omitted, and omitted-active counts, so a shortened history remains explicit and any omitted active work is conspicuous. The transport's larger wire bound remains the final safety check.

Every link tier also receives a change-latched goal frame on joining and as the durable Goal changes, including an explicit absent value when cleared. The status strip has a separate read-only Goal chip with status, settled turns, and token usage/budget. Tapping it opens the objective, public status reason, and accounted time/usage details. An unbounded budget and incomplete usage are explicit; incomplete usage is a lower bound, never a fabricated total. Unaccounted current request usage is not added to the durable counters. Raw Goal reasons can contain provider errors and private runtime diagnostics. Their producers mark these reasons as diagnostic; every link tier then receives only a safe recovery-category message. The native Goal retains the full diagnostic, which never enters the shared Goal frame. Model-reported blockers, user pauses and budget explanations remain public session content and retain their text. Objective and public reason text are each capped at 4 KiB with an explicit truncation notice, keeping the frame below 64 KiB even with JSON escaping. Goal changes stay in Emacs: the browser adds no Goal editing, pause, resume, or completion authority.

The panel's Open transcript action opens that agent's live transcript as a full-screen sheet. The viewer polls with fetch-agent frames while the sheet is open and refetches promptly on a roster change; the host validates the path against the agent registry -- never the filesystem -- and answers with targeted agent frames chunked under the wire bound. The reply crosses mevedel-collaboration--canonical-records over the agent's resident conversation buffer, the same projection that scrubs the root transcript, so only visible user text, response text, and settled tool records and allowlisted presentation travel, excluding raw hidden audit and internal render data. A digest rides each reply; a poll carrying a still-matching digest is answered with one unchanged frame instead of the transcript, and a per-guest throttle bounds what a hostile client can make the host project. A settled agent of a resumed session stays on disk until first opened; a guest fetch queues that one load on a timer, as the host's own open would perform it, and sends nothing while it runs, so the viewer's loading note stays until a later poll finds the conversation. A failed load is remembered for the room and refused instead of retried. The frame handler itself never starts target I/O. Artifact cards in an agent reply receive room-wide ids derived from the agent path and transcript-local id, so they remain openable without colliding with a root-transcript card. Agent chat and kill stay in Emacs; writable browser links can interrupt through the scoped background-work control described above.

Remote interactions

Full-link guests are also presented pending interactions as ui-request frames — generic requests (approve/deny/feedback), permission prompts (one-shot allow-once/deny-once/feedback; session, workspace, and always authority is never mintable remotely), plan approval (accept with the host-configured axes; Worktree acceptance requires an owner link and an explicit branch name), ApplyPatch review (apply the staged selection or request a revision with whole-patch feedback; side-by-side editing stays in Emacs), and Ask questionnaires (the frame carries questions, option descriptions and samples, and current answers; the guest answers atomically, with a blank answer meaning no preference, or dismisses only the questionnaire) — and the first answer, from Emacs or any guest, settles everywhere. Pending interactions stay above an open panel, such as a shared editor or an artifact, because an item's own conversation runs turns that ask too. A session shared while an interaction already waits presents that one too: starting a room adopts the session's pending prompts in the order they arrived, since each announces itself only once, when it is raised. mevedel-collaboration-remote-interactions gates that surface. Lease transfer, save, rewind, arbitrary forks, publication, and execution-target changes remain host operations. Owner acceptance of a Worktree plan is the typed, host-validated exception, not a general browser fork command.

Plan cards render Markdown with the full implementation instructions and read-only settings, without native keyboard hints that a browser cannot use. Full-link guests may accept Here plans and send feedback, but Worktree acceptance is owner-only. Owners supply the branch name through a prefilled text input; the host applies the same Git branch validation as the native workflow and never opens a minibuffer for a browser response. The host rechecks owner authority on receipt, independently of which controls the browser shows. If the complete plan, instructions and settings exceed the bounded card payload, the card explicitly requires full review in Emacs and offers feedback only. It never offers browser acceptance of an undisclosed truncated plan.

Ask shows one open question and single-line summaries of the others. For a single-select question, choosing an option or pressing Enter in a custom answer opens the next unanswered question. A question with multiSelect: true instead allows several checked options plus optional custom text; toggling or editing keeps it open. Clearing custom text leaves selected options intact. Enter on custom text matching an offered multi-select label commits that option and clears the custom field. Custom text is trimmed; whitespace alone is no preference. Submit sends the entire form, including unanswered questions as no preference. Previous/Next question and the summaries reopen answers for review. Tab and Shift-Tab retain normal browser focus traversal instead of skipping HTML controls, unlike Emacs's question-navigation bindings. The navigation row keeps its space at both ends of the form.

Question descriptors expose the optional multiSelect boolean. Atomic response answers contain strings for single-select questions and arrays of strings for multi-select questions, including [] for no preference. The host validates each answer's shape before changing any state, rejects duplicate multi answers and more than one custom value, and orders selected labels by the offered options with custom text last. The same submission path settles native and browser answers; the first accepted submission wins. The shared JSON decoder represents empty arrays, null and false as Lisp nil, so these three wire values all mean no preference in a multi-select answer slot.

Option descriptions are visible text, including on touch devices. Focus or hover inspects an option's inline sample without selecting it; Sample does the same on touch. The last inspected sample remains when an option has none. Samples wrap at the browser's own width in a fixed-height panel, with long samples scrolling. The whole form reserves the tallest question at that width; changing questions, answers or samples does not move its footer or the composer. Resizing recomputes that reservation. Repeated unchanged host frames preserve drafts, focus and scroll. Changed cards replace their existing slot rather than moving behind later requests. When feedback remains available, a changed card also retains its open feedback disclosure, text and focused selection without stealing focus from the composer. Browser requests read oldest first, so the scrolling stack initially exposes the decision that has waited longest. For the same questionnaire definition, host answer updates refresh untouched questions but preserve locally edited answers, custom drafts, the open question, sample inspection and focused selection. Changing the question definitions resets that local form; settling the request clears it everywhere.

A refused interaction action is reported to the answering browser through the same error boundary as other guest actions: owners receive the actionable host diagnostic, while full-link guests receive a redacted notice. Rejection does not pretend that the pending request was approved. Option IDs must name a nonnegative offered option; malformed IDs cannot select the first choice accidentally.

Permission cards include the same warnings as Emacs, including a side-conversation mutation's still-active parent. Their full operation and authority details remain available regardless of whether the host folded its native card. Remote permission outcomes remain one-shot only.

Patch cards show the current host-selected, host-edited changes rather than the original authored patch. A plain-text notice before the diff explains the selection, staged feedback, parent activity and any conflict or incomplete rollback. Raw conflict exceptions stay in Emacs: they may contain private host paths unrelated to the authored operands. Browser notices explain the failure and recovery action without copying those exceptions; the diff retains its authored relative or resource operands. The primary action follows the current selection (apply changes, request revision, or reject). Selection/editing stay in Emacs; semantic changes refresh the same pending card. Explicit truncation marks oversized review text.

Commands, skills, and tool display

A writable guest's welcome also publishes the invocation roster its link tier is admitted to. mevedel-collaboration-guest-skills is an alist keyed by full and owner whose values follow the global-minor-mode mode-list shape: t, nil, or a list of names, (not NAME...), t, and nil read left to right until one decides, with an implicit nil at the end. An owner link without its own entry inherits the full entry; the default admits an owner link to everything and a full link to nothing. The candidate universe is every local slash command plus every user-invocable skill, a bare name resolving to the command first, and mevedel-collaboration-unsafe-guest-commands wins over any admission: it holds the commands that escalate, mutate durable state, manage the share, or only report to the host's display (tokens, ps, tools, worktree included), none of which can do anything useful for a guest. The guest may queue a command through the typed invoke field or combine selected skills through the skills name array on one prompt frame. These fields are mutually exclusive. Free text remains skill-inert; only selected names are planned. The host revalidates every selection against the original link tier on receipt and again at delivery. A removed or disallowed skill drops the whole queued selection. Selected skills use the existing ordered skill planner, including its command/instruction and fork semantics. /review and /verify sent without an argument are queued as uncommitted, because their argument-less form is a minibuffer picker on the host, and their chip hint lists the accepted target forms. Browser tool rows use the host's bounded semantic presentation tree. Direct ToolCall expressions display the underlying tool (for example, Skill: artifact-dashboard); composed expressions retain ToolCall and nested tool rows, parallel groups and returned output. Skill bodies render as safe Markdown, with delivered dependencies in separate collapsed foldouts. Missing historical dependency bodies are labelled unavailable rather than reread from current files. Raw model-facing reminder wrappers are not the display body. Plain activity runs over the native grouping threshold fold into one browser disclosure with the native semantic summary. Running counts and failed tool names stay in its one-line heading; the full summary is available on hover. Opening the group exposes the original tool/message/completion rows, with their own disclosure choices intact. Newly grouped rows already being read remain open; an explicit group fold wins thereafter. Scope changes, compound tools, artifact cards and background result notices split runs. Ordinary agent messages and a command's own completion can join them. Main, earlier conversation and agent transcripts share this grouping renderer. Noteworthy sandbox boundaries use the host's formatted, bounded disclosure in the collapsed tool header, including nested Bash rows. Raw sandbox facts and grant details are not sent as presentation metadata. Routine empty-input WriteStdin observations stay out of the guest transcript; their output and terminal status belong to the original Bash row. After the running owner's bounded tail replaces earlier output, the Bash row discloses that its preview is truncated even when the latest poll omitted no bytes. On completion, retained output remains available through the result link. After compaction, that row appears only in Earlier conversation, where projection uses later completion evidence from the session's segments to show the final result without a duplicate current-segment output card. This also applies to Bash children of ToolCall. If an intervening archive is unreadable and the command is no longer running, its original row keeps the available output but warns that completion evidence is unavailable instead of guessing an outcome. Yielded root and child executions produce one compact, output-free completion line, matching the native view: ● Background command "Run checks" completed (exit code 0) Show result. The description takes precedence over the command. Failures, stops, and timeouts say failed, stopped, and timed out; only a failed bullet uses the error color. Missing outcome evidence says ended, not completed. Monitor completions say ● Monitor "Watch build" stopped · 2 events instead of showing an exit code. The owner suffix appears only for another agent's execution, not the transcript's own path.

Local completion audits and forwarded EXECUTION / MONITOR_TERMINAL mail share this result link, deduplicated by owner and execution ID in each receiving transcript. Identity, description, kind, event count, and terminal facts come from trusted audits or the structured mailbox opener, never metadata-looking text in its output body. A MONITOR mailbox produces a separate, unlinked ● Monitor event: <first line> · <description> notice; only its preview comes from the payload. Background-event turn attribution hides the harness's fixed wake prompt, while retaining the assistant's response and event notices. An ordinary user prompt with the same words remains visible. Ordinary retained-agent deliveries also remain in the receiving conversation: result notices say Agent /root/name finished, failed, interrupted, or incomplete from the structural result envelope, never body prose or the current roster. Their Markdown final answers start folded and retain their disclosure choice across updates. Interim messages use a separate Message from /root/name disclosure. Open transcript inspects the retained child without changing the parent's composer or workspace. Show result and the panel's Full output make a separate read-only request; they do not duplicate the body in the parent transcript or alter the model-visible mailbox. For running work, the host requires a yielded execution in this room's live registry with the exact owner/ID, a root or registered child's receiving transcript, and its readable native tool source, including nested ToolCall sources. It reads a bounded request-time snapshot of that execution's artifact output and labels the response source:live; completion breadcrumbs are not required before settlement. Terminal requests validate the receiving root or registered child's trusted local breadcrumb, or a forwarded mailbox in the parent's live or archived segments. They prefer the child's trusted terminal row or completion audit and retained output when the child transcript is resident. If it is cold, the bounded forwarded mailbox output is the fallback. Missing archives or evidence are reported explicitly, not resolved from a browser-supplied path. Each response is capped at 50,000 output bytes and repeated requests from one guest are throttled. View-only guests can use the link. Nested open and closed choices survive record updates, reconnect snapshots and agent transcript refreshes; the composer draft stays untouched. Direct ApplyPatch calls keep artifact cards and diff presentation.

Artifact viewing

Artifacts are files in the workspace artifact store, authored through the normal patch workflow.

In a room, the projection turns each selected applied artifact destination into a card carrying its readable title, size and the id of the store artifact it belongs to -- never the bytes, and never the host-side path, which stays in the unserialized :artifact-path field. A multi-file patch produces one card per artifact destination; rejected changes produce none, and a mixed code/artifact patch retains its ordinary tool row. Cards, attached-item chips and the artifact panel display the store title; a secondary file adds its relative filename so files stay distinguishable. Browser history retains that title, while downloads keep the actual filename. File stats and title metadata are memoized against per-publish-tick target round trips and invalidated when ApplyPatch settles or the store changes. A deleted artifact still projects, marked missing, so it reads as deleted rather than as a gap in the log. The sidebar combines live and archived artifact records with the store artifacts attached to the room's session (last record per name wins), so compaction does not remove access to published work. Archived transcripts are read once per segment to reconstruct artifact metadata; their full browser projection is sent only when that segment is expanded.

Bytes travel only on demand: opening a card sends artifact-get with the record id, and opening a store artifact sends it with artifact:ID -- resolution is by identity against live or archived root records, agent records already published to that guest, or the store's own artifact ids, never by a guest-supplied path. Any link to the workspace may reach any store artifact this way, which deliberately widens the room's published records as the only authority (see ADR 0124). Agent artifact ids are namespaced before publication because canonical ids are only transcript-local. The request id must be a nonnegative JavaScript-safe integer before projection or file I/O starts, the resolved path is re-verified inside the artifact store before a byte is read, files over 16 MiB are refused, and repeat fetches from a guest within one second are dropped. The host answers targeted artifact frames with base64 chunks under the wire bound. The viewer renders HTML in an <iframe sandbox="allow-scripts"> (never allow-same-origin, which beside allow-scripts would hand the artifact the viewer's origin -- the decrypted transcript and the room key) with a prepended CSP of default-src 'none', so a self-contained page can run inline scripts and styles while external subresource loading is blocked by the policy. Because the frame's origin is opaque, the viewer's colour-theme choice cannot be read from it: a prelude script bakes the current data-theme stamp onto the artifact's root and follows later toggles over postMessage, so an artifact themes off html[data-theme] beside prefers-color-scheme. The prelude also handles local #section links inside the artifact, including encoded section IDs, so they scroll within its sandbox rather than navigating to the room URL. This applies in both the panel and the separate artifact tab. Markdown is parsed by locally bundled Marked and sanitized by DOMPurify before DOM insertion. It supports nested lists, links (including email autolinks), tables, and inline formatting on wrapped list lines. Single newlines remain visible line breaks. Authored HTML is displayed literally, not interpreted; code blocks retain the viewer's text-based highlighting. Each code block has a Copy button, including unlabeled and diff blocks. It copies only the rendered code, preserving its whitespace, and shows Copied or Copy failed according to the browser clipboard result. If browser sanitization is unavailable, rendering refuses the content rather than inserting unsanitized HTML. Images display inline, plain text shows as text, and anything else is offered as a download. "Open in tab" puts an HTML artifact beside the conversation: a same-origin shell opened synchronously from the click -- the bytes are already in hand, so no popup blocker races the transfer -- whose only content is the same sandboxed frame. Guests author artifacts through the model: they ask, the model writes, everyone gets the card. There is no guest upload path into the artifact store -- guests add files to the project instead -- and the relay is untouched: artifact frames are sealed like every other frame.

Full and owner links can Delete an open artifact after confirming, in a room or the lobby. The host resolves it from its own record of the card or from the store id, never from a guest path, and deletes the whole artifact -- its id directory with versions and discussions, and its dedicated session -- through the same path as the Emacs cockpit. A dedicated session open in Emacs is closed first; only a turn running there refuses the deletion (see the artifact store). The card then reads as deleted. Every live room of the workspace is re-published, and every room and the lobby receive the store's new listing.

The artifact store in the browser

A room's project menu opens Project artifacts, a sheet; the lobby has an Artifacts tab with its create buttons beside Refresh. Both listings show the workspace's artifact store newest first, one line each: name, kind, size, version count, age of the last change, and in a room whether the artifact is attached to the room's session; the id shows on hovering the name. Lobby rows show how many sessions attach the artifact. Explicit listings refresh saved-session discovery; change notifications reuse that discovery with current live-session attachments, so ordinary edits do not rescan saved sessions. Changes coalesce into one notification per workspace after the current mutation, when its transport is idle. That fanout reads the latest artifact store once and resolves the rooms still open, so saving does not wait for browser listing updates. A save that changes only an item's content, not its title or kind, is announced up to two seconds later, together with further such saves: the item's viewers already have the edit, and listings only show its size and time. An immediate notification for the workspace meanwhile carries them. The host sends the listing when a guest joins a room or asks for it (store-list), and to every room and lobby of the workspace after a store change (store-artifacts): once per change however many steps it took, built once for all of them, after the change is acknowledged, and only where a guest is connected. Each row shows Open, which any link can use to open file artifacts in the panel or whiteboards and documents in their editor. From the lobby, an editor opens through the artifact's dedicated session, with a bearer link capped to the guest's existing tier. Each row keeps its other actions in a ⋯ menu, which a refreshed listing keeps open. Any link can list an artifact's Versions and View a historical snapshot without changing the current content. Editor snapshots preview as SVG or HTML exports; historical previews have no discuss, delete or assistant actions. The bounded artifact-get transfer accepts an optional version number, resolved by the host inside the store.

Full and owner lobby links also offer New whiteboard and New document. Creation runs through the workspace's editing queue, then opens the new item's dedicated editor room. Full and owner links also act through store-action frames:

  • Restore makes any saved version the newest, including the latest saved snapshot when live edits have changed the artifact since. A whiteboard or document restores as the guest's edit through its editing queue, and the answer waits until it is saved, or says why it was not;
  • Save version, for a whiteboard or document, keeps its current state as a version;
  • Attach, in a room, attaches the artifact to the room's session;
  • Duplicate copies it into a new, independent artifact under a name the guest gives, attached to the room's session;
  • Conversation opens the artifact's dedicated session in a room of its own, created on first use;
  • Delete deletes it for everyone, after a confirmation, with its versions, discussions and dedicated session. The answer waits for a whiteboard's or document's editing queue. A dedicated session open in Emacs is closed, so guests in its room see "Artifact deleted" rather than a session the host ended.

Actions that would need a decision in Emacs, such as resuming a dedicated session another client holds, are refused with the usual notice.

Discussing an HTML artifact

Writable guests discuss part of an HTML artifact in the panel (see discussions). Discuss turns on discuss mode: hovering highlights what a click would pick, a word under the pointer or otherwise the element; Alt prefers the element, Arrow Up widens the highlight to its parent and Arrow Down narrows it again, and Enter picks it. Dragging across text keeps the browser's own selection and discusses that passage; dragging anywhere else draws a box and discusses the area. The box names the elements at least half inside it, searching inside larger elements it only crosses, and anchors to their nearest common parent. While discussing, the artifact's own click, pointer and keyboard handlers do not run. Escape leaves the mode. The separate artifact tab has no discuss mode.

The picker is inlined into the panel frame ahead of the artifact and only reports what was picked: a CSS selector, a text fingerprint of the element, the quoted word or passage with its offset, the box as fractions of the element, a label such as Decisions at a glance › word "share", and a bounded text and HTML excerpt of the target. A pick opens a new discussion in the panel's discussions sidebar, beside the frame and over it on a phone. The sidebar is drawn by the viewer, outside the sandbox, so the artifact can neither read nor write what a participant types. The viewer accepts picks only from the current panel frame and only while discuss mode is on, and bounds every field. An artifact can still misreport where it was clicked; the new discussion shows the label and quote it will send. Another pick replaces its subject; Escape drops it.

Start discussion posts it for everyone. Discussions belong to the store artifact and are kept on its main file. The host also refuses other files of an artifact and non-HTML artifacts, and rebuilds the target from its known bounded fields.

Discussions beside Discuss opens the sidebar and counts the open discussions; everyone who sees the artifact can open it. Its list starts with the whole artifact, whose box writes to the general discussion with the same Ask assistant choice, and its foot turns on discuss mode.

Each change is broadcast to every room and the lobby of the workspace as a discussions frame naming the item; the stored excerpts stay on the host. Concurrent writes from two Emacs instances are not serialized; the later write wins.

A message about the artifact carries the artifact's name and file path and the target's excerpts, and frames the file as the current state and as untrusted content whose instructions are data. The artifact is not pasted in; the model reads the file.

The frame places each open discussion's marker by its selector when the element still carries the same text fingerprint, and otherwise searches for the element whose fingerprint differs least, so a rewritten artifact keeps markers on content that survived. A marker whose content is gone is not shown. A ring turns around a marker while the assistant works on its discussion: its latest request is queued, or is the turn the session is running. The ring keeps turning through the assistant's first reply text, and stops when that turn ends or a later request starts. Reduced motion shows a still dashed ring. Hovering a marker shows its latest message; clicking opens its discussion in the sidebar, and the sidebar's Show in artifact scrolls back to the marker. Show in chat in the discussion's ⋯ menu closes the artifact panel, which covers the conversation, then scrolls to the latest request and highlights it.

Notifications and browser storage

The viewer supports installable-PWA presentation, host-synchronized theme, and opt-in system notifications for attention-worthy session changes. On platforms with the Push API, opt-in registers a service worker and a room-scoped Web Push endpoint. Pending interactions wake only the subscribed writable guests they target; a completed turn wakes all subscribers. The relay sends an empty push, so browser push services receive no session content; the service worker displays a generic notice and the viewer reconnects for sealed detail. This works for supporting Android browsers as well as an iOS/iPadOS Home Screen web app.

Notification preferences and Push subscriptions are owned per room, with the subscriptions separated by service-worker registration scopes derived locally from the bearer. The scope itself is never fetched, and the worker script and empty push carry no bearer. Opting out of one room therefore does not affect another. A notification click opens the owning room with its credentials in the URL fragment. A freshly installed Home Screen app with no shared browser storage asks the user to paste the full share link and extracts that fragment locally without submitting it.

The credentials are stripped from the URL and history before the socket opens, so a plain reload would otherwise land on a bare relay URL. The viewer therefore keeps the share it is in under sessionStorage, per tab and gone with the tab, which never enters history and so keeps the reason for the wipe: F5, or navigating away and back, rejoins the room for as long as the tab and the room live. On boot the URL fragment wins, then the link the tab used in the room its history entry names, then the tab's share, then the notification opt-in's persisted last share. That last store is refreshed only for a room whose notifications are on. On an initial connection the relay answers an unknown room with close code 4004. The host hands out a new room's links before its own relay dial settles, so a guest following one at once, as the lobby does, can arrive first; the viewer therefore retries a first-join 4004 for five seconds. A 4004 that outlasts that window is terminal: the viewer shows "Room closed" and drops the stale stored share instead of retrying for three minutes. Code 4001 (room closed by the host) starts the bounded reconnect window; a 4004 during that window stays retryable because the guest may have reached the relay before the host recreated the room.

An ended room shows a prominent status panel above the loaded conversation, or centered in the main area when no messages are loaded. It explains that continuing requires a new invitation from the host. The loaded transcript remains readable.

An actively focused viewer reports itself active and receives no Web Push. When a push-enabled viewer moves away, it receives Web Push whether its page is still live, suspended, or disconnected. When service-worker Push is unsupported or its subscription is unavailable, the viewer uses the live-page Notification fallback. The host retains endpoint routing metadata in memory and replays it after a relay transport reconnect, so background delivery survives an ordinary host network blip.

Notification opt-in retains the bearer link in browser local storage so a suspended PWA can reconnect. The viewer clears it and unsubscribes after observing room termination or exhausting reconnects; otherwise browser site data can outlive the room, although an ended room id and key no longer authorize a live share.

Connection recovery

The host delivers authenticated frames and peer controls in arrival order once remote I/O is idle. Input received while a callback is running waits behind already received input, including peer departures. Each delivery checks idle state again, so a callback that starts remote work delays the remaining input. Disconnecting or stopping the transport discards its pending input; a reconnect starts with fresh guest admission.

TCP/TLS dialing runs asynchronously, so starting or reconnecting a room does not wait for an unreachable address before returning control to Emacs. The share panel can appear while the transport is still connecting; its presence does not prove that the relay room is ready. TLS retains Emacs' configured certificate and hostname verification policy. Only the current connection may report an open room; callbacks from cancelled or replaced attempts cannot revive it. A dial the relay has not answered within 30 seconds is abandoned and retried like a dropped connection. Cancelling a pending dial detaches its callbacks immediately; its socket is deleted once network setup settles, because deleting during asynchronous DNS can block inside Emacs. Authentication uses the same nonblocking socket cleanup. A nonblocking connect can stall without ever failing: a headless host that dialed while its container started stayed connecting indefinitely, so the relay never knew its lobby room and every link to it showed "Room closed".

The host reconnects to the relay with bounded backoff, from 1 up to 30 seconds, after a network blip; the relay garbage-collects the room with the host connection, so guests treat room-closed as retryable, rejoin the same room id, and re-hello for a fresh welcome and snapshot. A guest gives up after three minutes without its room.

A connection can also die without any notice: after a suspend or a network change the host's socket still reads as open, and a write fails only once the kernel stops retransmitting, long after guests gave up. The host therefore pings the relay every 15 seconds and requires inbound bytes, a pong or the relay's own 30-second ping, within 45 seconds. A connection silent for longer is dropped and redialed, so a dead path is replaced within about a minute. The window outlasts the relay's own dead-host detection (a 10-second ping timeout), so the redial finds the old room collected rather than being refused as a second host.

Starting a room confirms that visible text, paths, and tool results may contain credentials or secrets and that the links are bearer credentials.

Shared-item requests retain item and discussion correlation in the canonical transcript's guest attribution. The editor panel reuses those records and the existing pending-input queue. Each item has separate model context, selected from the live transcript and archived segments without a second transcript store. In a Claude session each item question starts an isolated native conversation with that context, so it neither resumes nor enters the room's native history. Room chat can retrieve those turns through the same history resources. Canonical provider-failure summaries also appear in browser conversations. See shared editing.

Shared editing is optional: missing Node or helper resources on the Emacs host disable editing actions with a reason while chat, static artifacts, and the saved item list remain available. Recheck availability enables them after repair without reloading the room. Browser guests do not install Node. See the runtime contract.