Skip to content

Terminal Presentation Broker ​

One agent runtime can appear in Agents, one or more Agent Session surfaces, and authenticated remote clients at the same time. The Rust terminal session broker makes those views independent presentations of one PTY without allowing them to fight over input or geometry.

Core Invariants ​

For each live terminal session:

  1. At most one presentation owns input and canonical PTY geometry.
  2. Every presentation consumes the same ordered output stream.
  3. Registration, DOM focus, and viewport observation are lease-passive. A visible interactive desktop terminal explicitly activates an unowned runtime at startup or after deferred registration recovers; it cannot automatically replace an existing owner.
  4. Mirrors never resize the PTY.
  5. Split, move, hide, suspend, and close affect presentations, not the runtime.
  6. Desktop and remote clients use the same generation, lease, snapshot, sequence, and geometry rules.

Structured prompt delivery is a separate backend-authorized control path. The terminal lease applies to terminal keystrokes and binary input; it must not gate Inbox, Chat, Commands, mailbox, automation, or broadcast delivery.

Session, Presentation, and Consumer ​

The broker actor is created with the PTY runtime and survives with zero presentations. It owns canonical geometry, a VT parser, bounded replay, the current runtime generation and lease epoch, presentation records, and feed consumers.

A presentation registration includes:

  • presentation_id and session_id;
  • client_kind: desktop or remote;
  • non-authoritative desired_geometry;
  • visibility and render_state;
  • requested interactive or read_only capability;
  • the client's observed lease epoch.

IDs must be non-empty, contain no whitespace, and stay within 512 UTF-8 bytes. One session accepts at most 64 desktop presentations and three remote presentations. The broker trusts the local desktop identity; a remote presentation must match its authenticated attachment identity. Requested interaction can only reduce the capability allowed by that identity.

Presentations and feed consumers are deliberately separate. One desktop consumer fans a session feed out to all local renderers. Each authenticated remote socket has its own cursor. Hidden presentations do not receive private queues.

Generation, Epoch, and Sequences ​

These counters solve different stale-client problems:

ValueChanges whenRejects
runtime_generationthe PTY runtime is created or replacedinput, resize, cursors, or activation for an older runtime
lease_epochownership is revoked or a transfer beginsinput, resize, or activation based on stale ownership
stream_sequencean ordered output, geometry, ownership, or lifecycle event is emittedmissing or reordered feed application
geometry_sequencean owner proposes a new resizereordered resize requests from the same owner
interaction_sequencea presentation participates in an interaction/activationnondeterministic fallback ownership based on client clocks

Resume, clear, or PTY replacement increments the runtime generation and lease epoch, revokes owners and pending activation, resets parser/replay state, and requires fresh presentation synchronization. A reused session ID therefore cannot let a stale renderer write to a replacement PTY.

The desktop client treats runtime_paused as the beginning of that transition. It stops issuing presentation updates and snapshot requests to the paused generation, remembers the current owner, and waits for the replacement generation before re-registering. A PresentationNotFound retry is only a fallback for missed or racy lifecycle delivery, not the normal clear flow.

Lifecycle delivery can race the ordered event subscription. If a replacement snapshot advances the client generation before runtime_replaced arrives, the same-generation lifecycle notice still rebuilds the presentation registry once. The recovered owner then re-reports its renderer geometry because viewport acknowledgements from the previous generation cannot size the new PTY.

After synchronization, a client may restore the exact presentation that owned the immediately preceding generation through the ordinary two-phase activation protocol. Restoration is allowed only while that presentation is still visible, mounted, interactive, registered, and the replacement remains ownerless. This preserves an existing user ownership choice without making mount, focus, or registration implicit activation signals, and without letting registration order select a new owner.

Ordered Stream and Recovery ​

PTY reads and broker state changes are serialized by the per-session actor. Every event carries runtime_generation and a monotonic sequence. Replay is bounded to 4,096 events and 1 MiB of raw output; the older bound wins.

Consumers pull batches after a cursor. The broker clamps each batch to 256 events and 256 KiB, and clients acknowledge the highest applied sequence for lag diagnostics. Acknowledgements do not pin retention. Desktop wakeups are coalesced over 16 ms so bursty output does not create one UI event per PTY read.

A snapshot contains canonical geometry, generation, a unique ID, a sequence_barrier, VT terminal state, the visible grid, and up to 1,000 lines of scrollback. Its serialized payload is capped at 2 MiB. Apply it as follows:

  1. Resize the desktop xterm and parser to the snapshot's canonical geometry, then replay its formatted VT state. Fit or pan that source grid inside the presentation viewport without changing its columns or rows.
  2. Set the cursor to sequence_barrier.
  3. Ignore events at or below the barrier.
  4. Apply only consecutive later events.

Provider-specific output policy is applied before both VT parsing and event publication. In particular, the Codex runtime ignores CSI 3 J so a provider scrollback erase cannot create one history in broker snapshots and another in desktop or remote xterm. The filter carries partial control-sequence prefixes across PTY reads. Clients do not repeat this policy. Missing or invalid formatted state is reported as degraded rather than silently treated as an equivalent plain projection. A plain grid is shown only when no accurate earlier frame is available.

Provider agent terminals install one xterm parser handler for CSI ... SP q (DECSCUSR). The handler consumes provider cursor-shape and blink changes while the terminal keeps a blinking bar caret. It is installed on the desktop headless parser, the desktop renderer, and the remote renderer, and is disposed with each terminal instance. Because the handler belongs to xterm's parser, split controls and controls embedded in other parser contexts follow xterm's normal parsing rules. Cursor visibility, cursor movement, synchronized output, SGR, provider-authored glyphs, and animation frames remain in the stream. The generic user shell terminal keeps its application-controlled cursor semantics.

An expired replay cursor, missing sequence, generation change, or terminated runtime is a structured batch result. Gap and generation-change results include a recovery snapshot and new barrier. Slow clients resynchronize instead of growing an unbounded backlog.

Explicit Ownership Transfer ​

Activation is a two-phase protocol:

  1. Begin with presentation_id, runtime_generation, and the observed lease_epoch.
  2. The broker verifies the presentation is visible, mounted, interactive, synchronized, and backed by a live runtime. It advances the epoch, records a pending activation, removes the active owner for the transfer window, and returns an activation ID plus snapshot/barrier.
  3. After applying the snapshot, acknowledge the exact activation ID, generation, and epoch. The broker applies at most one current desired geometry and publishes the new owner.

Input and resize are rejected while a transfer is pending. If acknowledgement does not arrive within five seconds, the broker rolls back to the previous owner only when that presentation is still eligible; otherwise the PTY remains ownerless. Duplicate begin and acknowledgement calls for the same transfer are idempotent. Stale identity, generation, epoch, activation, or geometry requests return a lease decision without disconnecting an otherwise healthy client.

When an established owner loses its presentation, the broker may promote the eligible presentation with the highest server-owned interaction sequence. A candidate must be visible, mounted, interactive, synchronized, and connected to a live runtime. If there is no candidate, canonical geometry remains unchanged until the next explicit activation.

Suspending a current owner marks it as requiring resynchronization and disables input. Remount uses an owner-resync begin/snapshot/ack protocol. It retains the same generation and epoch only if no competing activation has won in the meantime.

Geometry and Fitting ​

report_terminal_presentation_viewport records a presentation's latest desired size without resizing the PTY. Only an active lease holder can send a presentation-aware resize with a strictly increasing geometry_sequence. Native PTY resize, parser resize, canonical geometry update, and the geometry event are one serialized commit.

Geometry limits are:

ClientColumnsRows
Desktop20..5008..200
Remote20..2408..80

Mirrors render the owner's canonical grid. They keep natural scale when it fits, reduce local scale only to the configured readability floor, pan if it still cannot fit, and letterbox when the viewport is larger. The default floor is 0.75 and cannot be configured below 0.5. A phone, narrow split, or remote viewport therefore never degrades the desktop PTY by imposing smallest-client-wins geometry.

A resize or activation acknowledgement can include a snapshot at newly committed geometry before the provider has repainted. Desktop clients consume its sequence barrier but retain the last accurate fitted frame. They stop writing later output to that old-width xterm. Once ordered post-geometry output arrives, the desktop feed requests one fresh snapshot for all presentations, sizes each xterm to its source grid, and resumes replay. While the provider has not repainted, the old frame remains visible; the routine geometry transition has no prompt or overlay. A live owner on the current runtime generation can continue typing, including escape-sequence keys and bracketed paste. Mouse reports, cursor-position reports, and binary input remain gated until a current-generation frame matches the broker geometry and the renderer fits that grid exactly. Lease loss or runtime replacement still rejects owner input. A degraded formatting snapshot keeps its factual status indicator, while ordinary keyboard input remains available.

Pending is bounded. A provider that has nothing new to draw after a resize (an Ink prompt after a vertical-only change) never repaints, so after one second without output the presentation reads one snapshot for itself and replays it. The broker parser already holds the terminal at the committed geometry, so that frame is accurate, and later provider output still applies on top of it. The settle reads state only. It sends no input and never retries against the provider.

The pending status applies only when the broker actually changes the PTY size, because only a size change makes the provider repaint. A forced viewport report at the geometry the broker already holds (the owner re-reporting after a runtime replacement such as New Session) and a resize the broker clamps back to its current size produce no snapshot and no repaint, so the owner keeps its ready status instead of waiting for output an idle provider will never send.

Keyboard continuity applies to every owner resize, including repeated silent resizes and snapshot recovery. Coordinate-dependent input remains unavailable until the rendered source frame and committed geometry agree; the PTY lease and runtime generation remain mandatory for all input.

Alternate-screen application ownership ​

The broker owns PTY transport, presentation leases, canonical geometry, snapshots, and ordered output. A terminal application still owns the protocol state it negotiates inside that transport. When an application such as OpenCode enters the alternate buffer and enables xterm mouse tracking, xterm forwards click, drag, hover, and wheel reports to the application instead of treating them as Wardian or xterm scrollback gestures. Remote touch input adapts vertical travel into the same wheel protocol only while that alternate-screen mouse mode is active. On Codex's normal screen, Wardian captures wheel input to scroll retained history before xterm can emit mouse reports; alternate-screen wheel reports remain owned by the application.

Snapshots and replay preserve alternate-buffer, mouse-mode, and synchronized- output controls. Synchronized-output boundaries are renderer instructions, not broker events: xterm holds intermediate row refreshes until the application closes the frame. Normal-buffer presentations may use rendered-row geometry to fill a host, while alternate buffers use xterm's measured cell grid so application-owned layout is not inferred from stale row DOM.

Text and binary input share a dedicated FIFO in the desktop session client. This preserves the byte order emitted by xterm when rapid keyboard and mouse events would otherwise create concurrent Tauri IPC requests. The input FIFO is separate from snapshot, event-drain, and geometry serialization so a renderer refresh cannot delay interactive input or create a resize backlog. The final presentation drains that FIFO before unregistering and releasing the session client, and marks itself closing before that drain begins. Already accepted input completes in order, while late renderer callbacks cannot append work to a client being released or be overtaken by a replacement presentation.

Desktop and Remote Consistency ​

TerminalSessionClient owns one ordered desktop subscription per session and fans snapshots and events to independent xterms. Each surface/card has a stable presentation ID and its own renderer, local scale, pan, and viewport state.

The authenticated remote WebSocket registers a remote presentation and consumer, then uses the same activation, input, resize, snapshot, events, ack, and owner-resync DTOs. Remote authentication and attachment identity remain mandatory, and existing socket backpressure and rate limits still apply. Non-owner or stale requests are nonfatal protocol results, not reasons to close the socket.

Renderer Budgets ​

The desktop process permits at most 24 mounted xterm renderers and 12 WebGL contexts. The pools are independent deterministic LRUs:

  • touching a visible or interacted presentation keeps it warm;
  • WebGL eviction falls back to xterm's DOM renderer;
  • xterm eviction keeps the logical presentation registered, marks it for synchronization, and restores it from a broker snapshot when needed;
  • an Agents presentation may be hidden while its budgeted xterm remains mounted; hidden still disables input and reveal, but does not itself discard renderer state;
  • an ordinary component unmount has a 30-second disposal grace so moves and zoom changes can reuse the renderer.

Renderer residency and presentation visibility are deliberately independent. The Agents surface keeps its current bounded resident set across short tab switches. A presentation outside that set is suspended and consumes no xterm budget. Hiding a resident presentation updates the broker to hidden while leaving its xterm mounted, so returning to Agents does not cause a simultaneous registration, snapshot, and renderer-construction burst.

First-paint reveal barrier ​

A new or restored xterm is not visible as soon as its DOM node exists. Its current reveal generation must complete this transaction first:

  1. confirm connected, non-zero host bounds and physical intersection;
  2. select WebGL or settle on the complete DOM fallback;
  3. measure the local viewport before registration;
  4. apply and await the broker snapshot or recovery write at source geometry;
  5. verify either the local grid or the source-sized grid with its measured fit against the stable host and backend;
  6. reveal the host atomically.

Registration's pre-snapshot hook keeps backend selection and the first fit inside the client's serialized registration transaction. Suspending, replacing, hiding, or changing the renderer invalidates the reveal generation, so stale font, snapshot, or intersection continuations cannot reveal a newer renderer. Resize observation schedules at most one animation-frame fit and skips unchanged pixels for an already-ready renderer. If fit metrics are not ready or keep changing across the write barrier, an eligible hidden renderer retries at a throttled cadence even without another observer notification. Only one retry can be pending; invalidation and teardown cancel it, and each attempt checks the presentation generation and renderer identity again. Timer expiry is never used as evidence that terminal geometry has settled.

Graph and Garden have a separate 30-second heavy-child grace through their surface render policy. Neither grace weakens broker ownership or changes PTY geometry.

Presentation Close Versus Runtime Lifecycle ​

Unregistering a presentation removes only that view. If it was the owner, the broker advances the epoch and applies the fallback rule. If it was the final presentation, the runtime, parser, replay ring, and canonical geometry remain alive.

Runtime commands are explicit and backend-owned:

  • pause retains broker state but makes runtime input and activation unavailable;
  • resume/replacement establishes a new generation and requires resync;
  • kill terminates the broker/runtime and notifies consumers;
  • none of these are implied by closing a tab, group, workbench, remote socket, or renderer.

Persisted workbench tabs are not pruned when a runtime disappears. They restore as unavailable placeholders and can recover when the resource returns.

When a provider fails during startup restoration, the backend publishes its launch error into a paused terminal session with no PTY or input handles. The error is included in the first snapshot, so existing views and terminals opened later can show it. A successful restart replaces that diagnostic generation with the real runtime and clears the old error. The agent watch output retains the same launch explanation; the terminal is a presentation of that failure.

Compatibility Boundary ​

Legacy raw desktop input/resize and remote v1 paths are temporary adapters to the same broker. They must not introduce a second owner or geometry writer. New workbench code uses presentation-aware commands carrying session, presentation, generation, epoch, and geometry sequence.

Provider readiness and structured interaction delivery are separate from the presentation lease. Do not use renderer focus, terminal repaint text, or a fixed delay as evidence that a provider is ready for a structured message.

Verification Boundaries ​

  • Rust unit tests prove identity, limits, activation races and timeouts, stale lease rejection, fallback, resync, replay gaps, bounded snapshots, generation changes, geometry ordering, and shutdown.
  • Frontend unit/integration tests prove desktop fan-out, cursor recovery, renderer budgets, mirror fitting, presentation state updates, explicit activation UI, and owner keyboard continuity while stale-frame coordinate input remains gated.
  • Browser E2E can prove presentation controls and mirror labels using mocked data, but cannot prove PTY resize, IPC, or native stream ordering.
  • Native E2E proves desktop owner/mirror races, stable geometry, gap-free output, close-without-kill, safe-mode continuity, and authenticated desktop-to-remote-to-desktop transfer.
  • Real-provider native E2E is required only for claims about a specific CLI's prompt, TUI redraw, resume, or provider-specific behavior.

Run the focused native workbench package after rebuilding native assets:

bash
npm run tauri -- build --debug --no-bundle
npm run test:e2e:native:workbench

The same commands apply in PowerShell. Always run with the harness's isolated WARDIAN_HOME; never point a test at a production Wardian home.

Released under the MIT License.