detached 32ce382

docs: clearer README and guides, fix facts that drifted from the code (#281)

* docs(readme): add feature list and config sample, trim verbose sections

* docs(readme): present local, remote, and hybrid modes

* docs: rewrite guides for clarity and fix facts that drifted from the code

* docs: drop dated benchmark results and move contributor docs out of the user index
commit
32ce382a83a5ddd9e1765866a36b7d568c41863d
author
bnema · · 2026-09-24 04:45 UTC
committer
GitHub · · 2026-09-24 04:45 UTC
parent
9d8d577
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
13 files · +491 -1247
M README.md +52 -38
@@ -15,6 +15,25 @@
1515
1616 ---
1717
18+## Three modes
19+
20+| Mode | Command | What you get |
21+|---|---|---|
22+| **Local** | `vev` | Sessions on this machine, kept by a local daemon. |
23+| **Remote** | `vev attach user@host` | Sessions on a server, rendered there and sent as small diffs over SSH + QUIC. |
24+| **Hybrid** | `vev host add user@host` | Local and remote sessions in one picker; switch between them without leaving vev. |
25+
26+Remote and hybrid need vev installed on the remote host. Set `VEV_REMOTE_TRANSPORT=stdio` to use SSH only. See [remote sessions](docs/remote-resilience.md).
27+
28+## Features
29+
30+- **No prefix key**: Alt shortcuts plus a command palette with short codes.
31+- **Tabs, splits, stacks, and a floating terminal** per tab.
32+- **Persistent sessions**: detach and re-attach; named sessions survive daemon restarts.
33+- **Browser terminal**: the same UI in a private web page.
34+- **Copy mode** with vim motions, notifications, and a fuzzy session picker.
35+- **Scriptable**: `vev cmd` controls a running daemon; `--ui-driver` drives it headlessly.
36+
1837 ## Install
1938
2039 ```sh
@@ -28,30 +47,23 @@ Releases support Linux x86_64/arm64 and macOS Apple silicon.
2847 ## Usage
2948
3049 ```text
31-vev create an ephemeral session
32-vev new <name> create a named session
33-vev attach <name> attach to a local session (alias: a)
34-vev attach user@host[:session] attach to a remote daemon
50+vev new numbered session
51+vev new <name> new named session
52+vev attach <name> attach (alias: a)
53+vev attach user@host[:session] attach to a remote host
3554 vev ls [<host>|--all] list sessions
36-vev host add|rm <host> manage remote hosts
37-vev host list list remote hosts
38-vev kill <name>|--sessions kill one or every session
39-vev kill --all stop every vev process (named sessions return)
40-vev --web-daemon start the private browser terminal
41-vev --ui-driver [options] drive a headless attachment over JSONL
42-vev --ui-observe ... expose passive observation for an attach
43-vev --ui-control ... expose observation and input control
55+vev host add|rm|list manage remote hosts
56+vev kill <name>|--sessions kill one or all sessions
57+vev kill --all stop vev entirely
58+vev --web-daemon start the browser terminal
4459 ```
4560
46-The daemon starts on first use and survives an empty session registry: removing the last session is not shutdown, so it can be reused. `kill --sessions` purges every session and leaves vev running. `kill --all` stops everything: every vev window, the broker, and the daemon, even from an older build; named sessions come back on the next start. Numbered sessions survive detach; named sessions also survive daemon restarts. Exiting the final shell removes that session and returns to the most recently used session; with no previous session, vev exits. See [durable-session recovery](docs/durable-session-recovery.md).
47-
48-> [!NOTE]
49-> A release that bumps vev's protocol version resets named sessions saved by an older protocol. Session names are retained, but layouts, tabs, terminal history, recovery transcripts, and process-recovery state are discarded. Each local or remote daemon applies this reset to its own sessions when it first starts with the new protocol.
61+- **Numbered sessions** survive detach, not a daemon stop.
62+- **Named sessions** also come back after `kill --all` or a daemon restart ([recovery](docs/durable-session-recovery.md)).
63+- The daemon starts on first use. Exiting the last shell of a session returns you to your previous one.
5064
5165 ## Keys and palette
5266
53-No prefix key; bindings use Alt.
54-
5567 | Key | Action |
5668 |---|---|
5769 | Alt+Space | command palette |
@@ -70,29 +82,28 @@ Type a code or fuzzy-search the palette. Common default codes:
7082 | `MFP` / `MAT` | move pane / tab | `SSP` | session picker |
7183 | `DET` | detach | `NTC` | notifications |
7284
73-In the session picker, press `/` to fuzzy-search session names, hosts, tab names, and tab details; use Arrow keys or Ctrl+n/Ctrl+p to move between matches. Escape clears the query, and a second Escape leaves search.
85+- **Session picker** (`SSP`): ↑/↓ or `j`/`k` to move, `/` to search, `s` to sort, `x` to kill, Escape or `q` to close.
86+- **Copy mode**: scroll up, then vim motions, `v` to select, `y` to copy.
87+- With local and remote daemons attached, `CNS` asks where to create the session.
7488
75-When a client is attached to both a local and a remote daemon, `CNS` and `CNS <name>` list eligible daemon destinations in the existing palette. If two or more destinations exist, choose one explicitly with Up or Down before pressing Enter. The unqualified choice appears only when the client has an exact local route; remote choices remain host-qualified. A direct remote start without a proven local route never labels its home route as local.
89+## Configuration
7690
77-Scroll up to enter copy mode; use vim motions, `v` to select, and `y` to copy. All codes and bindings are configurable in [configuration](docs/configuration.md).
91+Optional file: `~/.config/vev/config`. Changes apply live.
7892
79-## Remote attach
80-
81-```sh
82-vev attach user@host[:session]
93+```text
94+theme = auto # auto, dark, or light
95+open-palette = alt+space # any binding can be changed
96+code.new-tab = CNT # any palette code can be changed
97+floating.command = # empty = your shell
98+scrollback.megabytes = 50
99+bar.top-right = date +%H:%M # status command shown in the bar
83100 ```
84101
85-SSH bootstraps an authenticated direct QUIC connection. Set `VEV_REMOTE_TRANSPORT=stdio` to use SSH only. The remote host needs vev installed; see [remote resilience](docs/remote-resilience.md) for firewall, transport, and host-list details.
102+Full list: [configuration](docs/configuration.md).
86103
87104 ## Browser terminal
88105
89-`vev --web-daemon` starts a background web gateway on `127.0.0.1:8778` and prints a private access link. The browser displays the ordinary multiplexer UI, including its palette, tabs and splits, using HTML/CSS and a small native JavaScript adapter. No frontend framework or external assets are loaded.
90-
91-Use `--web-listen` and `--web-origin` (or `web.listen` and `web.origin` in config) for a private network or an HTTPS reverse proxy. VEV serves HTTP; the proxy owns TLS. See [browser terminal](docs/web-terminal.md) for setup, isolated visual testing, controls and current limitations.
92-
93-## UI driver
94-
95-`vev --ui-driver` provides bounded capture, key/text input, and deterministic waits for a headless attachment. Existing clients stay unchanged unless `--ui-observe` or `--ui-control` is explicitly enabled. See [UI driver](docs/ui-driver.md) for the JSONL contract, private socket bridge, completion semantics, and isolated endpoint configuration.
106+`vev --web-daemon` serves the UI on `127.0.0.1:8778` and prints a private link. Use `--web-listen` and `--web-origin` behind an HTTPS proxy. See [browser terminal](docs/web-terminal.md).
96107
97108 ## Scripting
98109
@@ -104,16 +115,17 @@ vev cmd toast -l warn "build failed"
104115 vev cmd list-panes --json
105116 ```
106117
107-Use `vev cmd --help` for commands and targeting options.
118+Use `vev cmd --help` for all commands. For headless capture and input, see [UI driver](docs/ui-driver.md).
108119
109120 ## Documentation
110121
111122 - [Configuration](docs/configuration.md): bindings, palette, theme, overlays, and bar anchors
112123 - [Terminal compatibility](docs/terminal.md)
113-- [Remote resilience](docs/remote-resilience.md)
124+- [Remote sessions](docs/remote-resilience.md)
125+- [Session navigation](docs/session-navigation.md)
114126 - [Durable session recovery](docs/durable-session-recovery.md)
115-- [UI driver](docs/ui-driver.md): headless capture/control and opt-in interactive observation
116-- [Performance](docs/performance.md)
127+- [Browser terminal](docs/web-terminal.md)
128+- [UI driver](docs/ui-driver.md)
117129
118130 ## Development
119131
@@ -125,7 +137,7 @@ VEV_ENV=dev go run . kill --sessions
125137 rm -rf .dev/dev
126138 ```
127139
128-Each name shares one daemon and its vev-owned files under `.dev/<name>/`; use separate names to run binaries side by side. This isolates vev configuration, runtime files, durable sessions, hosts, snapshots, and logs, but it is not an OS, filesystem, or network sandbox.
140+Each name gets its own daemon, config, sessions, and logs under `.dev/<name>/`. It is not a sandbox.
129141
130142 ```sh
131143 make test # go test ./... -race
@@ -133,3 +145,5 @@ make lint # goimports check, go vet
133145 make mocks # regenerate mocks
134146 make demo # regenerate docs/assets/demo.gif (needs Docker)
135147 ```
148+
149+Contributor docs: [architecture](docs/architecture.md), [protocol](docs/protocol.md), [performance measurement](docs/performance.md).
@@ -15,6 +15,25 @@
15 15
16 ---16 ---
17 17
18+## Three modes
19+
20+| Mode | Command | What you get |
21+|---|---|---|
22+| **Local** | `vev` | Sessions on this machine, kept by a local daemon. |
23+| **Remote** | `vev attach user@host` | Sessions on a server, rendered there and sent as small diffs over SSH + QUIC. |
24+| **Hybrid** | `vev host add user@host` | Local and remote sessions in one picker; switch between them without leaving vev. |
25+
26+Remote and hybrid need vev installed on the remote host. Set `VEV_REMOTE_TRANSPORT=stdio` to use SSH only. See [remote sessions](docs/remote-resilience.md).
27+
28+## Features
29+
30+- **No prefix key**: Alt shortcuts plus a command palette with short codes.
31+- **Tabs, splits, stacks, and a floating terminal** per tab.
32+- **Persistent sessions**: detach and re-attach; named sessions survive daemon restarts.
33+- **Browser terminal**: the same UI in a private web page.
34+- **Copy mode** with vim motions, notifications, and a fuzzy session picker.
35+- **Scriptable**: `vev cmd` controls a running daemon; `--ui-driver` drives it headlessly.
36+
18 ## Install37 ## Install
19 38
20 ```sh39 ```sh
@@ -28,30 +47,23 @@ Releases support Linux x86_64/arm64 and macOS Apple silicon.
28 ## Usage47 ## Usage
29 48
30 ```text49 ```text
31-vev create an ephemeral session50+vev new numbered session
32-vev new <name> create a named session51+vev new <name> new named session
33-vev attach <name> attach to a local session (alias: a)52+vev attach <name> attach (alias: a)
34-vev attach user@host[:session] attach to a remote daemon53+vev attach user@host[:session] attach to a remote host
35 vev ls [<host>|--all] list sessions54 vev ls [<host>|--all] list sessions
36-vev host add|rm <host> manage remote hosts55+vev host add|rm|list manage remote hosts
37-vev host list list remote hosts56+vev kill <name>|--sessions kill one or all sessions
38-vev kill <name>|--sessions kill one or every session57+vev kill --all stop vev entirely
39-vev kill --all stop every vev process (named sessions return)58+vev --web-daemon start the browser terminal
40-vev --web-daemon start the private browser terminal
41-vev --ui-driver [options] drive a headless attachment over JSONL
42-vev --ui-observe ... expose passive observation for an attach
43-vev --ui-control ... expose observation and input control
44 ```59 ```
45 60
46-The daemon starts on first use and survives an empty session registry: removing the last session is not shutdown, so it can be reused. `kill --sessions` purges every session and leaves vev running. `kill --all` stops everything: every vev window, the broker, and the daemon, even from an older build; named sessions come back on the next start. Numbered sessions survive detach; named sessions also survive daemon restarts. Exiting the final shell removes that session and returns to the most recently used session; with no previous session, vev exits. See [durable-session recovery](docs/durable-session-recovery.md).61+- **Numbered sessions** survive detach, not a daemon stop.
47-62+- **Named sessions** also come back after `kill --all` or a daemon restart ([recovery](docs/durable-session-recovery.md)).
48-> [!NOTE]63+- The daemon starts on first use. Exiting the last shell of a session returns you to your previous one.
49-> A release that bumps vev's protocol version resets named sessions saved by an older protocol. Session names are retained, but layouts, tabs, terminal history, recovery transcripts, and process-recovery state are discarded. Each local or remote daemon applies this reset to its own sessions when it first starts with the new protocol.
50 64
51 ## Keys and palette65 ## Keys and palette
52 66
53-No prefix key; bindings use Alt.
54-
55 | Key | Action |67 | Key | Action |
56 |---|---|68 |---|---|
57 | Alt+Space | command palette |69 | Alt+Space | command palette |
@@ -70,29 +82,28 @@ Type a code or fuzzy-search the palette. Common default codes:
70 | `MFP` / `MAT` | move pane / tab | `SSP` | session picker |82 | `MFP` / `MAT` | move pane / tab | `SSP` | session picker |
71 | `DET` | detach | `NTC` | notifications |83 | `DET` | detach | `NTC` | notifications |
72 84
73-In the session picker, press `/` to fuzzy-search session names, hosts, tab names, and tab details; use Arrow keys or Ctrl+n/Ctrl+p to move between matches. Escape clears the query, and a second Escape leaves search.85+- **Session picker** (`SSP`): ↑/↓ or `j`/`k` to move, `/` to search, `s` to sort, `x` to kill, Escape or `q` to close.
86+- **Copy mode**: scroll up, then vim motions, `v` to select, `y` to copy.
87+- With local and remote daemons attached, `CNS` asks where to create the session.
74 88
75-When a client is attached to both a local and a remote daemon, `CNS` and `CNS <name>` list eligible daemon destinations in the existing palette. If two or more destinations exist, choose one explicitly with Up or Down before pressing Enter. The unqualified choice appears only when the client has an exact local route; remote choices remain host-qualified. A direct remote start without a proven local route never labels its home route as local.89+## Configuration
76 90
77-Scroll up to enter copy mode; use vim motions, `v` to select, and `y` to copy. All codes and bindings are configurable in [configuration](docs/configuration.md).91+Optional file: `~/.config/vev/config`. Changes apply live.
78 92
79-## Remote attach93+```text
80-94+theme = auto # auto, dark, or light
81-```sh95+open-palette = alt+space # any binding can be changed
82-vev attach user@host[:session]96+code.new-tab = CNT # any palette code can be changed
97+floating.command = # empty = your shell
98+scrollback.megabytes = 50
99+bar.top-right = date +%H:%M # status command shown in the bar
83 ```100 ```
84 101
85-SSH bootstraps an authenticated direct QUIC connection. Set `VEV_REMOTE_TRANSPORT=stdio` to use SSH only. The remote host needs vev installed; see [remote resilience](docs/remote-resilience.md) for firewall, transport, and host-list details.102+Full list: [configuration](docs/configuration.md).
86 103
87 ## Browser terminal104 ## Browser terminal
88 105
89-`vev --web-daemon` starts a background web gateway on `127.0.0.1:8778` and prints a private access link. The browser displays the ordinary multiplexer UI, including its palette, tabs and splits, using HTML/CSS and a small native JavaScript adapter. No frontend framework or external assets are loaded.106+`vev --web-daemon` serves the UI on `127.0.0.1:8778` and prints a private link. Use `--web-listen` and `--web-origin` behind an HTTPS proxy. See [browser terminal](docs/web-terminal.md).
90-
91-Use `--web-listen` and `--web-origin` (or `web.listen` and `web.origin` in config) for a private network or an HTTPS reverse proxy. VEV serves HTTP; the proxy owns TLS. See [browser terminal](docs/web-terminal.md) for setup, isolated visual testing, controls and current limitations.
92-
93-## UI driver
94-
95-`vev --ui-driver` provides bounded capture, key/text input, and deterministic waits for a headless attachment. Existing clients stay unchanged unless `--ui-observe` or `--ui-control` is explicitly enabled. See [UI driver](docs/ui-driver.md) for the JSONL contract, private socket bridge, completion semantics, and isolated endpoint configuration.
96 107
97 ## Scripting108 ## Scripting
98 109
@@ -104,16 +115,17 @@ vev cmd toast -l warn "build failed"
104 vev cmd list-panes --json115 vev cmd list-panes --json
105 ```116 ```
106 117
107-Use `vev cmd --help` for commands and targeting options.118+Use `vev cmd --help` for all commands. For headless capture and input, see [UI driver](docs/ui-driver.md).
108 119
109 ## Documentation120 ## Documentation
110 121
111 - [Configuration](docs/configuration.md): bindings, palette, theme, overlays, and bar anchors122 - [Configuration](docs/configuration.md): bindings, palette, theme, overlays, and bar anchors
112 - [Terminal compatibility](docs/terminal.md)123 - [Terminal compatibility](docs/terminal.md)
113-- [Remote resilience](docs/remote-resilience.md)124+- [Remote sessions](docs/remote-resilience.md)
125+- [Session navigation](docs/session-navigation.md)
114 - [Durable session recovery](docs/durable-session-recovery.md)126 - [Durable session recovery](docs/durable-session-recovery.md)
115-- [UI driver](docs/ui-driver.md): headless capture/control and opt-in interactive observation127+- [Browser terminal](docs/web-terminal.md)
116-- [Performance](docs/performance.md)128+- [UI driver](docs/ui-driver.md)
117 129
118 ## Development130 ## Development
119 131
@@ -125,7 +137,7 @@ VEV_ENV=dev go run . kill --sessions
125 rm -rf .dev/dev137 rm -rf .dev/dev
126 ```138 ```
127 139
128-Each name shares one daemon and its vev-owned files under `.dev/<name>/`; use separate names to run binaries side by side. This isolates vev configuration, runtime files, durable sessions, hosts, snapshots, and logs, but it is not an OS, filesystem, or network sandbox.140+Each name gets its own daemon, config, sessions, and logs under `.dev/<name>/`. It is not a sandbox.
129 141
130 ```sh142 ```sh
131 make test # go test ./... -race143 make test # go test ./... -race
@@ -133,3 +145,5 @@ make lint # goimports check, go vet
133 make mocks # regenerate mocks145 make mocks # regenerate mocks
134 make demo # regenerate docs/assets/demo.gif (needs Docker)146 make demo # regenerate docs/assets/demo.gif (needs Docker)
135 ```147 ```
148+
149+Contributor docs: [architecture](docs/architecture.md), [protocol](docs/protocol.md), [performance measurement](docs/performance.md).
M docs/architecture.md +26 -458
@@ -10,7 +10,7 @@ vev uses a hexagonal core with typed session messages at the client and daemon b
1010 | `failed` | A definite failure, including validation/cancellation before dispatch. | code is nonzero; output is empty. |
1111 | `outcome_unknown` | Dispatch was attempted but no final outcome can be established (timeout, cancellation, EOF, or lost reply). | code and output are empty; callers must not replay automatically. |
1212
13-Every result retains the request ID for strict correlation. Version negotiation requires exact equality, so protocol 58 intentionally has no compatibility fallback for the deleted legacy `ok` field.
13+Every result keeps its request ID for strict correlation.
1414
1515 ## Package ownership
1616
@@ -31,38 +31,14 @@ Every result retains the request ID for strict correlation. Version negotiation
3131 (PreambleRequest first, PreambleResponse acceptance, exact version,
3232 negotiated ceilings, one absolute deadline), direction checks, and
3333 decode-failure classification.
34-- `internal/adapters/brokerwire`: translates between typed broker messages
35- and the separate broker Protobuf conversation (`schema/broker.proto`;
36- client tags 101-111, server tags 201-209, disjoint from every session
37- tag, each direction its own closed `oneof`). It owns the stateless
38- broker codec over `wire.ScanEnvelope`, the broker preamble (roles 3/4,
39- exact `protocol.Version`, 1 MiB..16 MiB / 1..64 KiB ceilings, 4 KiB
40- byte-length bound), multipart snapshot assembly, per-connection operation
41- and stream trackers, and the bounded admission lock. Production clients
42- use this codec through the broker IPC adapter.
43-- `internal/adapters/brokeripc`: the Plan 001 P3.3 private local broker
44- endpoint. It implements `ports.BrokerListener` (accepting same-user clients
45- and running one brokerwire session per accepted connection) and
46- `ports.BrokerService` (the client adapter over the same conversation) on one
47- per-user AF_UNIX endpoint, reusing the P3.2 private IPC carriage
48- (`ipc.ListenMux`/`ipc.DialMuxContext`: owner-only directory, 0600 socket,
49- same-user `SO_PEERCRED` admission, race-safe stale-socket recovery,
50- foreign-path refusal) and the shared `streamframe` framing. It owns the
51- bounded per-connection queues, the snapshot publisher, the per-stream typed
52- bridge over `sessionwire`, and deterministic disconnect cleanup. Production
53- clients connect through this endpoint.
34+- `internal/adapters/brokerwire`: typed broker messages ↔ the broker Protobuf conversation (`schema/broker.proto`). See [Broker](#broker).
35+- `internal/adapters/brokeripc`: the private `broker.sock` endpoint. Implements `ports.BrokerListener` and `ports.BrokerService`.
5436 - `internal/adapters/quic`: one-stream QUIC carriage (TLS 1.3, epoch ALPN,
5537 exact SHA-256 pin, single bidirectional stream, bounded admission) plus
5638 the short-lived SSH bootstrap (ephemeral certificate, 32-byte token,
5739 nonce, ≤4 KiB readiness, ≤15 s expiry, atomic one-time consumption).
5840 QUIC library types never leave the package.
59-- `internal/adapters/brokerconfig`: owns the strict isolated offline broker
60- sandbox configuration (`config.json` marker), the textual offline-root
61- validation that keeps the sandbox out of production runtime/state, and the
62- immutable `ports.BrokerEndpointResolver` over the provisioned endpoint
63- registrations, identities, policies, and absolute Unix mux routes. It is
64- composed only by the hidden offline broker entry points (`_broker-serve`,
65- `_broker-launcher`, and `_broker-status`).
41+- `internal/adapters/brokerconfig`: parses `broker.json` (registrations, local binding, warm-transport settings) and provides the immutable `ports.BrokerEndpointResolver`.
6642 - `internal/adapters/uidriver`: owns strict JSONL decoding, response serialization, bounded controller queues, private Unix sockets, peer credentials, and the stdio bridge. It consumes `ports.UIService` and never creates an attachment.
6743 - `internal/adapters/webterm`: owns the authenticated loopback HTTP/WebSocket frontend, browser event encoding, VT-backed terminal and transactional HTML output. It implements `ports.Terminal`; application composition supplies the ordinary client `Supervisor`.
6844 - `internal/adapters/uiterm`: owns the immutable VT mirror/snapshot and deterministic headless terminal. The concrete terminal writer (`adapters/term`) taps successful writes and flushes into this sink only when observation is enabled.
@@ -153,433 +129,25 @@ Use cases exchange `protocol.ClientMessage` and `protocol.ServerMessage` values.
153129 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.
154130 - Bump `internal/protocol.Version` for negotiated wire layout changes (currently `59`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.
155131
156-## Broker wire core (Plan 001 P3.1, not activated)
157-
158-`internal/adapters/brokerwire` owns the broker's separate Protobuf
159-conversation end to end: client tags 101-113 and server tags 201-210 are
160-disjoint from every session tag, and each direction is its own closed
161-directional `oneof` union. A broker connection opens with exactly one
162-broker preamble (roles 3/4) that carries the same magic, epoch, exact
163-`protocol.Version`, and 1 MiB..16 MiB / 1..64 KiB ceilings as the session
164-handshake; a serialized preamble is bounded to 4 KiB (`wire.PreambleLimit`,
165-re-exported through `brokerwire.CheckPreambleSize`) before scan or
166-allocation. The broker negotiates no optional capabilities, so nonzero
167-capability bits are refused with code 7.
168-
169-The package is stateful only where P3.1 requires it: per-connection state
170-(ready/close, one subscription generation series, one assembler), multipart
171-snapshot assembly with an 80 MiB staged ceiling that charges at least 64
172-bytes per tab, and bounded operation/stream trackers. Every worker method
173-holds the connection lock across both the ready check and the tracker
174-mutation, so a racing close can never admit work after shutdown. The codec
175-itself stays stateless over `wire.ScanEnvelope`. No socket, framing pump,
176-or P3.2 transport is introduced here.
177-
178-## Broker connection pool
179-
180-`internal/usecase/broker.Pool` owns bounded physical keys and logical stream
181-reservations, but no session data or traffic queues. Local and registered remote
182-requests use the same operation. A broker-owned `BrokerEndpointResolver` checks
183-current registration authority and exact requested policy on every request and
184-returns an authenticated service binding. Display aliases and adapter addresses
185-are not keys. Identity plus complete trust, launch, isolation, transport,
186-protocol, catalogue, and environment policy select one physical connection;
187-authenticated identity and policy are checked again after connection.
188-
189-Clients receive pool-issued epoch-scoped connection IDs and allocate their own
190-logical stream IDs through `BrokerService.NextStreamID`, which is
191-thread-safe, strictly monotone, never zero, and refuses an exhausted counter
192-instead of wrapping. An allocated ID is consumed even when the open it names is
193-refused. Admission at every layer (client, listener, pool) uses one bounded
194-anti-replay window of 1024 IDs instead of a bare monotone comparison, so two
195-concurrent opens that are scheduled in either order are both admitted while a
196-replayed or evicted ID is stale. Active clients, physical keys, and
197-pending/live streams have explicit caps; retired IDs need no unbounded tombstone
198-collection. Coalesced connection attempts are independent of any single waiter
199-and are canceled when their final reservation leaves. Acquisition coalescing
200-keys on the exact policy plus the explicit `BrokerDaemonStartMode`, so an
201-existing-only request is never shared with a dial that may start the target;
202-after authentication the canonical key is identity plus exact policy alone.
203-Physical retirement keeps its key occupied until Close finishes. The pool keeps
204-at most eight idle physical transports warm by default, including connections
205-established by a first successful probe. `warmTransports: 0` disables warm
206-reuse; `warmIdleTimeout` defaults to `off` and may set an idle deadline.
207-Broker shutdown always closes warm transports. Fake-clock idle eviction and
208-shutdown close transports outside the bookkeeping lock.
209-
210-Logical and physical ports expose terminal signals independent of message reads.
211-A stream terminal failure releases only its reservation; physical failure fans
212-out separately scoped typed loss errors. The pool owns neither framing nor
213-fairness: transport adapters implement independently bounded queues and
214-prompt cancellation/Close. Shutdown joins owned operations and relies on
215-that explicit port contract. Production clients use this pool for local and
216-remote logical streams.
217-
218-Physical `Done` is authoritative: adapters publish stable `Err` and
219-`FailureKind`, then close physical `Done` before terminating affected logical
220-streams. Loss carries the exact epoch/connection/stream and a nonzero failure
221-kind (invalid/absent adapter kinds defensively become transport failure), with
222-its diagnostic cause retained locally through `errors.Is/As`. Resolver,
223-connector, and open errors map deterministically: deadline before cancellation,
224-then a validated broker code, otherwise unavailable. Invalid resolved bindings
225-and nil successful opens are incompatible adapter results. Adapter diagnostics
226-are never copied to display text. Resolved addresses are bounded opaque tokens,
227-not display labels or pooling identities.
228-
229-At terminal observation the precedence is pool shutdown, request/client
230-cancellation, physical loss, entry retirement, then logical completion. Thus
231-shutdown reports cancelled rather than a synthetic transport loss caused by
232-local Close; a physical loss already published to a stream is not rewritten by
233-later shutdown. Explicit logical Close remains idempotent and preserves the
234-underlying Close error. Timer ownership is single-goroutine; Stop followed by a
235-nonblocking drain on false precedes Reset, supporting buffered timer adapters.
236-Policy identities have independent field-labelled validation and a named bound.
237-These are P2.2 port contracts, not a wire migration or production activation.
238-
239-## Broker registry and daemon observation (Plan 001 P5.2a, not activated)
240-
241-`internal/usecase/broker.Registry` owns configured hosts and their immutable
242-observation projections. `ports.BrokerDaemonObservation` is the broker-native
243-projection of one daemon, local or remote: configured authority (local flag,
244-endpoint, registration, policy, display origin, rank) is never taken from a
245-probe, while observed identity, process incarnation, protocol version,
246-capability bitmask, availability, failure/freshness state, and the exact
247-session catalogue come only from probe results. A remote host's authority is
248-stamped by the registry from durable membership; the local entry's authority is
249-stamped from the broker's configured local binding, because the local daemon
250-has no registration and no durable membership. Unknown identity or version is
251-zero, never invented; an observed identity always carries a protocol version,
252-and availability is never zero (an unobserved daemon carries
253-`RemoteAvailabilityUnknown`). A
254-`ports.BrokerSnapshot` publishes these observations (`Daemons`, local entry
255-first when present, then remotes in registration order) plus the tombstone
256-set; `brokerwire` mirrors them through `SnapshotBegin`/`SnapshotDaemon`/
257-`SnapshotSession` parts. Local observations are never durable: the snapshot
258-store persists and reloads remote observations only, with remote policy
259-re-stamped from membership on load, so membership stays the single policy
260-authority for remotes. Observing a daemon never attaches to it and never starts
261-a stopped one.
262-Attachment streams declare one closed admission variant
263-(`ports.BrokerStreamAdmission`: exact attach/resume, named creation, ephemeral
264-creation) validated identically at the ports, broker IPC, and daemonmux
265-boundaries. Every stream also declares one closed daemon-start authorization
266-(`ports.BrokerDaemonStartMode`: `ExistingOnly` or `StartIfNeeded`), carried
267-unchanged from the open request through resolution, the wire, and the mux Open:
268-observation and daemon-stop are always `ExistingOnly` and never start a target,
269-while attach/creation and the explicit list/mutate controls may be
270-`StartIfNeeded`; the zero value is refused. The mode can only narrow the
271-resolved launch policy, never widen it.
272-
273-## Broker-owned local route (Plan 001 P5.3a, not activated)
274-
275-The broker reaches its own machine daemon, which is not a configured host, over
276-one broker-owned local binding. `internal/adapters/brokerconfig` parses an
277-optional top-level `local` object beside the registrations: the required
278-`identity`, `route`, and `policy`, plus an optional `displayOrigin` that is
279-sanitized and defaults to `local`. The route must be a Unix daemonmux carriage
280-this process dials directly, so an SSH or QUIC route is refused and the binding
281-can never name another machine. The identity, policy, and route are the only
282-source of local authority, and `Config.LocalBinding` alone publishes them. The
283-binding's own carriage is deliberately excluded from `Config.LocalMuxRoute`:
284-that route is the single registration carriage a remote-side mux helper bridges
285-to, and no helper ever bridges the broker's own local carriage.
286-
287-A local open-stream request carries no endpoint and no registration, so the
288-resolver fences it against the binding's identity and policy instead: a request
289-whose policy conflicts with the provisioned local policy is refused, and a
290-configuration with no local object provisions no local route. The resolved
291-(identity, policy) pair is the pool key, so every local logical stream shares
292-one physical daemonmux carriage to the local daemon, exactly as every stream to
293-one remote identity does; the route's opaque address selects that carriage and
294-never participates in the key.
295-
296-The registry publishes a local observation whenever a local producer is
297-configured, always at index zero ahead of every remote and never more than one.
298-Its configured authority (local flag, display origin, policy, rank) is stamped
299-from the binding and never from durable membership, since the local daemon has
300-no membership; its observed identity, incarnation, protocol version,
301-availability, and session catalogue come only from the local probe. The local
302-entry is never durable: the snapshot store persists and reloads remote
303-observations only. Observing the local daemon dials only an existing local Unix
304-socket, so it never starts a stopped daemon; it completes the authenticated
305-daemonmux physical preamble, opens exactly one `BrokerStreamObservation` logical
306-stream, sends one typed remote-catalog control request, validates the catalogue
307-the daemon answers, and closes the stream and carriage. It sends no `Hello`,
308-claims no geometry, requests no admission, creates no session, process, or
309-durable identity, and never attaches. The remote host probe and the local probe
310-share one semantic implementation, so the two cannot drift apart. Nothing in
311-`internal/app` or `main` activates this composition for ordinary operation; P7
312-performs the coordinated cutover.
313-
314-## Broker-owned daemon start (Plan 001 P7, UI-driver slice 1)
315-
316-The one place a resolved start authorization reaches transport mechanics is
317-`internal/app/broker_daemon_start.go`. `daemonmux.RawCarrierDialer` receives the
318-whole `ports.BrokerDialTarget` — address, fence, policy, `StartMode`, and
319-expected identity — instead of a bare opaque address, so an authorization can
320-never be flattened into an address or smuggled through a context value, and the
321-carriage owner sees exactly the authority the connector validated and is about
322-to verify in the physical preamble. `brokerMuxConnector` looks the route up by
323-the target's opaque address, re-checks that the provisioned entry still matches
324-the resolved fence and policy, and passes the whole target on.
325-
326-`dialBrokerLocalDaemon` dials one local daemon for one resolved target.
327-`BrokerDaemonExistingOnly` is exactly one private daemonmux carriage dial: no
328-lifecycle probe, no spawn lock, no process, and no fallback of any kind.
329-`BrokerDaemonStartIfNeeded` performs the same dial first and returns the daemon
330-it finds — an already running daemon is never restarted. Only a dial that proves
331-absence (an unclassified failure, a cancellation, a foreign path, a rejected
332-peer, and a permission refusal all report false) can reach the start, and then
333-only when the resolved policy names the broker launch authority and the carriage
334-is exactly the private endpoint the launched daemon will publish. A registered
335-endpoint is never started by the local mechanics. The start itself is the shared
336-election in `spawn.go` — lifecycle availability, one elected spawner under the
337-spawn lock, then a bounded redial of the same carriage — so observation and
338-daemon-stop stay `ExistingOnly` and a policy that forbids launching still
339-refuses visibly. `spawn.go` is generic over the dialed value, so the session
340-transport and the daemonmux carriage obey exactly one election discipline, and
341-no path here ever dials a session or a session control stream to prove
342-readiness.
343-
344-The remote-side mux helpers apply the same rule from their own broker-owned
345-configuration. `dialBrokerRoute` propagates the target's `StartMode` before any
346-connect-or-spawn, `sshMuxCommandSpec` appends the closed
347-`--daemon-start existing-only|if-needed` word to the provisioned remote argv,
348-and `parseBrokerMuxArgs` defaults an absent flag to the safe `existing-only`
349-while refusing an unknown value. A helper starts only the daemon behind its own
350-provisioned mux carriage, under its own policy, and never starts a broker, an
351-observer, or a recursive helper.
352-
353-## Broker local IPC (Plan 001 P3.3, not activated)
354-
355-`internal/adapters/brokeripc` carries the P3.1 broker conversation over one
356-per-user Unix endpoint. The carriage is the P3.2 private AF_UNIX carriage in
357-`internal/adapters/ipc`: an owner-only (0700, `pkg/safedir.EnsurePrivate`)
358-parent directory, a bound socket tightened to 0600, race-safe recovery of a
359-stale socket whose owner died, refusal (never removal) of a path that is not a
360-socket, and same-user kernel peer credentials (`SO_PEERCRED` on Linux) on both
361-accept and dial, failing closed on a platform or build without that check. The endpoint name (`broker.sock`) is the client-facing local endpoint. The daemon's Unix daemonmux socket is local-only composition infrastructure owned by the broker/daemon path; it is not published as a client API and ordinary clients never dial it.
362-
363-The listener owns its accept loop: one client slot is acquired before each
364-accept, each accepted carriage completes the broker preamble and broker
365-admission in its own goroutine, and admitted sessions are published through a
366-bounded FIFO to `Accept`. A client that stalls during the preamble therefore
367-occupies one slot and one goroutine, never the loop. Each session runs the
368-brokerwire connection state machine (one Register, one subscription generation
369-series, bounded operation and stream trackers), one snapshot publisher that
370-writes the P3.1 multipart layout and coalesces through the core's capacity-one
371-subscription, and one bridge per logical stream. Streams carry the typed
372-session protocol: `sessionwire` runs over a bounded private byte pipe on each
373-side, and two relay goroutines move typed messages between the client's session
374-connection and the admitted broker core connection, so no session byte is
375-reinterpreted.
376-
377-Every frame must carry the epoch and connection identity the listener assigned
378-at accept; a mismatched frame is fenced, and a stale stream open is answered
379-with a `StreamClosed` carrying the stale admission code. A malformed,
380-oversize, or wrong-direction envelope settles exactly the connection that sent
381-it. Setup is fully bounded on both halves: the client dialer bounds the Unix
382-dial, the preamble, the Register send, and the wait for Registered with one
383-context/deadline, closing the carriage to interrupt a peer that stalls after a
384-successful preamble and joining every setup worker, while a registered
385-connection detaches from that setup context; the server requires the client's
386-Register within the same accept-time handshake budget that bounded the preamble
387-and admission, so a silent same-user peer can neither pin an admitted core lease
388-nor hold a listener slot. Admission is the `ports.BrokerAuthority` seam,
389-implemented by `internal/usecase/broker.Authority`; the pre-Register guard
390-consults a registered marker set when Register handling begins, so a Register
391-arriving exactly at the deadline is admitted rather than settled as a timeout,
392-and a genuine registration timeout is classified as an orderly peer disconnect
393-so a listener draining its sessions never reports one as its own failure.
394-Operational bounds are explicit: concurrent clients
395-per listener, per-stream inbound chunks and bytes, pending mutating operations,
396-and the brokerwire per-connection stream and dedup bounds. Disconnect cleanup is
397-deterministic:
398-the session cancels its context, stops publishing, closes the outer carriage
399-(so a stream writer parked on a non-reading peer is interrupted), settles every
400-bridged stream (releasing its core connection and carriage), closes the
401-brokerwire state, joins every worker, closes the admitted core service, and
402-releases its client slot. Mutating operations report `outcome unknown` when a
403-reply is lost after the request was sent, and the client never replays them
404-blindly.
405-
406-P3.3 introduces no production activation: nothing in `internal/app` or `main`
407-constructs this adapter for ordinary operation. P3.4 composes the broker use
408-case, its stores, and this listener behind the hidden `_broker-serve` sandbox
409-entry point, with the listener consuming
410-`ports.BrokerAuthority` instead of defining its own seam. The composing caller
411-owns the `Registry.Run` lifecycle: it starts the single run to own and drain the
412-durable writer and cancels and joins that run before closing the store, because
413-`NewAuthority` neither starts nor settles the run. P7 performs the coordinated
414-cutover.
415-
416-## Broker offline sandbox (Plan 001 P3.4 slice C, not activated)
417-
418-The hidden `_broker-serve --offline-root ABS [--idle-grace DURATION]` entry
419-point is a foreground, isolated sandbox over one operator-supplied offline root.
420-It is excluded from public help and changes no ordinary command, path, or
421-factory. The root must be absolute, cleaned, symlink-free, and outside the
422-production runtime and state directories; the runtime, state, and log paths are
423-derived only beneath it and secured with `pkg/safedir.EnsurePrivate`, and the
424-durable store is opened with `brokerstore.Open` over the sandbox state
425-directory with no legacy inputs.
426-
427-`internal/adapters/brokerconfig` owns the strict sandbox configuration: a
428-bounded, owner-only `config.json` carrying the exact `vev.broker.offline/v1`
429-marker, with unknown fields, duplicate keys, trailing data, invalid UTF-8,
430-excessive nesting, and duplicate endpoints refused. Each immutable registration
431-provisions the exact daemon registration, the stable authenticated daemon
432-identity, the exact connection policy, and one absolute Unix mux route. The
433-resulting `ports.BrokerEndpointResolver` performs no I/O and invents no address:
434-a request must name a configured endpoint, carry its exact registration, and
435-request a compatible policy, so a request can never supply an address, identity,
436-secret, command, or policy of its own. Because the pool keys one physical
437-transport by the exact authenticated identity plus policy pair and deliberately
438-ignores the opaque route address, two registrations that share that pair must
439-agree on the one route the pool dials: an alias of an identical route is
440-accepted under distinct endpoints, while a second route for the same pair is
441-refused instead of being dialed for neither endpoint.
442-
443-The composition samples a cryptographically random non-zero broker epoch and
444-builds a store-backed, observation-disabled `broker.Registry` (whose single
445-`Run` the supervisor owns and joins), the immutable resolver, a
446-`daemonmux.EndpointConnector` over `ipc.DialMuxContext`, a bounded `Pool`, the
447-idle `Supervisor` (5m default grace), `broker.Authority`, and the
448-`brokeripc` listener. A temporary startup lease pins the supervisor open until
449-the listener and its accept drain are ready; the drain continuously takes
450-admitted sessions without closing them, because each returned service is the
451-listener's own connection-scoped session. A signal or parent-context
452-cancellation, or the idle grace, shuts down in the fixed order listener ->
453-pool -> registry -> store -> log -> owner lock. The foreground process holds a
454-reusable lifetime owner lock (`internal/adapters/lifecycle.TryAcquire`) on the
455-sandbox runtime directory for its whole run, so a second owner fails closed.
456-The optional `idleGrace` field in `config.json` provisions the sandbox's
457-effective idle grace as a Go duration string. It is the single deterministic
458-source for that grace: `_broker-serve` and `_broker-launcher` refuse an explicit
459-`--idle-grace` that conflicts with the provisioned value, because a status probe
460-cannot read a running broker's effective grace through the broker IPC protocol.
461-When no grace is provisioned, an explicit flag (or the supervisor default)
462-applies.
463-
464-## Broker offline connect-or-spawn and status (Plan 001 P3.4 slice D, not activated)
465-
466-The hidden `_broker-launcher --offline-root ABS [--idle-grace DURATION]` entry
467-point is the intermediate half of a double fork: it validates the sandbox
468-configuration (including the idle-grace conflict rule), starts one detached
469-`_broker-serve` in a new session, and exits, exactly like the daemon launcher.
470-The hidden `_broker-status --offline-root ABS [--ensure] [--timeout DURATION]`
471-entry point reports the offline broker's state. Both are excluded from public
472-help and change no ordinary command, path, or factory.
473-
474-Status without `--ensure` is dial-only: it performs one bounded probe of the
475-sandbox socket and prints exactly one bounded JSON document,
476-`{status, endpoint, epoch, revision, host_count}`, where `endpoint` is the
477-broker IPC socket path and the epoch, revision, and host count are zero when the
478-broker is offline. A missing socket and a stale socket left by a dead owner are
479-both recoverable absence; a foreign path, a live endpoint that fails the broker
480-handshake, and a stalled handshake are incompatible and never respawned over.
481-`--timeout` is the probe's single absolute bound in dial-only mode, so a caller
482-can tighten the probe or rely on the default (10s) and observe the same bound.
483-Dial-only status always exits zero, so the JSON is the sole result. `--ensure`
484-runs one absolute overall deadline (default 10s): it validates and secures the
485-sandbox root exactly as the launcher does before creating the spawn directory or
486-taking any lock. On success it prints the ready report and exits zero; on any
487-layout, configuration, or startup failure it still prints the offline report but
488-exits 3, so the caller always observes one offline JSON document and a distinct
489-exit code. The launcher is spawned under the ensure context
490-(`exec.CommandContext`), so the overall deadline or a signal bounds a launcher
491-that never exits; a launcher that already started the detached `_broker-serve`
492-is still safe to kill, because that serve is in its own session and survives.
493-
494-A foreground `_broker-serve` that hits an unexpected terminal accept failure
495-commits the same ordered shutdown as a cancellation, so a socket that can no
496-longer admit work is never left bound.
497-
498-Race safety rests on two descriptor-backed exclusive locks that are never
499-stolen by age and whose inodes are never unlinked. Both refuse any lock with
500-group or other access and trust an owner-only file whatever its exact owner bit
501-pattern, so a lock created under a restrictive umask (normalized to 0600 on
502-creation) or by a different tool never wedges startup; a group/other-readable
503-path stays refused. The broker lifetime lock
504-(`layout.Runtime/lifecycle.lock`) is held by the foreground `_broker-serve` for
505-its whole run; only the elected spawner probes it, and only as a liveness hint.
506-The spawn-election lock (`layout.Spawn/lifecycle.lock`, a separate private
507-directory beneath runtime) elects exactly one status caller to spawn. The
508-elected spawner re-dials after winning the lock, then holds the election until
509-the endpoint answers a valid broker registration and publishes its initial
510-snapshot, or until the deadline expires. A waiter never touches the lifetime
511-lock, so it can never briefly steal ownership from the broker it is waiting for,
512-and every wait is a bounded retry rather than a sleep loop. If the elected
513-spawner dies before readiness, the kernel releases its lock and a waiter takes
514-over; a duplicate `_broker-serve` fails its own lifetime acquisition
515-nonblocking. Every spawn uses the existing double-fork process pattern, and the
516-status probe and launcher inherit only the ambient environment minus the
517-performance-trace inputs.
518-
519-## Broker remote mux helpers (Plan 001 P3.4 slice E, not activated)
520-
521-The offline broker reaches a remote daemon's private daemonmux carriage over one
522-of three explicitly provisioned, bounded routes, and P3.4 slice E adds the
523-transport-selecting connector plus three hidden remote helpers. The routing
524-schema is strict and immutable: `internal/adapters/brokerconfig` parses a
525-registration's `route` as either the original Unix mux path string or an object
526-with an explicit kind, and refuses unknown kinds, unknown fields, trailing data,
527-and any kind/field mismatch. The bounded `ssh-stdio` and `ssh-quic` variants
528-carry an ssh target, an explicit remote argv, and optional trust inputs (a
529-known-hosts file and a connect timeout). A route can never carry an identity, a
530-policy, a command chosen at request time, or a secret. Identity and policy stay
531-authoritative on the registration, and the resolver publishes only an opaque
532-bounded route address: a digest of the route's own contents, so the pooling
533-layer never carries a target, a path, or an argv word.
534-
535-`_broker-serve` composes a transport-selecting `daemonmux.EndpointConnector`. Its
536-dial function receives the whole resolved `ports.BrokerDialTarget`, looks the
537-route up by its opaque address in the immutable configuration it was built
538-with, refuses an address the configuration did not produce, and re-checks that
539-the provisioned entry still matches the resolved fence and policy before
540-dialing. The target travels whole so the closed daemon-start authorization
541-reaches the transport owner instead of being flattened into an address. A
542-`unix` route dials the provisioned private Unix
543-mux carriage directly when it is a provisioned endpoint carriage, and goes
544-through the broker-owned daemon start (below) when it is the broker's own local
545-binding route; an `ssh-stdio` route starts one explicit ssh child (built
546-with `sshstdio.BuildCommandForMux` and carrying only trust inputs that narrow
547-verification, never disabling host-key checking, forcing an auth bypass, or
548-requesting a PTY) through `sshstdio.DialMuxContext`, and appends the closed
549-`--daemon-start existing-only|if-needed` word to the provisioned remote argv; an
550-`ssh-quic` route runs one
551-SSH-authenticated QUIC bootstrap, reads exactly one readiness line, and pins the
552-freshly minted ephemeral certificate through `quic.DialMuxContext`. One
553-caller-supplied setup context bounds bootstrap, pinned dial, and the daemonmux
554-physical handshake together; `brokerMuxSetupTimeout` is an independent backstop
555-for the broker-side bootstrap readiness wait and the child's kill/reap cleanup,
556-so an earlier caller deadline stays authoritative while a helper that never
557-prints readiness is still interrupted and reaped within the bound. The captured
558-bootstrap stderr sink is read only after that kill and reap, and the sink is
559-itself synchronized, so the os/exec copy goroutine never races the diagnostic. A
560-successful physical connection detaches from that context and stays pool-owned.
561-QUIC credentials are minted fresh per physical
562-connection and consumed exactly once, and captured bootstrap stderr is bounded
563-and sanitized, so a token, a nonce, or raw remote text never reaches an address,
564-a log, or an error.
565-
566-The three hidden helpers are excluded from public help. `_broker-mux-stdio
567---offline-root ABS [--daemon-start existing-only|if-needed]` validates the
568-sandbox and bridges its own stdio to the single
569-provisioned private Unix daemonmux carriage, applying exactly the start
570-authorization the broker propagated: an absent flag defaults to the safe
571-`existing-only`, an unknown value is refused, and a `start-if-needed` request is
572-gated by the helper's own provisioned identity and policy, which can only narrow
573-it. `_broker-mux-quic-bootstrap
574---offline-root ABS` validates the sandbox, starts one detached
575-`_broker-mux-quic-proxy` in a new session, forwards its single readiness line,
576-and exits. `_broker-mux-quic-proxy --offline-root ABS` mints one ephemeral QUIC
577-server, admits exactly one authenticated carriage, and bridges it to the same
578-private Unix daemonmux carriage under the same propagated authorization. A
579-helper only ever bridges to the configured
580-private endpoint: it never starts a broker, an observer, or an ordinary daemon
581-outside that one daemon-start authorization, never dials the production daemon
582-socket, and never fabricates a daemon
583-incarnation, which the daemon on the far side of the carriage alone owns.
584-Ordinary production composition still waits for P7; the helpers change no
585-ordinary command, path, or factory.
132+## Broker
133+
134+The broker is a per-user process that sits between every client and every daemon. It starts on first use and stops after 5 minutes idle (`broker.DefaultIdleGrace`).
135+
136+```text
137+client ──broker.sock──▶ broker ──daemonmux──▶ local daemon
138+ │
139+ └──SSH stdio or SSH+QUIC──▶ remote daemon
140+```
141+
142+What it owns:
143+
144+- **Hosts.** `internal/usecase/broker.Registry` holds configured hosts and publishes one `ports.BrokerSnapshot`: the local daemon first, then remotes. Configured authority (endpoint, policy, rank) comes from membership; observed facts (identity, version, availability, session catalogue) come only from probes. Only remote observations are persisted.
145+- **Connections.** `broker.Pool` keys one physical connection by authenticated identity plus exact policy. Many logical streams share it. Display aliases and addresses are never keys. Idle connections stay warm (`warmTransports`, `warmIdleTimeout`).
146+- **Daemon start.** Every stream carries a `ports.BrokerDaemonStartMode`: `ExistingOnly` (observation, stop) or `StartIfNeeded` (attach, create). The mode can narrow the launch policy, never widen it. A running daemon is never restarted. The start path lives in `internal/app/broker_daemon_start.go` and uses the shared spawn election in `spawn.go`.
147+- **Wire.** `internal/adapters/brokerwire` owns the broker's own Protobuf conversation (`schema/broker.proto`, client tags 101+, server tags 201+). It uses the same preamble, epoch, and exact `protocol.Version` as sessions. Streams carry the typed session protocol unchanged.
148+- **Local IPC.** `internal/adapters/brokeripc` serves `broker.sock`: owner-only directory, `0600` socket, same-user peer credentials. Setup, queues, and clients are bounded. A lost reply to a mutating operation is reported as `outcome unknown` and never replayed.
149+- **Remote helpers.** Hidden commands `_broker-mux-stdio`, `_broker-mux-quic-bootstrap`, and `_broker-mux-quic-proxy` run on the remote host and bridge to that host's private daemonmux socket. They take a closed `--daemon-start existing-only|if-needed` flag (default `existing-only`). QUIC credentials are fresh per connection and used once.
150+
151+Other hidden entry points: `_broker-production-serve`, `_broker-production-launcher`, and `_broker-ready` (a dial-only readiness probe that prints one `vev.broker-ready/v1` JSON line).
152+
153+Locks: the broker holds `lifecycle.lock` in its runtime directory while running. A separate spawn lock elects one process to start it. Locks are never stolen by age.
@@ -10,7 +10,7 @@ vev uses a hexagonal core with typed session messages at the client and daemon b
10 | `failed` | A definite failure, including validation/cancellation before dispatch. | code is nonzero; output is empty. |10 | `failed` | A definite failure, including validation/cancellation before dispatch. | code is nonzero; output is empty. |
11 | `outcome_unknown` | Dispatch was attempted but no final outcome can be established (timeout, cancellation, EOF, or lost reply). | code and output are empty; callers must not replay automatically. |11 | `outcome_unknown` | Dispatch was attempted but no final outcome can be established (timeout, cancellation, EOF, or lost reply). | code and output are empty; callers must not replay automatically. |
12 12
13-Every result retains the request ID for strict correlation. Version negotiation requires exact equality, so protocol 58 intentionally has no compatibility fallback for the deleted legacy `ok` field.13+Every result keeps its request ID for strict correlation.
14 14
15 ## Package ownership15 ## Package ownership
16 16
@@ -31,38 +31,14 @@ Every result retains the request ID for strict correlation. Version negotiation
31 (PreambleRequest first, PreambleResponse acceptance, exact version,31 (PreambleRequest first, PreambleResponse acceptance, exact version,
32 negotiated ceilings, one absolute deadline), direction checks, and32 negotiated ceilings, one absolute deadline), direction checks, and
33 decode-failure classification.33 decode-failure classification.
34-- `internal/adapters/brokerwire`: translates between typed broker messages34+- `internal/adapters/brokerwire`: typed broker messages ↔ the broker Protobuf conversation (`schema/broker.proto`). See [Broker](#broker).
35- and the separate broker Protobuf conversation (`schema/broker.proto`;35+- `internal/adapters/brokeripc`: the private `broker.sock` endpoint. Implements `ports.BrokerListener` and `ports.BrokerService`.
36- client tags 101-111, server tags 201-209, disjoint from every session
37- tag, each direction its own closed `oneof`). It owns the stateless
38- broker codec over `wire.ScanEnvelope`, the broker preamble (roles 3/4,
39- exact `protocol.Version`, 1 MiB..16 MiB / 1..64 KiB ceilings, 4 KiB
40- byte-length bound), multipart snapshot assembly, per-connection operation
41- and stream trackers, and the bounded admission lock. Production clients
42- use this codec through the broker IPC adapter.
43-- `internal/adapters/brokeripc`: the Plan 001 P3.3 private local broker
44- endpoint. It implements `ports.BrokerListener` (accepting same-user clients
45- and running one brokerwire session per accepted connection) and
46- `ports.BrokerService` (the client adapter over the same conversation) on one
47- per-user AF_UNIX endpoint, reusing the P3.2 private IPC carriage
48- (`ipc.ListenMux`/`ipc.DialMuxContext`: owner-only directory, 0600 socket,
49- same-user `SO_PEERCRED` admission, race-safe stale-socket recovery,
50- foreign-path refusal) and the shared `streamframe` framing. It owns the
51- bounded per-connection queues, the snapshot publisher, the per-stream typed
52- bridge over `sessionwire`, and deterministic disconnect cleanup. Production
53- clients connect through this endpoint.
54 - `internal/adapters/quic`: one-stream QUIC carriage (TLS 1.3, epoch ALPN,36 - `internal/adapters/quic`: one-stream QUIC carriage (TLS 1.3, epoch ALPN,
55 exact SHA-256 pin, single bidirectional stream, bounded admission) plus37 exact SHA-256 pin, single bidirectional stream, bounded admission) plus
56 the short-lived SSH bootstrap (ephemeral certificate, 32-byte token,38 the short-lived SSH bootstrap (ephemeral certificate, 32-byte token,
57 nonce, ≤4 KiB readiness, ≤15 s expiry, atomic one-time consumption).39 nonce, ≤4 KiB readiness, ≤15 s expiry, atomic one-time consumption).
58 QUIC library types never leave the package.40 QUIC library types never leave the package.
59-- `internal/adapters/brokerconfig`: owns the strict isolated offline broker41+- `internal/adapters/brokerconfig`: parses `broker.json` (registrations, local binding, warm-transport settings) and provides the immutable `ports.BrokerEndpointResolver`.
60- sandbox configuration (`config.json` marker), the textual offline-root
61- validation that keeps the sandbox out of production runtime/state, and the
62- immutable `ports.BrokerEndpointResolver` over the provisioned endpoint
63- registrations, identities, policies, and absolute Unix mux routes. It is
64- composed only by the hidden offline broker entry points (`_broker-serve`,
65- `_broker-launcher`, and `_broker-status`).
66 - `internal/adapters/uidriver`: owns strict JSONL decoding, response serialization, bounded controller queues, private Unix sockets, peer credentials, and the stdio bridge. It consumes `ports.UIService` and never creates an attachment.42 - `internal/adapters/uidriver`: owns strict JSONL decoding, response serialization, bounded controller queues, private Unix sockets, peer credentials, and the stdio bridge. It consumes `ports.UIService` and never creates an attachment.
67 - `internal/adapters/webterm`: owns the authenticated loopback HTTP/WebSocket frontend, browser event encoding, VT-backed terminal and transactional HTML output. It implements `ports.Terminal`; application composition supplies the ordinary client `Supervisor`.43 - `internal/adapters/webterm`: owns the authenticated loopback HTTP/WebSocket frontend, browser event encoding, VT-backed terminal and transactional HTML output. It implements `ports.Terminal`; application composition supplies the ordinary client `Supervisor`.
68 - `internal/adapters/uiterm`: owns the immutable VT mirror/snapshot and deterministic headless terminal. The concrete terminal writer (`adapters/term`) taps successful writes and flushes into this sink only when observation is enabled.44 - `internal/adapters/uiterm`: owns the immutable VT mirror/snapshot and deterministic headless terminal. The concrete terminal writer (`adapters/term`) taps successful writes and flushes into this sink only when observation is enabled.
@@ -153,433 +129,25 @@ Use cases exchange `protocol.ClientMessage` and `protocol.ServerMessage` values.
153 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.129 - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`.
154 - Bump `internal/protocol.Version` for negotiated wire layout changes (currently `59`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.130 - Bump `internal/protocol.Version` for negotiated wire layout changes (currently `59`). The preamble epoch (`wire.ProtocolEpoch`, QUIC ALPN `vev/1`) bumps only for an intentional clean break.
155 131
156-## Broker wire core (Plan 001 P3.1, not activated)132+## Broker
157-133+
158-`internal/adapters/brokerwire` owns the broker's separate Protobuf134+The broker is a per-user process that sits between every client and every daemon. It starts on first use and stops after 5 minutes idle (`broker.DefaultIdleGrace`).
159-conversation end to end: client tags 101-113 and server tags 201-210 are135+
160-disjoint from every session tag, and each direction is its own closed136+```text
161-directional `oneof` union. A broker connection opens with exactly one137+client ──broker.sock──▶ broker ──daemonmux──▶ local daemon
162-broker preamble (roles 3/4) that carries the same magic, epoch, exact138+ │
163-`protocol.Version`, and 1 MiB..16 MiB / 1..64 KiB ceilings as the session139+ └──SSH stdio or SSH+QUIC──▶ remote daemon
164-handshake; a serialized preamble is bounded to 4 KiB (`wire.PreambleLimit`,140+```
165-re-exported through `brokerwire.CheckPreambleSize`) before scan or141+
166-allocation. The broker negotiates no optional capabilities, so nonzero142+What it owns:
167-capability bits are refused with code 7.143+
168-144+- **Hosts.** `internal/usecase/broker.Registry` holds configured hosts and publishes one `ports.BrokerSnapshot`: the local daemon first, then remotes. Configured authority (endpoint, policy, rank) comes from membership; observed facts (identity, version, availability, session catalogue) come only from probes. Only remote observations are persisted.
169-The package is stateful only where P3.1 requires it: per-connection state145+- **Connections.** `broker.Pool` keys one physical connection by authenticated identity plus exact policy. Many logical streams share it. Display aliases and addresses are never keys. Idle connections stay warm (`warmTransports`, `warmIdleTimeout`).
170-(ready/close, one subscription generation series, one assembler), multipart146+- **Daemon start.** Every stream carries a `ports.BrokerDaemonStartMode`: `ExistingOnly` (observation, stop) or `StartIfNeeded` (attach, create). The mode can narrow the launch policy, never widen it. A running daemon is never restarted. The start path lives in `internal/app/broker_daemon_start.go` and uses the shared spawn election in `spawn.go`.
171-snapshot assembly with an 80 MiB staged ceiling that charges at least 64147+- **Wire.** `internal/adapters/brokerwire` owns the broker's own Protobuf conversation (`schema/broker.proto`, client tags 101+, server tags 201+). It uses the same preamble, epoch, and exact `protocol.Version` as sessions. Streams carry the typed session protocol unchanged.
172-bytes per tab, and bounded operation/stream trackers. Every worker method148+- **Local IPC.** `internal/adapters/brokeripc` serves `broker.sock`: owner-only directory, `0600` socket, same-user peer credentials. Setup, queues, and clients are bounded. A lost reply to a mutating operation is reported as `outcome unknown` and never replayed.
173-holds the connection lock across both the ready check and the tracker149+- **Remote helpers.** Hidden commands `_broker-mux-stdio`, `_broker-mux-quic-bootstrap`, and `_broker-mux-quic-proxy` run on the remote host and bridge to that host's private daemonmux socket. They take a closed `--daemon-start existing-only|if-needed` flag (default `existing-only`). QUIC credentials are fresh per connection and used once.
174-mutation, so a racing close can never admit work after shutdown. The codec150+
175-itself stays stateless over `wire.ScanEnvelope`. No socket, framing pump,151+Other hidden entry points: `_broker-production-serve`, `_broker-production-launcher`, and `_broker-ready` (a dial-only readiness probe that prints one `vev.broker-ready/v1` JSON line).
176-or P3.2 transport is introduced here.152+
177-153+Locks: the broker holds `lifecycle.lock` in its runtime directory while running. A separate spawn lock elects one process to start it. Locks are never stolen by age.
178-## Broker connection pool
179-
180-`internal/usecase/broker.Pool` owns bounded physical keys and logical stream
181-reservations, but no session data or traffic queues. Local and registered remote
182-requests use the same operation. A broker-owned `BrokerEndpointResolver` checks
183-current registration authority and exact requested policy on every request and
184-returns an authenticated service binding. Display aliases and adapter addresses
185-are not keys. Identity plus complete trust, launch, isolation, transport,
186-protocol, catalogue, and environment policy select one physical connection;
187-authenticated identity and policy are checked again after connection.
188-
189-Clients receive pool-issued epoch-scoped connection IDs and allocate their own
190-logical stream IDs through `BrokerService.NextStreamID`, which is
191-thread-safe, strictly monotone, never zero, and refuses an exhausted counter
192-instead of wrapping. An allocated ID is consumed even when the open it names is
193-refused. Admission at every layer (client, listener, pool) uses one bounded
194-anti-replay window of 1024 IDs instead of a bare monotone comparison, so two
195-concurrent opens that are scheduled in either order are both admitted while a
196-replayed or evicted ID is stale. Active clients, physical keys, and
197-pending/live streams have explicit caps; retired IDs need no unbounded tombstone
198-collection. Coalesced connection attempts are independent of any single waiter
199-and are canceled when their final reservation leaves. Acquisition coalescing
200-keys on the exact policy plus the explicit `BrokerDaemonStartMode`, so an
201-existing-only request is never shared with a dial that may start the target;
202-after authentication the canonical key is identity plus exact policy alone.
203-Physical retirement keeps its key occupied until Close finishes. The pool keeps
204-at most eight idle physical transports warm by default, including connections
205-established by a first successful probe. `warmTransports: 0` disables warm
206-reuse; `warmIdleTimeout` defaults to `off` and may set an idle deadline.
207-Broker shutdown always closes warm transports. Fake-clock idle eviction and
208-shutdown close transports outside the bookkeeping lock.
209-
210-Logical and physical ports expose terminal signals independent of message reads.
211-A stream terminal failure releases only its reservation; physical failure fans
212-out separately scoped typed loss errors. The pool owns neither framing nor
213-fairness: transport adapters implement independently bounded queues and
214-prompt cancellation/Close. Shutdown joins owned operations and relies on
215-that explicit port contract. Production clients use this pool for local and
216-remote logical streams.
217-
218-Physical `Done` is authoritative: adapters publish stable `Err` and
219-`FailureKind`, then close physical `Done` before terminating affected logical
220-streams. Loss carries the exact epoch/connection/stream and a nonzero failure
221-kind (invalid/absent adapter kinds defensively become transport failure), with
222-its diagnostic cause retained locally through `errors.Is/As`. Resolver,
223-connector, and open errors map deterministically: deadline before cancellation,
224-then a validated broker code, otherwise unavailable. Invalid resolved bindings
225-and nil successful opens are incompatible adapter results. Adapter diagnostics
226-are never copied to display text. Resolved addresses are bounded opaque tokens,
227-not display labels or pooling identities.
228-
229-At terminal observation the precedence is pool shutdown, request/client
230-cancellation, physical loss, entry retirement, then logical completion. Thus
231-shutdown reports cancelled rather than a synthetic transport loss caused by
232-local Close; a physical loss already published to a stream is not rewritten by
233-later shutdown. Explicit logical Close remains idempotent and preserves the
234-underlying Close error. Timer ownership is single-goroutine; Stop followed by a
235-nonblocking drain on false precedes Reset, supporting buffered timer adapters.
236-Policy identities have independent field-labelled validation and a named bound.
237-These are P2.2 port contracts, not a wire migration or production activation.
238-
239-## Broker registry and daemon observation (Plan 001 P5.2a, not activated)
240-
241-`internal/usecase/broker.Registry` owns configured hosts and their immutable
242-observation projections. `ports.BrokerDaemonObservation` is the broker-native
243-projection of one daemon, local or remote: configured authority (local flag,
244-endpoint, registration, policy, display origin, rank) is never taken from a
245-probe, while observed identity, process incarnation, protocol version,
246-capability bitmask, availability, failure/freshness state, and the exact
247-session catalogue come only from probe results. A remote host's authority is
248-stamped by the registry from durable membership; the local entry's authority is
249-stamped from the broker's configured local binding, because the local daemon
250-has no registration and no durable membership. Unknown identity or version is
251-zero, never invented; an observed identity always carries a protocol version,
252-and availability is never zero (an unobserved daemon carries
253-`RemoteAvailabilityUnknown`). A
254-`ports.BrokerSnapshot` publishes these observations (`Daemons`, local entry
255-first when present, then remotes in registration order) plus the tombstone
256-set; `brokerwire` mirrors them through `SnapshotBegin`/`SnapshotDaemon`/
257-`SnapshotSession` parts. Local observations are never durable: the snapshot
258-store persists and reloads remote observations only, with remote policy
259-re-stamped from membership on load, so membership stays the single policy
260-authority for remotes. Observing a daemon never attaches to it and never starts
261-a stopped one.
262-Attachment streams declare one closed admission variant
263-(`ports.BrokerStreamAdmission`: exact attach/resume, named creation, ephemeral
264-creation) validated identically at the ports, broker IPC, and daemonmux
265-boundaries. Every stream also declares one closed daemon-start authorization
266-(`ports.BrokerDaemonStartMode`: `ExistingOnly` or `StartIfNeeded`), carried
267-unchanged from the open request through resolution, the wire, and the mux Open:
268-observation and daemon-stop are always `ExistingOnly` and never start a target,
269-while attach/creation and the explicit list/mutate controls may be
270-`StartIfNeeded`; the zero value is refused. The mode can only narrow the
271-resolved launch policy, never widen it.
272-
273-## Broker-owned local route (Plan 001 P5.3a, not activated)
274-
275-The broker reaches its own machine daemon, which is not a configured host, over
276-one broker-owned local binding. `internal/adapters/brokerconfig` parses an
277-optional top-level `local` object beside the registrations: the required
278-`identity`, `route`, and `policy`, plus an optional `displayOrigin` that is
279-sanitized and defaults to `local`. The route must be a Unix daemonmux carriage
280-this process dials directly, so an SSH or QUIC route is refused and the binding
281-can never name another machine. The identity, policy, and route are the only
282-source of local authority, and `Config.LocalBinding` alone publishes them. The
283-binding's own carriage is deliberately excluded from `Config.LocalMuxRoute`:
284-that route is the single registration carriage a remote-side mux helper bridges
285-to, and no helper ever bridges the broker's own local carriage.
286-
287-A local open-stream request carries no endpoint and no registration, so the
288-resolver fences it against the binding's identity and policy instead: a request
289-whose policy conflicts with the provisioned local policy is refused, and a
290-configuration with no local object provisions no local route. The resolved
291-(identity, policy) pair is the pool key, so every local logical stream shares
292-one physical daemonmux carriage to the local daemon, exactly as every stream to
293-one remote identity does; the route's opaque address selects that carriage and
294-never participates in the key.
295-
296-The registry publishes a local observation whenever a local producer is
297-configured, always at index zero ahead of every remote and never more than one.
298-Its configured authority (local flag, display origin, policy, rank) is stamped
299-from the binding and never from durable membership, since the local daemon has
300-no membership; its observed identity, incarnation, protocol version,
301-availability, and session catalogue come only from the local probe. The local
302-entry is never durable: the snapshot store persists and reloads remote
303-observations only. Observing the local daemon dials only an existing local Unix
304-socket, so it never starts a stopped daemon; it completes the authenticated
305-daemonmux physical preamble, opens exactly one `BrokerStreamObservation` logical
306-stream, sends one typed remote-catalog control request, validates the catalogue
307-the daemon answers, and closes the stream and carriage. It sends no `Hello`,
308-claims no geometry, requests no admission, creates no session, process, or
309-durable identity, and never attaches. The remote host probe and the local probe
310-share one semantic implementation, so the two cannot drift apart. Nothing in
311-`internal/app` or `main` activates this composition for ordinary operation; P7
312-performs the coordinated cutover.
313-
314-## Broker-owned daemon start (Plan 001 P7, UI-driver slice 1)
315-
316-The one place a resolved start authorization reaches transport mechanics is
317-`internal/app/broker_daemon_start.go`. `daemonmux.RawCarrierDialer` receives the
318-whole `ports.BrokerDialTarget` — address, fence, policy, `StartMode`, and
319-expected identity — instead of a bare opaque address, so an authorization can
320-never be flattened into an address or smuggled through a context value, and the
321-carriage owner sees exactly the authority the connector validated and is about
322-to verify in the physical preamble. `brokerMuxConnector` looks the route up by
323-the target's opaque address, re-checks that the provisioned entry still matches
324-the resolved fence and policy, and passes the whole target on.
325-
326-`dialBrokerLocalDaemon` dials one local daemon for one resolved target.
327-`BrokerDaemonExistingOnly` is exactly one private daemonmux carriage dial: no
328-lifecycle probe, no spawn lock, no process, and no fallback of any kind.
329-`BrokerDaemonStartIfNeeded` performs the same dial first and returns the daemon
330-it finds — an already running daemon is never restarted. Only a dial that proves
331-absence (an unclassified failure, a cancellation, a foreign path, a rejected
332-peer, and a permission refusal all report false) can reach the start, and then
333-only when the resolved policy names the broker launch authority and the carriage
334-is exactly the private endpoint the launched daemon will publish. A registered
335-endpoint is never started by the local mechanics. The start itself is the shared
336-election in `spawn.go` — lifecycle availability, one elected spawner under the
337-spawn lock, then a bounded redial of the same carriage — so observation and
338-daemon-stop stay `ExistingOnly` and a policy that forbids launching still
339-refuses visibly. `spawn.go` is generic over the dialed value, so the session
340-transport and the daemonmux carriage obey exactly one election discipline, and
341-no path here ever dials a session or a session control stream to prove
342-readiness.
343-
344-The remote-side mux helpers apply the same rule from their own broker-owned
345-configuration. `dialBrokerRoute` propagates the target's `StartMode` before any
346-connect-or-spawn, `sshMuxCommandSpec` appends the closed
347-`--daemon-start existing-only|if-needed` word to the provisioned remote argv,
348-and `parseBrokerMuxArgs` defaults an absent flag to the safe `existing-only`
349-while refusing an unknown value. A helper starts only the daemon behind its own
350-provisioned mux carriage, under its own policy, and never starts a broker, an
351-observer, or a recursive helper.
352-
353-## Broker local IPC (Plan 001 P3.3, not activated)
354-
355-`internal/adapters/brokeripc` carries the P3.1 broker conversation over one
356-per-user Unix endpoint. The carriage is the P3.2 private AF_UNIX carriage in
357-`internal/adapters/ipc`: an owner-only (0700, `pkg/safedir.EnsurePrivate`)
358-parent directory, a bound socket tightened to 0600, race-safe recovery of a
359-stale socket whose owner died, refusal (never removal) of a path that is not a
360-socket, and same-user kernel peer credentials (`SO_PEERCRED` on Linux) on both
361-accept and dial, failing closed on a platform or build without that check. The endpoint name (`broker.sock`) is the client-facing local endpoint. The daemon's Unix daemonmux socket is local-only composition infrastructure owned by the broker/daemon path; it is not published as a client API and ordinary clients never dial it.
362-
363-The listener owns its accept loop: one client slot is acquired before each
364-accept, each accepted carriage completes the broker preamble and broker
365-admission in its own goroutine, and admitted sessions are published through a
366-bounded FIFO to `Accept`. A client that stalls during the preamble therefore
367-occupies one slot and one goroutine, never the loop. Each session runs the
368-brokerwire connection state machine (one Register, one subscription generation
369-series, bounded operation and stream trackers), one snapshot publisher that
370-writes the P3.1 multipart layout and coalesces through the core's capacity-one
371-subscription, and one bridge per logical stream. Streams carry the typed
372-session protocol: `sessionwire` runs over a bounded private byte pipe on each
373-side, and two relay goroutines move typed messages between the client's session
374-connection and the admitted broker core connection, so no session byte is
375-reinterpreted.
376-
377-Every frame must carry the epoch and connection identity the listener assigned
378-at accept; a mismatched frame is fenced, and a stale stream open is answered
379-with a `StreamClosed` carrying the stale admission code. A malformed,
380-oversize, or wrong-direction envelope settles exactly the connection that sent
381-it. Setup is fully bounded on both halves: the client dialer bounds the Unix
382-dial, the preamble, the Register send, and the wait for Registered with one
383-context/deadline, closing the carriage to interrupt a peer that stalls after a
384-successful preamble and joining every setup worker, while a registered
385-connection detaches from that setup context; the server requires the client's
386-Register within the same accept-time handshake budget that bounded the preamble
387-and admission, so a silent same-user peer can neither pin an admitted core lease
388-nor hold a listener slot. Admission is the `ports.BrokerAuthority` seam,
389-implemented by `internal/usecase/broker.Authority`; the pre-Register guard
390-consults a registered marker set when Register handling begins, so a Register
391-arriving exactly at the deadline is admitted rather than settled as a timeout,
392-and a genuine registration timeout is classified as an orderly peer disconnect
393-so a listener draining its sessions never reports one as its own failure.
394-Operational bounds are explicit: concurrent clients
395-per listener, per-stream inbound chunks and bytes, pending mutating operations,
396-and the brokerwire per-connection stream and dedup bounds. Disconnect cleanup is
397-deterministic:
398-the session cancels its context, stops publishing, closes the outer carriage
399-(so a stream writer parked on a non-reading peer is interrupted), settles every
400-bridged stream (releasing its core connection and carriage), closes the
401-brokerwire state, joins every worker, closes the admitted core service, and
402-releases its client slot. Mutating operations report `outcome unknown` when a
403-reply is lost after the request was sent, and the client never replays them
404-blindly.
405-
406-P3.3 introduces no production activation: nothing in `internal/app` or `main`
407-constructs this adapter for ordinary operation. P3.4 composes the broker use
408-case, its stores, and this listener behind the hidden `_broker-serve` sandbox
409-entry point, with the listener consuming
410-`ports.BrokerAuthority` instead of defining its own seam. The composing caller
411-owns the `Registry.Run` lifecycle: it starts the single run to own and drain the
412-durable writer and cancels and joins that run before closing the store, because
413-`NewAuthority` neither starts nor settles the run. P7 performs the coordinated
414-cutover.
415-
416-## Broker offline sandbox (Plan 001 P3.4 slice C, not activated)
417-
418-The hidden `_broker-serve --offline-root ABS [--idle-grace DURATION]` entry
419-point is a foreground, isolated sandbox over one operator-supplied offline root.
420-It is excluded from public help and changes no ordinary command, path, or
421-factory. The root must be absolute, cleaned, symlink-free, and outside the
422-production runtime and state directories; the runtime, state, and log paths are
423-derived only beneath it and secured with `pkg/safedir.EnsurePrivate`, and the
424-durable store is opened with `brokerstore.Open` over the sandbox state
425-directory with no legacy inputs.
426-
427-`internal/adapters/brokerconfig` owns the strict sandbox configuration: a
428-bounded, owner-only `config.json` carrying the exact `vev.broker.offline/v1`
429-marker, with unknown fields, duplicate keys, trailing data, invalid UTF-8,
430-excessive nesting, and duplicate endpoints refused. Each immutable registration
431-provisions the exact daemon registration, the stable authenticated daemon
432-identity, the exact connection policy, and one absolute Unix mux route. The
433-resulting `ports.BrokerEndpointResolver` performs no I/O and invents no address:
434-a request must name a configured endpoint, carry its exact registration, and
435-request a compatible policy, so a request can never supply an address, identity,
436-secret, command, or policy of its own. Because the pool keys one physical
437-transport by the exact authenticated identity plus policy pair and deliberately
438-ignores the opaque route address, two registrations that share that pair must
439-agree on the one route the pool dials: an alias of an identical route is
440-accepted under distinct endpoints, while a second route for the same pair is
441-refused instead of being dialed for neither endpoint.
442-
443-The composition samples a cryptographically random non-zero broker epoch and
444-builds a store-backed, observation-disabled `broker.Registry` (whose single
445-`Run` the supervisor owns and joins), the immutable resolver, a
446-`daemonmux.EndpointConnector` over `ipc.DialMuxContext`, a bounded `Pool`, the
447-idle `Supervisor` (5m default grace), `broker.Authority`, and the
448-`brokeripc` listener. A temporary startup lease pins the supervisor open until
449-the listener and its accept drain are ready; the drain continuously takes
450-admitted sessions without closing them, because each returned service is the
451-listener's own connection-scoped session. A signal or parent-context
452-cancellation, or the idle grace, shuts down in the fixed order listener ->
453-pool -> registry -> store -> log -> owner lock. The foreground process holds a
454-reusable lifetime owner lock (`internal/adapters/lifecycle.TryAcquire`) on the
455-sandbox runtime directory for its whole run, so a second owner fails closed.
456-The optional `idleGrace` field in `config.json` provisions the sandbox's
457-effective idle grace as a Go duration string. It is the single deterministic
458-source for that grace: `_broker-serve` and `_broker-launcher` refuse an explicit
459-`--idle-grace` that conflicts with the provisioned value, because a status probe
460-cannot read a running broker's effective grace through the broker IPC protocol.
461-When no grace is provisioned, an explicit flag (or the supervisor default)
462-applies.
463-
464-## Broker offline connect-or-spawn and status (Plan 001 P3.4 slice D, not activated)
465-
466-The hidden `_broker-launcher --offline-root ABS [--idle-grace DURATION]` entry
467-point is the intermediate half of a double fork: it validates the sandbox
468-configuration (including the idle-grace conflict rule), starts one detached
469-`_broker-serve` in a new session, and exits, exactly like the daemon launcher.
470-The hidden `_broker-status --offline-root ABS [--ensure] [--timeout DURATION]`
471-entry point reports the offline broker's state. Both are excluded from public
472-help and change no ordinary command, path, or factory.
473-
474-Status without `--ensure` is dial-only: it performs one bounded probe of the
475-sandbox socket and prints exactly one bounded JSON document,
476-`{status, endpoint, epoch, revision, host_count}`, where `endpoint` is the
477-broker IPC socket path and the epoch, revision, and host count are zero when the
478-broker is offline. A missing socket and a stale socket left by a dead owner are
479-both recoverable absence; a foreign path, a live endpoint that fails the broker
480-handshake, and a stalled handshake are incompatible and never respawned over.
481-`--timeout` is the probe's single absolute bound in dial-only mode, so a caller
482-can tighten the probe or rely on the default (10s) and observe the same bound.
483-Dial-only status always exits zero, so the JSON is the sole result. `--ensure`
484-runs one absolute overall deadline (default 10s): it validates and secures the
485-sandbox root exactly as the launcher does before creating the spawn directory or
486-taking any lock. On success it prints the ready report and exits zero; on any
487-layout, configuration, or startup failure it still prints the offline report but
488-exits 3, so the caller always observes one offline JSON document and a distinct
489-exit code. The launcher is spawned under the ensure context
490-(`exec.CommandContext`), so the overall deadline or a signal bounds a launcher
491-that never exits; a launcher that already started the detached `_broker-serve`
492-is still safe to kill, because that serve is in its own session and survives.
493-
494-A foreground `_broker-serve` that hits an unexpected terminal accept failure
495-commits the same ordered shutdown as a cancellation, so a socket that can no
496-longer admit work is never left bound.
497-
498-Race safety rests on two descriptor-backed exclusive locks that are never
499-stolen by age and whose inodes are never unlinked. Both refuse any lock with
500-group or other access and trust an owner-only file whatever its exact owner bit
501-pattern, so a lock created under a restrictive umask (normalized to 0600 on
502-creation) or by a different tool never wedges startup; a group/other-readable
503-path stays refused. The broker lifetime lock
504-(`layout.Runtime/lifecycle.lock`) is held by the foreground `_broker-serve` for
505-its whole run; only the elected spawner probes it, and only as a liveness hint.
506-The spawn-election lock (`layout.Spawn/lifecycle.lock`, a separate private
507-directory beneath runtime) elects exactly one status caller to spawn. The
508-elected spawner re-dials after winning the lock, then holds the election until
509-the endpoint answers a valid broker registration and publishes its initial
510-snapshot, or until the deadline expires. A waiter never touches the lifetime
511-lock, so it can never briefly steal ownership from the broker it is waiting for,
512-and every wait is a bounded retry rather than a sleep loop. If the elected
513-spawner dies before readiness, the kernel releases its lock and a waiter takes
514-over; a duplicate `_broker-serve` fails its own lifetime acquisition
515-nonblocking. Every spawn uses the existing double-fork process pattern, and the
516-status probe and launcher inherit only the ambient environment minus the
517-performance-trace inputs.
518-
519-## Broker remote mux helpers (Plan 001 P3.4 slice E, not activated)
520-
521-The offline broker reaches a remote daemon's private daemonmux carriage over one
522-of three explicitly provisioned, bounded routes, and P3.4 slice E adds the
523-transport-selecting connector plus three hidden remote helpers. The routing
524-schema is strict and immutable: `internal/adapters/brokerconfig` parses a
525-registration's `route` as either the original Unix mux path string or an object
526-with an explicit kind, and refuses unknown kinds, unknown fields, trailing data,
527-and any kind/field mismatch. The bounded `ssh-stdio` and `ssh-quic` variants
528-carry an ssh target, an explicit remote argv, and optional trust inputs (a
529-known-hosts file and a connect timeout). A route can never carry an identity, a
530-policy, a command chosen at request time, or a secret. Identity and policy stay
531-authoritative on the registration, and the resolver publishes only an opaque
532-bounded route address: a digest of the route's own contents, so the pooling
533-layer never carries a target, a path, or an argv word.
534-
535-`_broker-serve` composes a transport-selecting `daemonmux.EndpointConnector`. Its
536-dial function receives the whole resolved `ports.BrokerDialTarget`, looks the
537-route up by its opaque address in the immutable configuration it was built
538-with, refuses an address the configuration did not produce, and re-checks that
539-the provisioned entry still matches the resolved fence and policy before
540-dialing. The target travels whole so the closed daemon-start authorization
541-reaches the transport owner instead of being flattened into an address. A
542-`unix` route dials the provisioned private Unix
543-mux carriage directly when it is a provisioned endpoint carriage, and goes
544-through the broker-owned daemon start (below) when it is the broker's own local
545-binding route; an `ssh-stdio` route starts one explicit ssh child (built
546-with `sshstdio.BuildCommandForMux` and carrying only trust inputs that narrow
547-verification, never disabling host-key checking, forcing an auth bypass, or
548-requesting a PTY) through `sshstdio.DialMuxContext`, and appends the closed
549-`--daemon-start existing-only|if-needed` word to the provisioned remote argv; an
550-`ssh-quic` route runs one
551-SSH-authenticated QUIC bootstrap, reads exactly one readiness line, and pins the
552-freshly minted ephemeral certificate through `quic.DialMuxContext`. One
553-caller-supplied setup context bounds bootstrap, pinned dial, and the daemonmux
554-physical handshake together; `brokerMuxSetupTimeout` is an independent backstop
555-for the broker-side bootstrap readiness wait and the child's kill/reap cleanup,
556-so an earlier caller deadline stays authoritative while a helper that never
557-prints readiness is still interrupted and reaped within the bound. The captured
558-bootstrap stderr sink is read only after that kill and reap, and the sink is
559-itself synchronized, so the os/exec copy goroutine never races the diagnostic. A
560-successful physical connection detaches from that context and stays pool-owned.
561-QUIC credentials are minted fresh per physical
562-connection and consumed exactly once, and captured bootstrap stderr is bounded
563-and sanitized, so a token, a nonce, or raw remote text never reaches an address,
564-a log, or an error.
565-
566-The three hidden helpers are excluded from public help. `_broker-mux-stdio
567---offline-root ABS [--daemon-start existing-only|if-needed]` validates the
568-sandbox and bridges its own stdio to the single
569-provisioned private Unix daemonmux carriage, applying exactly the start
570-authorization the broker propagated: an absent flag defaults to the safe
571-`existing-only`, an unknown value is refused, and a `start-if-needed` request is
572-gated by the helper's own provisioned identity and policy, which can only narrow
573-it. `_broker-mux-quic-bootstrap
574---offline-root ABS` validates the sandbox, starts one detached
575-`_broker-mux-quic-proxy` in a new session, forwards its single readiness line,
576-and exits. `_broker-mux-quic-proxy --offline-root ABS` mints one ephemeral QUIC
577-server, admits exactly one authenticated carriage, and bridges it to the same
578-private Unix daemonmux carriage under the same propagated authorization. A
579-helper only ever bridges to the configured
580-private endpoint: it never starts a broker, an observer, or an ordinary daemon
581-outside that one daemon-start authorization, never dials the production daemon
582-socket, and never fabricates a daemon
583-incarnation, which the daemon on the far side of the carriage alone owns.
584-Ordinary production composition still waits for P7; the helpers change no
585-ordinary command, path, or factory.
D docs/compact-storage-benchmarks.md +0 -106
@@ -1,106 +0,0 @@
1-# Compact storage: measured budgets
2-
3-Measured September 5, 2026 (CEST; September 4 UTC) on Linux amd64, Ryzen 5 7535HS, Go 1.27.1,
4-`GOMAXPROCS=2`. The existing manual-test binary was not replaced or restarted.
5-
6-## References and method
7-
8-- Consumer baseline: clean vev `9bf07496` (before this integration).
9-- Measured compact consumer: the integration captured in vev `b59725e3`, using vev-vt `f912b86` (before the final review fixes).
10-- Current integration dependency: vev-vt `f2960699a487`. The figures below were not remeasured at this revision; they describe the earlier `f912b86` dependency state.
11-- Retained-history baseline: vev-vt foundation `a0f7c5a`, before compact pages.
12-- Consumer workloads: 120 × 40 terminals, 10,000 history rows per pane.
13-- Main consumer comparisons: three 100 ms samples, reported as medians.
14-- Additional four/eight-tab cases: one 100 ms sample; directional evidence only.
15-- Retained heap: three one-iteration samples with explicit GC; not process RSS.
16-
17-These are local short-run measurements, not a statistical latency guarantee.
18-
19-## Consumer results
20-
21-| Operation | Dense ms/op | Compact ms/op | Dense bytes/op | Compact bytes/op | Dense allocs/op | Compact allocs/op |
22-| --- | ---: | ---: | ---: | ---: | ---: | ---: |
23-| Live paint, 1 tab / 1 pane | 0.197 | 0.951 | 356,090 | 86,507 | 36 | 40 |
24-| Live paint, 1 tab / 4 panes | 0.129 | 0.444 | 355,884 | 86,041 | 36 | 40 |
25-| Copy entry, 1 tab / 1 pane | 0.708 | 2.180 | 2,100,051 | 449,512 | 64 | 85 |
26-| Copy entry, 1 tab / 4 panes | 0.629 | 1.389 | 1,542,092 | 296,382 | 82 | 114 |
27-| Copy entry, 4 tabs / 1 pane* | 1.165 | 2.144 | 2,096,593 | 433,169 | 68 | 88 |
28-| Copy entry, 4 tabs / 4 panes* | 0.974 | 1.383 | 1,541,684 | 296,647 | 86 | 118 |
29-| Copy entry, 8 tabs / 1 pane* | 1.113 | 2.169 | 2,098,087 | 434,861 | 76 | 96 |
30-
31-`*` Single-sample comparison.
32-
33-Main workloads emitted the same output bytes and one output frame per operation:
34-57/59 bytes for live paint and 524/971 bytes for copy entry.
35-
36-**The trade-off is real:** allocated bytes fall approximately 4–5×, but the
37-main workloads take approximately 2.2–4.8× longer. Do not describe this change as
38-a general throughput improvement. The integration's cell-by-cell grid copies
39-and page-local style bookkeeping remain optimization candidates. No unsafe
40-copying, pooling, or new allocator was introduced to make allocation counts pass.
41-
42-## Retained history
43-
44-Plain ASCII, 10,000 × 120 cells:
45-
46-| Representation | Median retained bytes |
47-| --- | ---: |
48-| Dense foundation | 95,283,872 |
49-| Compact, uncompressed | 22,769,712 |
50-| Compact after two idle compression passes | 4,209,776 |
51-
52-Uncompressed storage meets issue #9's **4× retained-memory reduction** target
53-(approximately 4.18×). Cold storage retains approximately 22.6× less than dense
54-storage, with 38 cold pages and 173,190 compressed bytes.
55-
56-Building this synthetic history allocates **148.5 MB cumulatively**, versus
57-95.8 MB for the dense reference. Lower retained memory does not imply lower
58-allocation traffic during construction. The mutable semantic tail contributes
59-to this cost. Retained-memory sample times include measurement GCs and must not
60-be treated as isolated append throughput.
61-
62-## Regression gates
63-
64-The previous 38/66–90 allocation-count thresholds came from vev's dense-frame
65-benchmarks, not from the vev-vt issues. The floating composition threshold of
66-two allocations explicitly assumed two dense slices.
67-
68-The recalibrated tests retain count checks and add independent byte checks:
69-
70-- Live paint: at most **44 allocations** and **96 KiB/op**.
71-- Copy entry: approximately 10% count headroom over measured compact values;
72- at most **512 KiB/op** for single-pane tabs and **384 KiB/op** for four-pane tabs.
73-- Cached floating composition: the measured cost of one compact clone plus one
74- allocation for initial style growth, and at most **40 KiB/op**.
75-- Warm title/bar composition still requires **zero allocations**.
76-- Retained-history and ownership/safety checks remain separate and unchanged.
77-
78-Byte checks use Go benchmark allocation accounting, excluding fixture setup.
79-Instrumented race runs exercise behavior but do not enforce byte budgets.
80-Wall-clock timings are reported rather than asserted as machine-dependent CI
81-thresholds. The CPU regression remains visible above; these gates do not claim
82-that it has been removed.
83-
84-## Reproduce
85-
86-Run the same consumer command in clean vev `9bf07496` and the integration tree.
87-The current integration measures the final dependency, not the exact historical
88-sample above. For the historical comparison, use a separate checkout of vev
89-`b59725e3`, which pins the compatible `f912b86` dependency and pre-review consumer
90-API. Do not downgrade the dependency alone in the current integration.
91-
92-```sh
93-GOMAXPROCS=2 go test ./internal/usecase/daemon -run '^$' \
94- -bench '^BenchmarkDaemonHistory(LivePaint|CopyEnter)$/^1tab-(1pane-control|4panes)$' \
95- -benchmem -benchtime=100ms -count=3
96-```
97-
98-Run in the dense foundation and compact vev-vt trees:
99-
100-```sh
101-GOMAXPROCS=2 go test -run '^$' \
102- -bench '^BenchmarkHistoryRetained10Kx120$/plain-ascii$' \
103- -benchtime=1x -count=3 -benchmem
104-```
105-
106-The compact tree also provides `BenchmarkColdHistoryRetained10Kx120`.
@@ -1,106 +0,0 @@
1-# Compact storage: measured budgets
2-
3-Measured September 5, 2026 (CEST; September 4 UTC) on Linux amd64, Ryzen 5 7535HS, Go 1.27.1,
4-`GOMAXPROCS=2`. The existing manual-test binary was not replaced or restarted.
5-
6-## References and method
7-
8-- Consumer baseline: clean vev `9bf07496` (before this integration).
9-- Measured compact consumer: the integration captured in vev `b59725e3`, using vev-vt `f912b86` (before the final review fixes).
10-- Current integration dependency: vev-vt `f2960699a487`. The figures below were not remeasured at this revision; they describe the earlier `f912b86` dependency state.
11-- Retained-history baseline: vev-vt foundation `a0f7c5a`, before compact pages.
12-- Consumer workloads: 120 × 40 terminals, 10,000 history rows per pane.
13-- Main consumer comparisons: three 100 ms samples, reported as medians.
14-- Additional four/eight-tab cases: one 100 ms sample; directional evidence only.
15-- Retained heap: three one-iteration samples with explicit GC; not process RSS.
16-
17-These are local short-run measurements, not a statistical latency guarantee.
18-
19-## Consumer results
20-
21-| Operation | Dense ms/op | Compact ms/op | Dense bytes/op | Compact bytes/op | Dense allocs/op | Compact allocs/op |
22-| --- | ---: | ---: | ---: | ---: | ---: | ---: |
23-| Live paint, 1 tab / 1 pane | 0.197 | 0.951 | 356,090 | 86,507 | 36 | 40 |
24-| Live paint, 1 tab / 4 panes | 0.129 | 0.444 | 355,884 | 86,041 | 36 | 40 |
25-| Copy entry, 1 tab / 1 pane | 0.708 | 2.180 | 2,100,051 | 449,512 | 64 | 85 |
26-| Copy entry, 1 tab / 4 panes | 0.629 | 1.389 | 1,542,092 | 296,382 | 82 | 114 |
27-| Copy entry, 4 tabs / 1 pane* | 1.165 | 2.144 | 2,096,593 | 433,169 | 68 | 88 |
28-| Copy entry, 4 tabs / 4 panes* | 0.974 | 1.383 | 1,541,684 | 296,647 | 86 | 118 |
29-| Copy entry, 8 tabs / 1 pane* | 1.113 | 2.169 | 2,098,087 | 434,861 | 76 | 96 |
30-
31-`*` Single-sample comparison.
32-
33-Main workloads emitted the same output bytes and one output frame per operation:
34-57/59 bytes for live paint and 524/971 bytes for copy entry.
35-
36-**The trade-off is real:** allocated bytes fall approximately 4–5×, but the
37-main workloads take approximately 2.2–4.8× longer. Do not describe this change as
38-a general throughput improvement. The integration's cell-by-cell grid copies
39-and page-local style bookkeeping remain optimization candidates. No unsafe
40-copying, pooling, or new allocator was introduced to make allocation counts pass.
41-
42-## Retained history
43-
44-Plain ASCII, 10,000 × 120 cells:
45-
46-| Representation | Median retained bytes |
47-| --- | ---: |
48-| Dense foundation | 95,283,872 |
49-| Compact, uncompressed | 22,769,712 |
50-| Compact after two idle compression passes | 4,209,776 |
51-
52-Uncompressed storage meets issue #9's **4× retained-memory reduction** target
53-(approximately 4.18×). Cold storage retains approximately 22.6× less than dense
54-storage, with 38 cold pages and 173,190 compressed bytes.
55-
56-Building this synthetic history allocates **148.5 MB cumulatively**, versus
57-95.8 MB for the dense reference. Lower retained memory does not imply lower
58-allocation traffic during construction. The mutable semantic tail contributes
59-to this cost. Retained-memory sample times include measurement GCs and must not
60-be treated as isolated append throughput.
61-
62-## Regression gates
63-
64-The previous 38/66–90 allocation-count thresholds came from vev's dense-frame
65-benchmarks, not from the vev-vt issues. The floating composition threshold of
66-two allocations explicitly assumed two dense slices.
67-
68-The recalibrated tests retain count checks and add independent byte checks:
69-
70-- Live paint: at most **44 allocations** and **96 KiB/op**.
71-- Copy entry: approximately 10% count headroom over measured compact values;
72- at most **512 KiB/op** for single-pane tabs and **384 KiB/op** for four-pane tabs.
73-- Cached floating composition: the measured cost of one compact clone plus one
74- allocation for initial style growth, and at most **40 KiB/op**.
75-- Warm title/bar composition still requires **zero allocations**.
76-- Retained-history and ownership/safety checks remain separate and unchanged.
77-
78-Byte checks use Go benchmark allocation accounting, excluding fixture setup.
79-Instrumented race runs exercise behavior but do not enforce byte budgets.
80-Wall-clock timings are reported rather than asserted as machine-dependent CI
81-thresholds. The CPU regression remains visible above; these gates do not claim
82-that it has been removed.
83-
84-## Reproduce
85-
86-Run the same consumer command in clean vev `9bf07496` and the integration tree.
87-The current integration measures the final dependency, not the exact historical
88-sample above. For the historical comparison, use a separate checkout of vev
89-`b59725e3`, which pins the compatible `f912b86` dependency and pre-review consumer
90-API. Do not downgrade the dependency alone in the current integration.
91-
92-```sh
93-GOMAXPROCS=2 go test ./internal/usecase/daemon -run '^$' \
94- -bench '^BenchmarkDaemonHistory(LivePaint|CopyEnter)$/^1tab-(1pane-control|4panes)$' \
95- -benchmem -benchtime=100ms -count=3
96-```
97-
98-Run in the dense foundation and compact vev-vt trees:
99-
100-```sh
101-GOMAXPROCS=2 go test -run '^$' \
102- -bench '^BenchmarkHistoryRetained10Kx120$/plain-ascii$' \
103- -benchtime=1x -count=3 -benchmem
104-```
105-
106-The compact tree also provides `BenchmarkColdHistoryRetained10Kx120`.
M docs/configuration.md +104 -83
@@ -1,6 +1,13 @@
11 # Configuration
22
3-vev reads `~/.config/vev/config` (`$XDG_CONFIG_HOME` respected). No file means defaults. The session daemon picks up changes within a couple of seconds. Browser gateway settings (`web.listen` and `web.origin`) require a gateway restart; flags override their config values. See [browser terminal](web-terminal.md#listener-and-public-origin) for private-network and HTTPS proxy setup.
3+vev reads `~/.config/vev/config` (`$XDG_CONFIG_HOME` is respected).
4+
5+- No file means defaults.
6+- Changes apply live within a couple of seconds.
7+- Exception: `web.listen` and `web.origin` need a gateway restart. See [browser terminal](web-terminal.md#listener-and-public-origin).
8+- Invalid values log a warning and fall back to the default.
9+
10+The full list with defaults:
411
512 ```text
613 # Browser gateway: HTTP listener and browser-facing origin, read at startup.
@@ -41,6 +48,8 @@ focus-pane-down = alt+j
4148 # Optional pane rearrangement actions are unbound by default.
4249 # consume-or-expel-pane-left = alt+H
4350 # consume-or-expel-pane-right = alt+L
51+# grow-pane-width, shrink-pane-width, grow-pane-height, shrink-pane-height,
52+# and equalize-panes are also unbound by default.
4453 switch-tab-1 = alt+1
4554 # ... through switch-tab-9 = alt+9
4655
@@ -60,6 +69,7 @@ bar.interval = 5s
6069 # Command palette codes: 2-3 letters or digits.
6170 code.new-tab = CNT
6271 code.new-session = CNS
72+code.create-ephemeral-session = CES
6373 code.close-tab = CLT
6474 code.split-right = SPR
6575 code.split-left = SPL
@@ -74,6 +84,14 @@ code.focus-pane-left = FPL
7484 code.focus-pane-right = FPR
7585 code.focus-pane-up = FPU
7686 code.focus-pane-down = FPD
87+code.resize-pane = RSZ
88+code.grow-pane-width = GPW
89+code.shrink-pane-width = SPW
90+code.grow-pane-height = GPH
91+code.shrink-pane-height = SPH
92+code.equalize-panes = EQP
93+code.move-pane = MFP
94+code.move-tab = MAT
7795 code.next-tab = NXT
7896 code.previous-tab = PVT
7997 code.back-session = BCK
@@ -84,39 +102,25 @@ code.toggle-floating-pane = TFP
84102 code.rename-session = RNS
85103 code.rename-tab = RNT
86104 code.detach = DET
105+code.notifications = NTC
106+code.yank-last-notification = YLN
87107 ```
88108
89-Invalid values log a warning and resolve that setting to its default on both initial load and reload.
90-
91109 ## Scrollback
92110
93-`vev` chooses the retention policy; `vev-vt` enforces it. The PTY transports
94-bytes and does not own scrollback.
95-
96-- `scrollback.megabytes`: 0–4096 decimal MB, default 50. Zero disables history.
97-- `scrollback.lines`: 0–1,000,000 lines, default 10,000. Zero removes the
98- additional line ceiling when the byte budget is positive.
99-- Limits apply independently to each pane, including hidden floating panes,
100- and exclude visible primary/alternate grids. They measure uncompressed
101- logical history bytes, not total process RSS.
102-- Reload applies to existing panes immediately. Lowering a limit evicts the
103- oldest rows; disabling history discards its retained contents. Existing copy
104- documents remain stable. New and restored panes use the current limits.
105-- Idle maintenance visits at most one sealed page per pane every five seconds,
106- skips busy panes, and compresses only pages that remain cold. Compression does
107- not grant additional retention or count compressed bytes toward the budget.
108-
109-Persistence has separate per-blob and aggregate safety ceilings; raising the
110-runtime retention budget does not remove them. The compact codec replaces the
111-old VT encoding directly: old VT history blobs are not readable by this alpha
112-build. Test with `VEV_ENV=dev`, not your normal daemon's persisted state.
113-
114-See [compact storage measurements](compact-storage-benchmarks.md) for memory,
115-allocation and CPU trade-offs.
111+| Key | Range | Default | Zero means |
112+|---|---|---|---|
113+| `scrollback.megabytes` | 0–4096 MB | 50 | no history |
114+| `scrollback.lines` | 0–1,000,000 | 10,000 | no line limit, bytes only |
115+
116+- Limits are per pane, including hidden floating panes.
117+- They count uncompressed history, not process memory.
118+- A reload applies to existing panes right away. Lowering a limit drops the oldest rows.
119+- Idle panes compress old history in the background. This saves memory but does not raise the limit.
116120
117121 ## Development environments
118122
119-Set `VEV_ENV` to a name such as `dev` to redirect all vev-owned paths into `.dev/<name>/` under the directory where vev was invoked. The name must be one safe segment of at most 64 ASCII letters, digits, dots, underscores, or hyphens, and must start with a letter or digit.
123+Set `VEV_ENV=<name>` to move every vev file into `.dev/<name>/` under the current directory. Use it to test a build without touching your real sessions.
120124
121125 ```sh
122126 VEV_ENV=dev go run .
@@ -124,25 +128,29 @@ VEV_ENV=dev go run . kill --sessions
124128 rm -rf .dev/dev
125129 ```
126130
127-The selected absolute root is retained by the detached daemon and inherited vev processes even when their working directory changes. A name isolates configuration, runtime files, durable sessions, hosts, snapshots, and logs from normal XDG locations and from other names. Invocations using the same name intentionally share a daemon and files; use separate names for side-by-side binaries.
128-
129-If the runtime path would exceed the portable Unix-socket limit (103 bytes, including `vev/broker/broker.sock`), sockets and lifecycle locks instead use `/tmp/vev-<uid>-<sha256>`; the hash identifies the complete original runtime directory. Configuration and durable state remain under `.dev/<name>/`. All roles resolve the same private runtime directory, and distinct roots remain isolated. Short paths keep their existing location. Stop processes using an older binary before switching versions if their runtime location changes; do not delete a live runtime directory or its lock files.
130-
131-This feature isolates vev-owned data only. It does not sandbox pane processes or provide OS, filesystem, or network isolation.
131+- Name: up to 64 letters, digits, `.`, `_`, or `-`, starting with a letter or digit.
132+- The same name shares one daemon. Use different names to run builds side by side.
133+- Config, sessions, hosts, snapshots, and logs are isolated.
134+- If the socket path gets too long, sockets move to `/tmp/vev-<uid>-<hash>`. Everything else stays in `.dev/<name>/`.
135+- It is not a sandbox: pane processes see your real system.
132136
133137 ## Remote hosts
134138
135-Remote host commands, listing, and successful direct-attach learning are always active.
136-
137-`$XDG_STATE_HOME/vev/hosts.json` (or `~/.local/state/vev/hosts.json` when unset) is the only host-list location. It is versioned JSON with pinned and learned targets. `vev host add` adds a pinned target, `vev host rm` atomically removes a target from both sets, and `vev host list` shows `pinned`, `learned`, or `pinned,learned` in its `SOURCE` column. Pinned hosts keep stored order; learned-only hosts follow in lexical order. vev rejects empty targets, surrounding whitespace, and internal whitespace or control characters; SSH alias grammar is left to OpenSSH.
139+```sh
140+vev host add user@host # add a host
141+vev host rm user@host # remove it
142+vev host list # show hosts and their status
143+```
138144
139-`vev ls <host>` and `vev ls --all` run `ssh -- <host> 'vev cmd remote-catalog --json'` for each known host. OpenSSH resolves aliases and connection settings from your SSH config. Remote session names appear as `session@host`. `vev ls --all` prints local sessions first, then remote sessions in merged host order. A catalog failure is reported after the successful output with the host and error; the command exits non-zero so partial output is not mistaken for a complete inventory.
145+- The broker stores hosts in `~/.local/state/vev/broker/state/state.json`.
146+- Hosts connect over SSH, so your SSH config (aliases, keys) applies.
147+- Remote sessions appear as `session@host` in `vev ls --all` and in the picker.
140148
141149 ### Warm remote transports
142150
143-The connection broker keeps a remote's physical transport warm after the last terminal client leaves it. Returning to that remote opens a new logical stream over the retained, already authenticated SSH or QUIC connection: no second SSH bootstrap, QUIC handshake, or remote helper start. Broker observation probes borrow the same pooled transport instead of dialing their own. The remote daemon sees a fresh attach; session state lives on the daemon, so the screen and scrollback are unchanged.
151+After you leave a remote, vev keeps its connection open for a while. Going back is then instant: no new SSH login or QUIC handshake.
144152
145-Retention is set in the broker configuration, `~/.config/vev/broker.json` (`$XDG_CONFIG_HOME` respected), which the broker creates on first use. It is read when the broker starts.
153+Configure this in `~/.config/vev/broker.json`. The file is created on first use and read when the broker starts.
146154
147155 ```json
148156 {
@@ -153,61 +161,76 @@ Retention is set in the broker configuration, `~/.config/vev/broker.json` (`$XDG
153161 }
154162 ```
155163
156-- `warmTransports`: 0–64 idle transports kept warm, default 8. When one more goes idle, the least recently idled one closes. `0` closes a transport as soon as its last stream ends. Transports with an attached client never count.
157-- `warmIdleTimeout`: `off` (default, no age bound) or a positive Go duration up to `24h`, such as `30s` or `5m`. A warm transport closes that long after it went idle.
164+- `warmTransports`: how many idle connections to keep (0–64, default 8). The oldest one closes first. `0` disables it.
165+- `warmIdleTimeout`: close an idle connection after this long, such as `5m` (max `24h`). Default `off`: no time limit.
158166
159-A remote closing the connection also evicts it; the next visit dials normally. Warm transports never keep the broker alive: when the broker stops after its idle grace, it closes them. Aliases stay distinct unless the broker authenticated them as the same daemon under the same connection policy. These keys replace the client-side `remote.attachment-cache`, `remote.attachment-cache-capacity`, and `remote.attachment-cache-idle-timeout` settings, which no longer exist.
167+Warm connections never keep the broker running.
160168
161169 ## Logs and durable state
162170
163-Set `VEV_LOG=debug`, `VEV_LOG=warn`, or `VEV_LOG=error` to change verbosity; the default is `info`. JSON-line logs such as `vev-daemon.log` live in `$XDG_STATE_HOME/vev`, or `~/.local/state/vev` when unset. The same state directory contains the strict session catalogue, any private pre-migration catalogue backup, notices, `hosts.json`, and `snapshots/`. The lifecycle lock and socket live in `$XDG_RUNTIME_DIR/vev` (with platform runtime fallbacks).
164-
165-Recovery events include `lifecycle_owner_wait`, `lifecycle_owner_acquired`, `lifecycle_owner_released`, `catalogue_validated`, `catalogue_compaction_recovery_complete`, `session_restore_complete`, `fallback_checkpoint_promoted`, `snapshot_head_repair_complete`, `session_degraded`, `snapshot_garbage_collection_complete`, `interrupted_transaction_recovery_complete`, and `daemon_startup_complete`.
171+| What | Where |
172+|---|---|
173+| Logs (JSON lines, e.g. `vev-daemon.log`) | `~/.local/state/vev/` |
174+| Sessions and snapshots | `~/.local/state/vev/` |
175+| Broker hosts and logs | `~/.local/state/vev/broker/` |
176+| Socket and lock | `$XDG_RUNTIME_DIR/vev/` |
166177
167-Snapshot garbage collection is startup-only: the daemon runs one coordinator-owned pass before it publishes its socket, and no periodic snapshot collection runs afterward. The five-second idle maintenance loop compresses sealed scrollback pages and never collects snapshots or incarnations (GO-002, deferred).
178+`$XDG_STATE_HOME` is respected. Set `VEV_LOG=debug|warn|error` to change the log level (default `info`).
168179
169-Catalogue failure is fail-closed: vev does not publish an empty replacement daemon. Preserve the state directory, inspect `catalogue_validation_failed`, correct storage or ownership problems, and retry without editing catalogue files. See [Durable session recovery](durable-session-recovery.md) for explicit recovery commands, migration, diagnostics, and the committed checkpoint plus up to two direct fallbacks retention policy.
180+If the daemon refuses to start because its session data is broken, do not edit the files. See [durable session recovery](durable-session-recovery.md).
170181
171182 ## Theme
172183
173-With `theme = auto`, vev follows the terminal's reported foreground, background, light/dark scheme, and ANSI palette. `theme.palette = on` is the default. `theme.accent = auto` derives one accent from the terminal's chromatic ANSI colors; larger and more cohesive repeated-color groups are preferred, then conventional normal/bright pairs, otherwise vev uses an eligible blue slot when available. `theme.accent = 0` through `theme.accent = 15` selects exactly that ANSI slot. Arbitrary RGB values and `off` are not accepted for `theme.accent`.
174-
175-When the terminal provides truecolor defaults and a usable RGB palette slot, vev uses the resolved accent for active chrome and derives softer bar, inactive, recent-session, and border colors from it. It preserves readability by reducing a surface intensity when needed. Terminal application/pane colors are not recolored. Without truecolor default colors or a usable RGB resolved accent, chrome backgrounds remain neutral; an available selected ANSI slot can still decorate foregrounds or borders. An explicit unavailable slot is never replaced with another slot.
176-
177-Slots `0`, `7`, `8`, and `15` are valid explicit selections, but log a warning because conventional neutral slots may provide little or no accent separation. An invalid accent value logs a warning and falls back to `auto`, including on hot reload.
184+| Setting | Effect |
185+|---|---|
186+| `theme = auto` | Follow your terminal's colors and light/dark mode. |
187+| `theme = dark` / `light` | Neutral built-in colors. Ignores the two settings below. |
188+| `theme.palette = off` | Neutral colors, no accent. |
189+| `theme.accent = auto` | Pick an accent from your terminal's ANSI colors (blue if unsure). |
190+| `theme.accent = 0`–`15` | Use exactly that ANSI color as the accent. |
178191
179-`theme.palette = off` is authoritative: it keeps exact neutral foreground/background rendering and ignores `theme.accent`. Forced `theme = dark` or `theme = light` is also neutral and ignores both palette and accent policy. Configuration reload immediately reapplies the current terminal snapshot; vev updates a live palette when the terminal reports a light/dark scheme change, but cannot detect palette changes a terminal does not report.
192+- vev only colors its own UI (bars, borders, palette). Pane content is never recolored.
193+- Tinted backgrounds need a truecolor terminal. Otherwise the accent only colors text and borders.
194+- vev follows light/dark switches your terminal reports. It cannot see palette changes the terminal does not report.
180195
181196 ## Bindings
182197
183-Key specs: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` through `alt+9`. Configuring an action replaces all of its built-in aliases (set `focus-pane-left` and the Alt+Arrow alias is gone). Tab switching also accepts the top-row symbols of non-QWERTY layouts, so AZERTY works without extra config.
198+Valid keys: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` to `alt+9`.
184199
185-`alt+[` is unsupported because terminals frame it as the CSI prefix `ESC [`, which vev passes through as terminal input. Cmd/Super is not a vev key-spec modifier; map a physical Cmd/Super chord in the terminal emulator to an unused, safe `ESC` + character sequence (not `ESC [`), then configure the matching `alt+<char>` in vev.
200+- Setting an action replaces all its default keys. For example, setting `focus-pane-left` removes Alt+Left too.
201+- Tab switching works with AZERTY and other layouts without extra config.
202+- `alt+[` is not supported: terminals use it for escape sequences.
203+- Cmd/Super is not supported. Map it in your terminal to `ESC` + a character, then bind `alt+<char>` in vev.
186204
187205 ## Pane consume or expel
188206
189-The `consume-or-expel-pane-left` and `consume-or-expel-pane-right` actions are unbound by default; the commented Alt+H/Alt+L bindings above are optional examples. Use palette codes `MPL` and `MPR`, or script them as `vev cmd consume-or-expel-pane-left` and `vev cmd consume-or-expel-pane-right`.
207+Moves the focused pane between columns, like the Niri window manager. It has no default key: use palette codes `MPL` / `MPR`, or bind `consume-or-expel-pane-left/right`.
190208
191-A singleton pane moves into the immediate column on the requested side; at the outer edge, nothing changes. A pane in a multi-member vertical or stack column moves out as an adjacent singleton column. This works only with canonical column layouts: one column, or a top-level horizontal split of columns, where each column is a singleton pane, a vertical split of panes, or a pane stack. Nested mixed splits are unsupported.
209+- A pane alone in its column joins the column next to it.
210+- A pane sharing a column leaves it and becomes its own column.
211+- Works only with column layouts. Nested mixed splits are not supported.
192212
193213 ## Navigation overflow
194214
195-Both settings are independent and default to `off`. With `nav.overflow-tabs = on`, left/right keyboard focus (`Alt+H`/`Alt+L` by default) continues from a pane edge to the adjacent tab. With `nav.overflow-sessions = on`, up/down keyboard focus (`Alt+K`/`Alt+J` by default) continues to the adjacent live session in alphabetical order. Neither setting wraps at the first or last destination.
215+Both are `off` by default.
196216
197-Overflow applies only to keyboard focus actions; mouse navigation does not overflow, and floating panes never overflow. Hot reload applies either setting to subsequent navigation without restarting the daemon.
217+- `nav.overflow-tabs = on`: Alt+h/l at the edge of the tab moves to the next tab.
218+- `nav.overflow-sessions = on`: Alt+k/j at the edge moves to the next session (alphabetical).
219+
220+No wrap-around. Only keyboard focus overflows, never the mouse or floating panes.
198221
199222 ## Copy mode
200223
201-Scroll up with the mouse to enter copy mode. Use `h`, `j`, `k`, and `l` to move, `w`, `b`, and `e` for word motions, `v` or Space to start line selection, and `y` or Enter to copy.
224+Scroll up with the mouse to enter. Scroll back to the bottom to leave.
202225
203-The mouse wheel moves the viewport immediately in either direction, independently
204-of keyboard cursor navigation. Scrolling has a short, decelerating animation:
205-the first row responds immediately and the remaining movement is paced at 16 ms
206-intervals. Reversing direction cancels the old tail. Keyboard input and mouse
207-selection stop the animation; selection scrolling remains immediate. Returning
208-to the bottom exits copy mode and shows live output.
226+| Key | Action |
227+|---|---|
228+| `h` `j` `k` `l` | move |
229+| `w` `b` `e` | word motions |
230+| `v` or Space | start selection |
231+| `y` or Enter | copy |
209232
210-Mouse drag selects a text range. Double-click selects the word under the pointer; dragging after a double-click extends by complete words.
233+With the mouse: drag to select, double-click to select a word.
211234
212235 ```ini
213236 # Unicode whitespace always separates words.
@@ -217,34 +240,32 @@ copy.word-separators = " -_@"
217240 copy.reduce-motion = off
218241 ```
219242
220-Set `copy.word-separators = ""` to use only Unicode whitespace as a separator.
221-Set `copy.reduce-motion = on` for immediate three-row wheel steps. The setting
222-applies to subsequent wheel input after configuration reload. Applications that
223-capture the mouse, and alternate-screen wheel-to-arrow forwarding, are unchanged.
224-
225-Motion is row-based: vev cannot animate fractional pixels inside the outer
226-terminal. The easing preserves requested scroll distance rather than adding
227-extra momentum, and drains any remaining tail on the first animation tick at
228-least 120 ms after the last wheel input.
243+- `copy.word-separators = ""` uses only whitespace.
244+- `copy.reduce-motion = on` scrolls three rows per wheel step, without animation.
229245
230246 ## Responsive overlays
231247
232-On complete frames below 80 columns, interactive overlays use full-width bottom drawers immediately above the bottom bar. This applies to the floating terminal, command palette, session picker, notification history, prompts, and copy search. A drawer keeps its overlay's preferred outer height, capped at the frame height minus four rows, so frame rows 0–2 remain visible. Drawers use only a top border.
233-
234-Opening an interactive overlay dims the complete underlying frame, including panes, both bars, notices, and any lower-priority overlay. Notice toasts remain compact rather than becoming drawers. Copy mode remains full-screen; only its search prompt uses the responsive drawer.
248+Below 80 columns, popups (palette, picker, floating terminal, prompts) become full-width drawers at the bottom of the screen. The background dims while a popup is open.
235249
236250 ## Palette anchor
237251
238-The command palette is centered by default. With `palette.anchor = auto`, it uses a full-width bottom shelf from 80 through 95 columns and a 64-column bottom-right rail from 96 columns up. Set an explicit anchor to position the shelf or rail. Below 80 columns, the palette is always a bottom drawer, so an explicit anchor does not override the responsive layout. Reload moves an open palette without losing your query.
252+- Default: centered.
253+- `palette.anchor = auto`: bottom bar from 80 to 95 columns, right-side rail from 96 columns.
254+- Any other value places it at that spot. Below 80 columns it is always a bottom drawer.
239255
240256 ## Floating terminal
241257
242-The command runs through your shell. A changed command applies on the next launch; changed dimensions on the next show or resize. Below 80 columns, `floating.height` continues to determine the drawer's outer height, capped to preserve the first three frame rows and the bottom bar; `floating.width` is replaced by the full frame width. Floating state is not restored across daemon restarts.
258+- `floating.command` runs through your shell. Changes apply on the next launch.
259+- Below 80 columns, the terminal takes the full width and keeps `floating.height`.
260+- Floating terminals are not restored after a daemon restart.
243261
244262 ## Bar anchors
245263
246-Anchor commands run on the daemon host every `bar.interval` (minimum 1s). vev reads the first line of stdout, strips ANSI codes, and keeps the last good value on failure. Scripts get `VEV_ANCHOR`, `VEV_SESSION`, `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, and `VEV_COLS`.
264+`bar.top-right` and `bar.bottom-right` show the output of a command in the bar. Both are off by default.
247265
248-Commands resolve against the environment of the client currently attached to the session, the same environment new panes inherit. A command that works in your shell will work as an anchor without restarting the daemon. When an anchor fails, the daemon logs the exit code and the command's stderr; exit 127 means the command was not found on that `PATH`.
266+- The command runs on the daemon host every `bar.interval` (minimum `1s`), with the same environment as your panes.
267+- vev shows the first line of output, without colors. On failure it keeps the last good value.
268+- Scripts receive `VEV_ANCHOR`, `VEV_SESSION`, `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, and `VEV_COLS`.
269+- Failures are logged with the exit code and stderr. Exit 127 means "command not found".
249270
250-Both anchors are disabled by default, so `go install github.com/bnema/vev@latest` needs no companion scripts. Set either command to opt in; explicit commands are run unchanged. Checkout and release installs include `vev-bar-top-right` and `vev-bar-bottom-right` example scripts, which can be configured by name when they are on the attaching client's `PATH`. The bottom-right example runs `git status --porcelain` on each refresh; raise the interval or replace it if that is too heavy for your repository.
271+Release installs ship two example scripts: `vev-bar-top-right` and `vev-bar-bottom-right`. The second one runs `git status` on every refresh; raise the interval on large repositories.
@@ -1,6 +1,13 @@
1 # Configuration1 # Configuration
2 2
3-vev reads `~/.config/vev/config` (`$XDG_CONFIG_HOME` respected). No file means defaults. The session daemon picks up changes within a couple of seconds. Browser gateway settings (`web.listen` and `web.origin`) require a gateway restart; flags override their config values. See [browser terminal](web-terminal.md#listener-and-public-origin) for private-network and HTTPS proxy setup.3+vev reads `~/.config/vev/config` (`$XDG_CONFIG_HOME` is respected).
4+
5+- No file means defaults.
6+- Changes apply live within a couple of seconds.
7+- Exception: `web.listen` and `web.origin` need a gateway restart. See [browser terminal](web-terminal.md#listener-and-public-origin).
8+- Invalid values log a warning and fall back to the default.
9+
10+The full list with defaults:
4 11
5 ```text12 ```text
6 # Browser gateway: HTTP listener and browser-facing origin, read at startup.13 # Browser gateway: HTTP listener and browser-facing origin, read at startup.
@@ -41,6 +48,8 @@ focus-pane-down = alt+j
41 # Optional pane rearrangement actions are unbound by default.48 # Optional pane rearrangement actions are unbound by default.
42 # consume-or-expel-pane-left = alt+H49 # consume-or-expel-pane-left = alt+H
43 # consume-or-expel-pane-right = alt+L50 # consume-or-expel-pane-right = alt+L
51+# grow-pane-width, shrink-pane-width, grow-pane-height, shrink-pane-height,
52+# and equalize-panes are also unbound by default.
44 switch-tab-1 = alt+153 switch-tab-1 = alt+1
45 # ... through switch-tab-9 = alt+954 # ... through switch-tab-9 = alt+9
46 55
@@ -60,6 +69,7 @@ bar.interval = 5s
60 # Command palette codes: 2-3 letters or digits.69 # Command palette codes: 2-3 letters or digits.
61 code.new-tab = CNT70 code.new-tab = CNT
62 code.new-session = CNS71 code.new-session = CNS
72+code.create-ephemeral-session = CES
63 code.close-tab = CLT73 code.close-tab = CLT
64 code.split-right = SPR74 code.split-right = SPR
65 code.split-left = SPL75 code.split-left = SPL
@@ -74,6 +84,14 @@ code.focus-pane-left = FPL
74 code.focus-pane-right = FPR84 code.focus-pane-right = FPR
75 code.focus-pane-up = FPU85 code.focus-pane-up = FPU
76 code.focus-pane-down = FPD86 code.focus-pane-down = FPD
87+code.resize-pane = RSZ
88+code.grow-pane-width = GPW
89+code.shrink-pane-width = SPW
90+code.grow-pane-height = GPH
91+code.shrink-pane-height = SPH
92+code.equalize-panes = EQP
93+code.move-pane = MFP
94+code.move-tab = MAT
77 code.next-tab = NXT95 code.next-tab = NXT
78 code.previous-tab = PVT96 code.previous-tab = PVT
79 code.back-session = BCK97 code.back-session = BCK
@@ -84,39 +102,25 @@ code.toggle-floating-pane = TFP
84 code.rename-session = RNS102 code.rename-session = RNS
85 code.rename-tab = RNT103 code.rename-tab = RNT
86 code.detach = DET104 code.detach = DET
105+code.notifications = NTC
106+code.yank-last-notification = YLN
87 ```107 ```
88 108
89-Invalid values log a warning and resolve that setting to its default on both initial load and reload.
90-
91 ## Scrollback109 ## Scrollback
92 110
93-`vev` chooses the retention policy; `vev-vt` enforces it. The PTY transports111+| Key | Range | Default | Zero means |
94-bytes and does not own scrollback.112+|---|---|---|---|
95-113+| `scrollback.megabytes` | 0–4096 MB | 50 | no history |
96-- `scrollback.megabytes`: 0–4096 decimal MB, default 50. Zero disables history.114+| `scrollback.lines` | 0–1,000,000 | 10,000 | no line limit, bytes only |
97-- `scrollback.lines`: 0–1,000,000 lines, default 10,000. Zero removes the115+
98- additional line ceiling when the byte budget is positive.116+- Limits are per pane, including hidden floating panes.
99-- Limits apply independently to each pane, including hidden floating panes,117+- They count uncompressed history, not process memory.
100- and exclude visible primary/alternate grids. They measure uncompressed118+- A reload applies to existing panes right away. Lowering a limit drops the oldest rows.
101- logical history bytes, not total process RSS.119+- Idle panes compress old history in the background. This saves memory but does not raise the limit.
102-- Reload applies to existing panes immediately. Lowering a limit evicts the
103- oldest rows; disabling history discards its retained contents. Existing copy
104- documents remain stable. New and restored panes use the current limits.
105-- Idle maintenance visits at most one sealed page per pane every five seconds,
106- skips busy panes, and compresses only pages that remain cold. Compression does
107- not grant additional retention or count compressed bytes toward the budget.
108-
109-Persistence has separate per-blob and aggregate safety ceilings; raising the
110-runtime retention budget does not remove them. The compact codec replaces the
111-old VT encoding directly: old VT history blobs are not readable by this alpha
112-build. Test with `VEV_ENV=dev`, not your normal daemon's persisted state.
113-
114-See [compact storage measurements](compact-storage-benchmarks.md) for memory,
115-allocation and CPU trade-offs.
116 120
117 ## Development environments121 ## Development environments
118 122
119-Set `VEV_ENV` to a name such as `dev` to redirect all vev-owned paths into `.dev/<name>/` under the directory where vev was invoked. The name must be one safe segment of at most 64 ASCII letters, digits, dots, underscores, or hyphens, and must start with a letter or digit.123+Set `VEV_ENV=<name>` to move every vev file into `.dev/<name>/` under the current directory. Use it to test a build without touching your real sessions.
120 124
121 ```sh125 ```sh
122 VEV_ENV=dev go run .126 VEV_ENV=dev go run .
@@ -124,25 +128,29 @@ VEV_ENV=dev go run . kill --sessions
124 rm -rf .dev/dev128 rm -rf .dev/dev
125 ```129 ```
126 130
127-The selected absolute root is retained by the detached daemon and inherited vev processes even when their working directory changes. A name isolates configuration, runtime files, durable sessions, hosts, snapshots, and logs from normal XDG locations and from other names. Invocations using the same name intentionally share a daemon and files; use separate names for side-by-side binaries.131+- Name: up to 64 letters, digits, `.`, `_`, or `-`, starting with a letter or digit.
128-132+- The same name shares one daemon. Use different names to run builds side by side.
129-If the runtime path would exceed the portable Unix-socket limit (103 bytes, including `vev/broker/broker.sock`), sockets and lifecycle locks instead use `/tmp/vev-<uid>-<sha256>`; the hash identifies the complete original runtime directory. Configuration and durable state remain under `.dev/<name>/`. All roles resolve the same private runtime directory, and distinct roots remain isolated. Short paths keep their existing location. Stop processes using an older binary before switching versions if their runtime location changes; do not delete a live runtime directory or its lock files.133+- Config, sessions, hosts, snapshots, and logs are isolated.
130-134+- If the socket path gets too long, sockets move to `/tmp/vev-<uid>-<hash>`. Everything else stays in `.dev/<name>/`.
131-This feature isolates vev-owned data only. It does not sandbox pane processes or provide OS, filesystem, or network isolation.135+- It is not a sandbox: pane processes see your real system.
132 136
133 ## Remote hosts137 ## Remote hosts
134 138
135-Remote host commands, listing, and successful direct-attach learning are always active.139+```sh
136-140+vev host add user@host # add a host
137-`$XDG_STATE_HOME/vev/hosts.json` (or `~/.local/state/vev/hosts.json` when unset) is the only host-list location. It is versioned JSON with pinned and learned targets. `vev host add` adds a pinned target, `vev host rm` atomically removes a target from both sets, and `vev host list` shows `pinned`, `learned`, or `pinned,learned` in its `SOURCE` column. Pinned hosts keep stored order; learned-only hosts follow in lexical order. vev rejects empty targets, surrounding whitespace, and internal whitespace or control characters; SSH alias grammar is left to OpenSSH.141+vev host rm user@host # remove it
142+vev host list # show hosts and their status
143+```
138 144
139-`vev ls <host>` and `vev ls --all` run `ssh -- <host> 'vev cmd remote-catalog --json'` for each known host. OpenSSH resolves aliases and connection settings from your SSH config. Remote session names appear as `session@host`. `vev ls --all` prints local sessions first, then remote sessions in merged host order. A catalog failure is reported after the successful output with the host and error; the command exits non-zero so partial output is not mistaken for a complete inventory.145+- The broker stores hosts in `~/.local/state/vev/broker/state/state.json`.
146+- Hosts connect over SSH, so your SSH config (aliases, keys) applies.
147+- Remote sessions appear as `session@host` in `vev ls --all` and in the picker.
140 148
141 ### Warm remote transports149 ### Warm remote transports
142 150
143-The connection broker keeps a remote's physical transport warm after the last terminal client leaves it. Returning to that remote opens a new logical stream over the retained, already authenticated SSH or QUIC connection: no second SSH bootstrap, QUIC handshake, or remote helper start. Broker observation probes borrow the same pooled transport instead of dialing their own. The remote daemon sees a fresh attach; session state lives on the daemon, so the screen and scrollback are unchanged.151+After you leave a remote, vev keeps its connection open for a while. Going back is then instant: no new SSH login or QUIC handshake.
144 152
145-Retention is set in the broker configuration, `~/.config/vev/broker.json` (`$XDG_CONFIG_HOME` respected), which the broker creates on first use. It is read when the broker starts.153+Configure this in `~/.config/vev/broker.json`. The file is created on first use and read when the broker starts.
146 154
147 ```json155 ```json
148 {156 {
@@ -153,61 +161,76 @@ Retention is set in the broker configuration, `~/.config/vev/broker.json` (`$XDG
153 }161 }
154 ```162 ```
155 163
156-- `warmTransports`: 0–64 idle transports kept warm, default 8. When one more goes idle, the least recently idled one closes. `0` closes a transport as soon as its last stream ends. Transports with an attached client never count.164+- `warmTransports`: how many idle connections to keep (0–64, default 8). The oldest one closes first. `0` disables it.
157-- `warmIdleTimeout`: `off` (default, no age bound) or a positive Go duration up to `24h`, such as `30s` or `5m`. A warm transport closes that long after it went idle.165+- `warmIdleTimeout`: close an idle connection after this long, such as `5m` (max `24h`). Default `off`: no time limit.
158 166
159-A remote closing the connection also evicts it; the next visit dials normally. Warm transports never keep the broker alive: when the broker stops after its idle grace, it closes them. Aliases stay distinct unless the broker authenticated them as the same daemon under the same connection policy. These keys replace the client-side `remote.attachment-cache`, `remote.attachment-cache-capacity`, and `remote.attachment-cache-idle-timeout` settings, which no longer exist.167+Warm connections never keep the broker running.
160 168
161 ## Logs and durable state169 ## Logs and durable state
162 170
163-Set `VEV_LOG=debug`, `VEV_LOG=warn`, or `VEV_LOG=error` to change verbosity; the default is `info`. JSON-line logs such as `vev-daemon.log` live in `$XDG_STATE_HOME/vev`, or `~/.local/state/vev` when unset. The same state directory contains the strict session catalogue, any private pre-migration catalogue backup, notices, `hosts.json`, and `snapshots/`. The lifecycle lock and socket live in `$XDG_RUNTIME_DIR/vev` (with platform runtime fallbacks).171+| What | Where |
164-172+|---|---|
165-Recovery events include `lifecycle_owner_wait`, `lifecycle_owner_acquired`, `lifecycle_owner_released`, `catalogue_validated`, `catalogue_compaction_recovery_complete`, `session_restore_complete`, `fallback_checkpoint_promoted`, `snapshot_head_repair_complete`, `session_degraded`, `snapshot_garbage_collection_complete`, `interrupted_transaction_recovery_complete`, and `daemon_startup_complete`.173+| Logs (JSON lines, e.g. `vev-daemon.log`) | `~/.local/state/vev/` |
174+| Sessions and snapshots | `~/.local/state/vev/` |
175+| Broker hosts and logs | `~/.local/state/vev/broker/` |
176+| Socket and lock | `$XDG_RUNTIME_DIR/vev/` |
166 177
167-Snapshot garbage collection is startup-only: the daemon runs one coordinator-owned pass before it publishes its socket, and no periodic snapshot collection runs afterward. The five-second idle maintenance loop compresses sealed scrollback pages and never collects snapshots or incarnations (GO-002, deferred).178+`$XDG_STATE_HOME` is respected. Set `VEV_LOG=debug|warn|error` to change the log level (default `info`).
168 179
169-Catalogue failure is fail-closed: vev does not publish an empty replacement daemon. Preserve the state directory, inspect `catalogue_validation_failed`, correct storage or ownership problems, and retry without editing catalogue files. See [Durable session recovery](durable-session-recovery.md) for explicit recovery commands, migration, diagnostics, and the committed checkpoint plus up to two direct fallbacks retention policy.180+If the daemon refuses to start because its session data is broken, do not edit the files. See [durable session recovery](durable-session-recovery.md).
170 181
171 ## Theme182 ## Theme
172 183
173-With `theme = auto`, vev follows the terminal's reported foreground, background, light/dark scheme, and ANSI palette. `theme.palette = on` is the default. `theme.accent = auto` derives one accent from the terminal's chromatic ANSI colors; larger and more cohesive repeated-color groups are preferred, then conventional normal/bright pairs, otherwise vev uses an eligible blue slot when available. `theme.accent = 0` through `theme.accent = 15` selects exactly that ANSI slot. Arbitrary RGB values and `off` are not accepted for `theme.accent`.184+| Setting | Effect |
174-185+|---|---|
175-When the terminal provides truecolor defaults and a usable RGB palette slot, vev uses the resolved accent for active chrome and derives softer bar, inactive, recent-session, and border colors from it. It preserves readability by reducing a surface intensity when needed. Terminal application/pane colors are not recolored. Without truecolor default colors or a usable RGB resolved accent, chrome backgrounds remain neutral; an available selected ANSI slot can still decorate foregrounds or borders. An explicit unavailable slot is never replaced with another slot.186+| `theme = auto` | Follow your terminal's colors and light/dark mode. |
176-187+| `theme = dark` / `light` | Neutral built-in colors. Ignores the two settings below. |
177-Slots `0`, `7`, `8`, and `15` are valid explicit selections, but log a warning because conventional neutral slots may provide little or no accent separation. An invalid accent value logs a warning and falls back to `auto`, including on hot reload.188+| `theme.palette = off` | Neutral colors, no accent. |
189+| `theme.accent = auto` | Pick an accent from your terminal's ANSI colors (blue if unsure). |
190+| `theme.accent = 0`–`15` | Use exactly that ANSI color as the accent. |
178 191
179-`theme.palette = off` is authoritative: it keeps exact neutral foreground/background rendering and ignores `theme.accent`. Forced `theme = dark` or `theme = light` is also neutral and ignores both palette and accent policy. Configuration reload immediately reapplies the current terminal snapshot; vev updates a live palette when the terminal reports a light/dark scheme change, but cannot detect palette changes a terminal does not report.192+- vev only colors its own UI (bars, borders, palette). Pane content is never recolored.
193+- Tinted backgrounds need a truecolor terminal. Otherwise the accent only colors text and borders.
194+- vev follows light/dark switches your terminal reports. It cannot see palette changes the terminal does not report.
180 195
181 ## Bindings196 ## Bindings
182 197
183-Key specs: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` through `alt+9`. Configuring an action replaces all of its built-in aliases (set `focus-pane-left` and the Alt+Arrow alias is gone). Tab switching also accepts the top-row symbols of non-QWERTY layouts, so AZERTY works without extra config.198+Valid keys: `alt+<char>`, `alt+space`, `alt+left/right/up/down`, `alt+1` to `alt+9`.
184 199
185-`alt+[` is unsupported because terminals frame it as the CSI prefix `ESC [`, which vev passes through as terminal input. Cmd/Super is not a vev key-spec modifier; map a physical Cmd/Super chord in the terminal emulator to an unused, safe `ESC` + character sequence (not `ESC [`), then configure the matching `alt+<char>` in vev.200+- Setting an action replaces all its default keys. For example, setting `focus-pane-left` removes Alt+Left too.
201+- Tab switching works with AZERTY and other layouts without extra config.
202+- `alt+[` is not supported: terminals use it for escape sequences.
203+- Cmd/Super is not supported. Map it in your terminal to `ESC` + a character, then bind `alt+<char>` in vev.
186 204
187 ## Pane consume or expel205 ## Pane consume or expel
188 206
189-The `consume-or-expel-pane-left` and `consume-or-expel-pane-right` actions are unbound by default; the commented Alt+H/Alt+L bindings above are optional examples. Use palette codes `MPL` and `MPR`, or script them as `vev cmd consume-or-expel-pane-left` and `vev cmd consume-or-expel-pane-right`.207+Moves the focused pane between columns, like the Niri window manager. It has no default key: use palette codes `MPL` / `MPR`, or bind `consume-or-expel-pane-left/right`.
190 208
191-A singleton pane moves into the immediate column on the requested side; at the outer edge, nothing changes. A pane in a multi-member vertical or stack column moves out as an adjacent singleton column. This works only with canonical column layouts: one column, or a top-level horizontal split of columns, where each column is a singleton pane, a vertical split of panes, or a pane stack. Nested mixed splits are unsupported.209+- A pane alone in its column joins the column next to it.
210+- A pane sharing a column leaves it and becomes its own column.
211+- Works only with column layouts. Nested mixed splits are not supported.
192 212
193 ## Navigation overflow213 ## Navigation overflow
194 214
195-Both settings are independent and default to `off`. With `nav.overflow-tabs = on`, left/right keyboard focus (`Alt+H`/`Alt+L` by default) continues from a pane edge to the adjacent tab. With `nav.overflow-sessions = on`, up/down keyboard focus (`Alt+K`/`Alt+J` by default) continues to the adjacent live session in alphabetical order. Neither setting wraps at the first or last destination.215+Both are `off` by default.
196 216
197-Overflow applies only to keyboard focus actions; mouse navigation does not overflow, and floating panes never overflow. Hot reload applies either setting to subsequent navigation without restarting the daemon.217+- `nav.overflow-tabs = on`: Alt+h/l at the edge of the tab moves to the next tab.
218+- `nav.overflow-sessions = on`: Alt+k/j at the edge moves to the next session (alphabetical).
219+
220+No wrap-around. Only keyboard focus overflows, never the mouse or floating panes.
198 221
199 ## Copy mode222 ## Copy mode
200 223
201-Scroll up with the mouse to enter copy mode. Use `h`, `j`, `k`, and `l` to move, `w`, `b`, and `e` for word motions, `v` or Space to start line selection, and `y` or Enter to copy.224+Scroll up with the mouse to enter. Scroll back to the bottom to leave.
202 225
203-The mouse wheel moves the viewport immediately in either direction, independently226+| Key | Action |
204-of keyboard cursor navigation. Scrolling has a short, decelerating animation:227+|---|---|
205-the first row responds immediately and the remaining movement is paced at 16 ms228+| `h` `j` `k` `l` | move |
206-intervals. Reversing direction cancels the old tail. Keyboard input and mouse229+| `w` `b` `e` | word motions |
207-selection stop the animation; selection scrolling remains immediate. Returning230+| `v` or Space | start selection |
208-to the bottom exits copy mode and shows live output.231+| `y` or Enter | copy |
209 232
210-Mouse drag selects a text range. Double-click selects the word under the pointer; dragging after a double-click extends by complete words.233+With the mouse: drag to select, double-click to select a word.
211 234
212 ```ini235 ```ini
213 # Unicode whitespace always separates words.236 # Unicode whitespace always separates words.
@@ -217,34 +240,32 @@ copy.word-separators = " -_@"
217 copy.reduce-motion = off240 copy.reduce-motion = off
218 ```241 ```
219 242
220-Set `copy.word-separators = ""` to use only Unicode whitespace as a separator.243+- `copy.word-separators = ""` uses only whitespace.
221-Set `copy.reduce-motion = on` for immediate three-row wheel steps. The setting244+- `copy.reduce-motion = on` scrolls three rows per wheel step, without animation.
222-applies to subsequent wheel input after configuration reload. Applications that
223-capture the mouse, and alternate-screen wheel-to-arrow forwarding, are unchanged.
224-
225-Motion is row-based: vev cannot animate fractional pixels inside the outer
226-terminal. The easing preserves requested scroll distance rather than adding
227-extra momentum, and drains any remaining tail on the first animation tick at
228-least 120 ms after the last wheel input.
229 245
230 ## Responsive overlays246 ## Responsive overlays
231 247
232-On complete frames below 80 columns, interactive overlays use full-width bottom drawers immediately above the bottom bar. This applies to the floating terminal, command palette, session picker, notification history, prompts, and copy search. A drawer keeps its overlay's preferred outer height, capped at the frame height minus four rows, so frame rows 0–2 remain visible. Drawers use only a top border.248+Below 80 columns, popups (palette, picker, floating terminal, prompts) become full-width drawers at the bottom of the screen. The background dims while a popup is open.
233-
234-Opening an interactive overlay dims the complete underlying frame, including panes, both bars, notices, and any lower-priority overlay. Notice toasts remain compact rather than becoming drawers. Copy mode remains full-screen; only its search prompt uses the responsive drawer.
235 249
236 ## Palette anchor250 ## Palette anchor
237 251
238-The command palette is centered by default. With `palette.anchor = auto`, it uses a full-width bottom shelf from 80 through 95 columns and a 64-column bottom-right rail from 96 columns up. Set an explicit anchor to position the shelf or rail. Below 80 columns, the palette is always a bottom drawer, so an explicit anchor does not override the responsive layout. Reload moves an open palette without losing your query.252+- Default: centered.
253+- `palette.anchor = auto`: bottom bar from 80 to 95 columns, right-side rail from 96 columns.
254+- Any other value places it at that spot. Below 80 columns it is always a bottom drawer.
239 255
240 ## Floating terminal256 ## Floating terminal
241 257
242-The command runs through your shell. A changed command applies on the next launch; changed dimensions on the next show or resize. Below 80 columns, `floating.height` continues to determine the drawer's outer height, capped to preserve the first three frame rows and the bottom bar; `floating.width` is replaced by the full frame width. Floating state is not restored across daemon restarts.258+- `floating.command` runs through your shell. Changes apply on the next launch.
259+- Below 80 columns, the terminal takes the full width and keeps `floating.height`.
260+- Floating terminals are not restored after a daemon restart.
243 261
244 ## Bar anchors262 ## Bar anchors
245 263
246-Anchor commands run on the daemon host every `bar.interval` (minimum 1s). vev reads the first line of stdout, strips ANSI codes, and keeps the last good value on failure. Scripts get `VEV_ANCHOR`, `VEV_SESSION`, `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, and `VEV_COLS`.264+`bar.top-right` and `bar.bottom-right` show the output of a command in the bar. Both are off by default.
247 265
248-Commands resolve against the environment of the client currently attached to the session, the same environment new panes inherit. A command that works in your shell will work as an anchor without restarting the daemon. When an anchor fails, the daemon logs the exit code and the command's stderr; exit 127 means the command was not found on that `PATH`.266+- The command runs on the daemon host every `bar.interval` (minimum `1s`), with the same environment as your panes.
267+- vev shows the first line of output, without colors. On failure it keeps the last good value.
268+- Scripts receive `VEV_ANCHOR`, `VEV_SESSION`, `VEV_TAB`, `VEV_PANE`, `VEV_PANE_CWD`, and `VEV_COLS`.
269+- Failures are logged with the exit code and stderr. Exit 127 means "command not found".
249 270
250-Both anchors are disabled by default, so `go install github.com/bnema/vev@latest` needs no companion scripts. Set either command to opt in; explicit commands are run unchanged. Checkout and release installs include `vev-bar-top-right` and `vev-bar-bottom-right` example scripts, which can be configured by name when they are on the attaching client's `PATH`. The bottom-right example runs `git status --porcelain` on each refresh; raise the interval or replace it if that is too heavy for your repository.271+Release installs ship two example scripts: `vev-bar-top-right` and `vev-bar-bottom-right`. The second one runs `git status` on every refresh; raise the interval on large repositories.
M docs/durable-session-recovery.md +38 -27
@@ -1,47 +1,58 @@
11 # Durable session recovery
22
3-vev opens named-session state only while holding `$XDG_RUNTIME_DIR/vev/lifecycle.lock` (or the platform runtime fallback). Catalogue corruption prevents socket publication; vev never substitutes an empty healthy daemon.
3+Named sessions are saved to disk and come back after a daemon restart. This page explains what is saved, what can go wrong, and how to recover.
44
5-## Session states
5+## What is saved
66
7-`stopped` is safe fresh metadata, `restoring` is validation in progress, and `degraded` preserves uncertain state without starting a replacement shell. A degraded session remains visible in `vev ls`, but attach is refused until an explicit recovery action succeeds. A session reserved for durable purge is hidden from listings and pickers until deletion either completes or leaves a retained broken record.
7+- Session name and working directory.
8+- Tabs and layout.
9+- Scrollback and the last visible screen, restored as copy-mode history.
10+- Allowed processes are relaunched (see `snapshot.restore_processes` in [configuration](configuration.md)).
811
9-## Bulk purge
12+The live process state, cursor, and terminal modes are not restored.
1013
11-`kill --sessions` purges every live, stopped, and broken session while leaving the daemon running. Its admission gate is transient and each phase spends its own bounded deadline (admission drain, stopped/broken sweep, shared live teardown), so an uncooperative repository can never hold purge admission or control indefinitely. A durable delete that ignores cancellation is completed by a detached worker under the exact session name, incarnation, and creation time it captured, so it can never remove a same-name replacement; until it returns, the stopped authority still reserves the name and the purge reports a typed failure. A live unit whose teardown aborts before it owns destructive work stays registered, and its snapshot-coordinator quarantine is rolled back so it keeps checkpointing.
14+vev keeps the latest checkpoint and the one before it as a fallback.
1215
13-## Recovery commands
16+## Session states in `vev ls`
1417
15-- `vev cmd -s NAME session-recovery discard`
18+| State | Meaning |
19+|---|---|
20+| `up` | Running. |
21+| `temporary` | Running numbered session. Not saved to disk. |
22+| `down` | Saved and stopped. Attaching restarts it. |
23+| `broken` | Saved state could not be loaded safely. Attach is refused until you recover it. |
1624
17-Discard creates a new incarnation and retains the old record and snapshots under `snapshots/quarantine/` until an explicit later purge.
25+## Recover a broken session
1826
19-## Catalogue format upgrades
27+```sh
28+vev cmd -s NAME session-recovery discard
29+```
2030
21-Catalogue format version 6 is independent from the live client/daemon protocol. At startup, vev losslessly converts supported version 3–5 records to version 6 before publishing its socket. The conversion preserves session identity, working directory, tabs, timestamps, checkpoint references, and degradation state; a wire-protocol update alone never resets durable sessions.
31+This deletes the saved tabs, layout, and history of `NAME`. The name and working directory are kept, so the next attach starts a fresh session.
2232
23-Before rewriting a legacy catalogue, vev creates and syncs a private `sessions.kv.pre-v6.bak` backup beside it. The catalogue is then replaced atomically. If the source is corrupt, from an unsupported future format, or conflicts with an existing backup, startup fails closed and leaves the catalogue untouched. Version 6 is not readable by older binaries; to roll back, stop vev and restore the backup before starting the older binary.
33+## Upgrades
2434
25-## Incompatible checkpoints
35+- A new vev release converts the old session catalogue automatically at startup. A backup is written next to it as `sessions.kv.pre-v6.bak`.
36+- To go back to an older vev, stop vev and restore that backup first.
37+- When the checkpoint format changes, old checkpoints are reset: names and working directories stay, but tabs, layout, and history are cleared.
38+- Installing a new binary does not restart a running daemon. Run `vev kill --all` to switch.
2639
27-Compact VT storage uses VEVM version 4 and VEVS version 5. Dense VT checkpoints are intentionally incompatible: there is no scrollback migration or legacy decoder. On the next daemon startup, healthy older VEVM checkpoints take the reset path below. This clears saved tabs, layout, screen contents, scrollback, and recovery transcripts while preserving session names and working directories. The live client/daemon protocol is unchanged. Installing a binary does not restart an existing daemon.
40+## When the daemon refuses to start
2841
29-After verifying a VEVM manifest's digest, vev treats any VEVM version mismatch as an incompatible healthy checkpoint. It atomically replaces only that named session's exact healthy checkpoint with a fresh incarnation. Unlike a protocol reset, this replacement retains the session name and working directory, but has no checkpoint, tabs, terminal history, or recovery transcript.
42+If the session catalogue cannot be read, the daemon stops instead of starting empty. This protects your data.
3043
31-Digest mismatches, corruption, validation failures, I/O errors, and ambiguous failures are never reset or purged. The session remains degraded for explicit recovery.
44+1. Do not edit or delete files in `~/.local/state/vev/`.
45+2. Look for `catalogue_validation_failed` in `~/.local/state/vev/vev-daemon.log`.
46+3. Fix the cause (disk full, wrong file owner), then start vev again.
47+4. For real corruption, back up the whole directory before trying anything else.
3248
33-## Retention
49+A `lifecycle_owner_wait` log line means another daemon is still starting or stopping. Wait for it to finish.
3450
35-Healthy sessions retain the committed checkpoint and up to two direct fallbacks. Degraded and unresolved state remains pinned.
51+## Where files live
3652
37-## Fail-closed startup
53+| What | Where |
54+|---|---|
55+| Catalogue, snapshots, logs | `~/.local/state/vev/` |
56+| Socket and lock | `$XDG_RUNTIME_DIR/vev/` |
3857
39-If the catalogue cannot be opened or validated, the daemon does not publish its socket. Do not delete or edit state files. Check `vev-daemon.log` for `catalogue_validation_failed`, preserve the complete state directory, correct filesystem ownership or capacity problems, and retry. For corruption, make a backup before attempting recovery or exporting data.
40-
41-A `lifecycle_owner_wait` event normally means another daemon is initializing or shutting down. Wait for ownership transfer; only terminate the prior process when its identity is known. The operating system releases `lifecycle.lock` when that process exits.
42-
43-## Diagnostics
44-
45-Durable state and catalogue migration backups are under `$XDG_STATE_HOME/vev` (default `~/.local/state/vev`). Snapshot quarantine is under its `snapshots/quarantine/` tree. Runtime ownership is under `$XDG_RUNTIME_DIR/vev`; logs are JSON lines in the state directory.
46-
47-Use session names, incarnation IDs, generation numbers, reason codes, and cursors from recovery events when diagnosing a failure. Recovery logs never include terminal or snapshot object contents.
58+Recovery logs never contain terminal content.
@@ -1,47 +1,58 @@
1 # Durable session recovery1 # Durable session recovery
2 2
3-vev opens named-session state only while holding `$XDG_RUNTIME_DIR/vev/lifecycle.lock` (or the platform runtime fallback). Catalogue corruption prevents socket publication; vev never substitutes an empty healthy daemon.3+Named sessions are saved to disk and come back after a daemon restart. This page explains what is saved, what can go wrong, and how to recover.
4 4
5-## Session states5+## What is saved
6 6
7-`stopped` is safe fresh metadata, `restoring` is validation in progress, and `degraded` preserves uncertain state without starting a replacement shell. A degraded session remains visible in `vev ls`, but attach is refused until an explicit recovery action succeeds. A session reserved for durable purge is hidden from listings and pickers until deletion either completes or leaves a retained broken record.7+- Session name and working directory.
8+- Tabs and layout.
9+- Scrollback and the last visible screen, restored as copy-mode history.
10+- Allowed processes are relaunched (see `snapshot.restore_processes` in [configuration](configuration.md)).
8 11
9-## Bulk purge12+The live process state, cursor, and terminal modes are not restored.
10 13
11-`kill --sessions` purges every live, stopped, and broken session while leaving the daemon running. Its admission gate is transient and each phase spends its own bounded deadline (admission drain, stopped/broken sweep, shared live teardown), so an uncooperative repository can never hold purge admission or control indefinitely. A durable delete that ignores cancellation is completed by a detached worker under the exact session name, incarnation, and creation time it captured, so it can never remove a same-name replacement; until it returns, the stopped authority still reserves the name and the purge reports a typed failure. A live unit whose teardown aborts before it owns destructive work stays registered, and its snapshot-coordinator quarantine is rolled back so it keeps checkpointing.14+vev keeps the latest checkpoint and the one before it as a fallback.
12 15
13-## Recovery commands16+## Session states in `vev ls`
14 17
15-- `vev cmd -s NAME session-recovery discard`18+| State | Meaning |
19+|---|---|
20+| `up` | Running. |
21+| `temporary` | Running numbered session. Not saved to disk. |
22+| `down` | Saved and stopped. Attaching restarts it. |
23+| `broken` | Saved state could not be loaded safely. Attach is refused until you recover it. |
16 24
17-Discard creates a new incarnation and retains the old record and snapshots under `snapshots/quarantine/` until an explicit later purge.25+## Recover a broken session
18 26
19-## Catalogue format upgrades27+```sh
28+vev cmd -s NAME session-recovery discard
29+```
20 30
21-Catalogue format version 6 is independent from the live client/daemon protocol. At startup, vev losslessly converts supported version 3–5 records to version 6 before publishing its socket. The conversion preserves session identity, working directory, tabs, timestamps, checkpoint references, and degradation state; a wire-protocol update alone never resets durable sessions.31+This deletes the saved tabs, layout, and history of `NAME`. The name and working directory are kept, so the next attach starts a fresh session.
22 32
23-Before rewriting a legacy catalogue, vev creates and syncs a private `sessions.kv.pre-v6.bak` backup beside it. The catalogue is then replaced atomically. If the source is corrupt, from an unsupported future format, or conflicts with an existing backup, startup fails closed and leaves the catalogue untouched. Version 6 is not readable by older binaries; to roll back, stop vev and restore the backup before starting the older binary.33+## Upgrades
24 34
25-## Incompatible checkpoints35+- A new vev release converts the old session catalogue automatically at startup. A backup is written next to it as `sessions.kv.pre-v6.bak`.
36+- To go back to an older vev, stop vev and restore that backup first.
37+- When the checkpoint format changes, old checkpoints are reset: names and working directories stay, but tabs, layout, and history are cleared.
38+- Installing a new binary does not restart a running daemon. Run `vev kill --all` to switch.
26 39
27-Compact VT storage uses VEVM version 4 and VEVS version 5. Dense VT checkpoints are intentionally incompatible: there is no scrollback migration or legacy decoder. On the next daemon startup, healthy older VEVM checkpoints take the reset path below. This clears saved tabs, layout, screen contents, scrollback, and recovery transcripts while preserving session names and working directories. The live client/daemon protocol is unchanged. Installing a binary does not restart an existing daemon.40+## When the daemon refuses to start
28 41
29-After verifying a VEVM manifest's digest, vev treats any VEVM version mismatch as an incompatible healthy checkpoint. It atomically replaces only that named session's exact healthy checkpoint with a fresh incarnation. Unlike a protocol reset, this replacement retains the session name and working directory, but has no checkpoint, tabs, terminal history, or recovery transcript.42+If the session catalogue cannot be read, the daemon stops instead of starting empty. This protects your data.
30 43
31-Digest mismatches, corruption, validation failures, I/O errors, and ambiguous failures are never reset or purged. The session remains degraded for explicit recovery.44+1. Do not edit or delete files in `~/.local/state/vev/`.
45+2. Look for `catalogue_validation_failed` in `~/.local/state/vev/vev-daemon.log`.
46+3. Fix the cause (disk full, wrong file owner), then start vev again.
47+4. For real corruption, back up the whole directory before trying anything else.
32 48
33-## Retention49+A `lifecycle_owner_wait` log line means another daemon is still starting or stopping. Wait for it to finish.
34 50
35-Healthy sessions retain the committed checkpoint and up to two direct fallbacks. Degraded and unresolved state remains pinned.51+## Where files live
36 52
37-## Fail-closed startup53+| What | Where |
54+|---|---|
55+| Catalogue, snapshots, logs | `~/.local/state/vev/` |
56+| Socket and lock | `$XDG_RUNTIME_DIR/vev/` |
38 57
39-If the catalogue cannot be opened or validated, the daemon does not publish its socket. Do not delete or edit state files. Check `vev-daemon.log` for `catalogue_validation_failed`, preserve the complete state directory, correct filesystem ownership or capacity problems, and retry. For corruption, make a backup before attempting recovery or exporting data.58+Recovery logs never contain terminal content.
40-
41-A `lifecycle_owner_wait` event normally means another daemon is initializing or shutting down. Wait for ownership transfer; only terminate the prior process when its identity is known. The operating system releases `lifecycle.lock` when that process exits.
42-
43-## Diagnostics
44-
45-Durable state and catalogue migration backups are under `$XDG_STATE_HOME/vev` (default `~/.local/state/vev`). Snapshot quarantine is under its `snapshots/quarantine/` tree. Runtime ownership is under `$XDG_RUNTIME_DIR/vev`; logs are JSON lines in the state directory.
46-
47-Use session names, incarnation IDs, generation numbers, reason codes, and cursors from recovery events when diagnosing a failure. Recovery logs never include terminal or snapshot object contents.
M docs/performance.md +12 -37
@@ -1,8 +1,17 @@
11 # Performance measurement
22
3-vev measures the current session and attachment stack with process-local runtime
4-marks and harness-owned end-to-end boundaries. Measurement does not alter
5-rendering, pacing, transport policy, wire bytes, or `ProtocolVersion`.
3+For contributors. How to measure vev's latency end to end, and the budgets it should meet.
4+
5+**Quick start:**
6+
7+```sh
8+go build -o /tmp/vev ./
9+go run ./cmd/vev-perf-harness --vev-bin /tmp/vev \
10+ --manifest testdata/perf/manifest.json --out testdata/perf/results \
11+ --scenario 1x4-idle-local
12+```
13+
14+Measurement never changes rendering, pacing, transports, or wire bytes.
615
716 ## Canonical matrix
817
@@ -208,37 +217,3 @@ go test ./internal/usecase/daemon -run '^$' \
208217 go test ./internal/usecase/daemon ./internal/usecase/copy -race \
209218 -run 'TestCopyScrollAnimation|TestCopyWheel|TestScrollRows|TestRenderRowsRange'
210219 ```
211-
212-### Local comparison (2026-09-05)
213-
214-AMD Ryzen 5 7535HS, Linux amd64, Go 1.27.1; three repetitions, median time.
215-Baseline: vev `cb489d5b` with vev-vt v0.5.0. Each operation is two wheel movements
216-with 10,000 retained rows; setup and copy entry are outside the timed loop.
217-
218-| Viewport | Before, ms/pair | After, ms/pair | Last ANSI bytes/wheel, before → after |
219-| --- | ---: | ---: | ---: |
220-| 120×40 | 1.306 | 0.654 | 4,998 → 713 |
221-| 182×53 | 2.531 | 1.159 | 9,873 → 1,023 |
222-| 240×70 | 4.329 | 1.850 | 17,088 → 1,313 |
223-
224-Important limits of this comparison:
225-
226-- The old alternating-wheel loop mostly moved a cursor within a stationary
227- viewport after its first scroll. The new loop actually moves the viewport on
228- every wheel event. These are the same input events, not equivalent old/new
229- visual behavior.
230-- Allocated bytes remain roughly unchanged: about 0.66/1.16/1.95 MB per pair.
231- Transactional compact-frame copies still dominate allocations; the speedup
232- is not evidence of eliminating GC pressure.
233-- This repetitive fixture compresses snapshots very well. Encoded output payloads
234- **increase** from 574/751/896 to 762/1,072/1,362 bytes per wheel because ordinary
235- incremental output is uncompressed. Fewer ANSI bytes means less terminal work,
236- not automatically fewer network bytes. Animation also adds intermediate frames.
237-- These are in-process measurements, not terminal presentation latency or a
238- substitute for the canonical remote matrix. Subjective motion and remote
239- latency still need real-terminal validation.
240-
241-Ghostty was inspected as a design reference at local commit `c81f0b268`, especially
242-`src/renderer/generic.zig` and `src/renderer/Thread.zig`: retain viewport state,
243-track dirty work, and separate terminal capture from rendering work. vev keeps
244-its server-side ANSI architecture; no GPU renderer or Ghostty code is imported.
@@ -1,8 +1,17 @@
1 # Performance measurement1 # Performance measurement
2 2
3-vev measures the current session and attachment stack with process-local runtime3+For contributors. How to measure vev's latency end to end, and the budgets it should meet.
4-marks and harness-owned end-to-end boundaries. Measurement does not alter4+
5-rendering, pacing, transport policy, wire bytes, or `ProtocolVersion`.5+**Quick start:**
6+
7+```sh
8+go build -o /tmp/vev ./
9+go run ./cmd/vev-perf-harness --vev-bin /tmp/vev \
10+ --manifest testdata/perf/manifest.json --out testdata/perf/results \
11+ --scenario 1x4-idle-local
12+```
13+
14+Measurement never changes rendering, pacing, transports, or wire bytes.
6 15
7 ## Canonical matrix16 ## Canonical matrix
8 17
@@ -208,37 +217,3 @@ go test ./internal/usecase/daemon -run '^$' \
208 go test ./internal/usecase/daemon ./internal/usecase/copy -race \217 go test ./internal/usecase/daemon ./internal/usecase/copy -race \
209 -run 'TestCopyScrollAnimation|TestCopyWheel|TestScrollRows|TestRenderRowsRange'218 -run 'TestCopyScrollAnimation|TestCopyWheel|TestScrollRows|TestRenderRowsRange'
210 ```219 ```
211-
212-### Local comparison (2026-09-05)
213-
214-AMD Ryzen 5 7535HS, Linux amd64, Go 1.27.1; three repetitions, median time.
215-Baseline: vev `cb489d5b` with vev-vt v0.5.0. Each operation is two wheel movements
216-with 10,000 retained rows; setup and copy entry are outside the timed loop.
217-
218-| Viewport | Before, ms/pair | After, ms/pair | Last ANSI bytes/wheel, before → after |
219-| --- | ---: | ---: | ---: |
220-| 120×40 | 1.306 | 0.654 | 4,998 → 713 |
221-| 182×53 | 2.531 | 1.159 | 9,873 → 1,023 |
222-| 240×70 | 4.329 | 1.850 | 17,088 → 1,313 |
223-
224-Important limits of this comparison:
225-
226-- The old alternating-wheel loop mostly moved a cursor within a stationary
227- viewport after its first scroll. The new loop actually moves the viewport on
228- every wheel event. These are the same input events, not equivalent old/new
229- visual behavior.
230-- Allocated bytes remain roughly unchanged: about 0.66/1.16/1.95 MB per pair.
231- Transactional compact-frame copies still dominate allocations; the speedup
232- is not evidence of eliminating GC pressure.
233-- This repetitive fixture compresses snapshots very well. Encoded output payloads
234- **increase** from 574/751/896 to 762/1,072/1,362 bytes per wheel because ordinary
235- incremental output is uncompressed. Fewer ANSI bytes means less terminal work,
236- not automatically fewer network bytes. Animation also adds intermediate frames.
237-- These are in-process measurements, not terminal presentation latency or a
238- substitute for the canonical remote matrix. Subjective motion and remote
239- latency still need real-terminal validation.
240-
241-Ghostty was inspected as a design reference at local commit `c81f0b268`, especially
242-`src/renderer/generic.zig` and `src/renderer/Thread.zig`: retain viewport state,
243-track dirty work, and separate terminal capture from rendering work. vev keeps
244-its server-side ANSI architecture; no GPU renderer or Ghostty code is imported.
M docs/protocol.md +17 -23
@@ -12,12 +12,12 @@ wrong-direction frame before any mutation (`sessionwire.ErrWrongDirection`
1212
1313 Client → server: `Hello`, `Input`, `Resize`, `Detach`, `Ping`, `List`,
1414 `Kill`, `Theme`, `Ack`, `ImagePush`, `ClientNotice`, `CommandRequest`,
15-`OutputResetRequest`, `RemotePreviewRequest`,
15+`OutputResetRequest`, `SelectTab`, `RemotePreviewWatch`,
1616 `RouteAttentionSubscription`, `SamePeerSwitchRequest`,
1717 `RecentRouteSnapshot`, `RouteNavigationFailure`,
1818 `SessionCreationFailure`, `UIFence`, `NavigationInventoryRequest`,
1919 `NavigationInventoryPublication`, `NavigationInventoryFailure`,
20-`PickerClose`, `PickerSelection`, `PickerPreviewRequest`.
20+`PickerClose`, `PickerSelection`.
2121
2222 Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`,
2323 `Sessions`, `CommandResult`, `AttachTarget`, `RemotePreview`,
@@ -26,8 +26,7 @@ Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`,
2626 `RouteRetired`, `SamePeerSwitchFailure`, `UIReceipt`, `UIViewUpdate`,
2727 `NavigationInventoryResponse`, `NavigationInventoryDemand`,
2828 `NavigationInventorySelection`, `PickerOffer`, `PickerSnapshot`,
29-`PickerClosed`, `PickerResult`, `PickerFailure`, `PickerPreview`,
30-`KillResult`.
29+`PickerClosed`, `PickerResult`, `PickerFailure`, `KillResult`.
3130
3231 `RouteNavigationFailure` is the one payload that appears in both unions.
3332 `sessionwire` conversion switches (`decodeProtoClient`/`decodeProtoServer`,
@@ -66,9 +65,9 @@ negotiated ceiling.
6665 the handshake deadline.
6766
6867 First frame routing (`daemon.go:handleConn`): `Hello` → full attach;
69-`List`, `CommandRequest`, `RemotePreviewRequest`,
70-`NavigationInventoryRequest`, `Kill` are one-shot
71-control connections answered and closed. Anything else, including any
68+`List`, `CommandRequest`, `NavigationInventoryRequest`, and `Kill` are
69+one-shot control connections answered and closed. `RemotePreviewWatch`
70+streams `RemotePreview` frames until the client closes. Anything else, including any
7271 malformed first frame, gets a typed compatibility response and no session
7372 mutation:
7473
@@ -83,7 +82,7 @@ mutation:
8382 | other / unknown | `ErrorMsg{ErrInternal, "expected hello"}` | no |
8483
8584 Live checks after decode: `Hello.Version`, `CommandRequest.Version`,
86-`NavigationInventoryRequest.Version`, and `RemotePreviewRequest.Version`
85+`NavigationInventoryRequest.Version`, and the watch's `RemotePreviewRequest.Version`
8786 (independent catalogue-era schema version `1`) must equal the negotiated
8887 constant or the request fails
8988 with the version-mismatch response above. There is no multi-version
@@ -110,21 +109,16 @@ bounded to 4 KiB (`wire.PreambleLimit`, exposed as
110109 `brokerwire.CheckPreambleSize`) before scan or allocation.
111110
112111 Refusals use the shared `PreambleRejectionCode` taxonomy. There is no
113-capability negotiation in P3.1: nonzero capability bits are refused as
114-code 7 (`limit refused`), and code 6 (`out of order`) is reserved for the
115-connection dispatcher, which owns framing-order detection and never surfaces
116-it from the stateless codec. Brokerwire is not activated by production
117-composition until P3.2. The P3.3 private local endpoint
118-(`internal/adapters/brokeripc`) carries this conversation over one per-user
119-AF_UNIX socket: `broker.sock` in the per-user runtime directory, with
120-same-user peer-credential admission, bounded per-connection queues, and
121-production composition deferred to P3.4 and the P7 cutover. P3.4 adds
122-the hidden, isolated offline sandbox entries -- `_broker-serve`,
123-`_broker-launcher`, and `_broker-status` -- over an operator-supplied private
124-root, plus the hidden remote mux helpers `_broker-mux-stdio`,
125-`_broker-mux-quic-bootstrap`, and `_broker-mux-quic-proxy` that bridge a
126-provisioned private daemonmux Unix carriage to stdio or to one freshly
127-authenticated QUIC stream; ordinary production composition still waits for P7.
112+capability negotiation: nonzero capability bits are refused as code 7
113+(`limit refused`), and code 6 (`out of order`) is reserved for the
114+connection dispatcher.
115+
116+`internal/adapters/brokeripc` carries this conversation over `broker.sock`
117+in the per-user runtime directory, with same-user peer-credential admission
118+and bounded per-connection queues. The remote helpers `_broker-mux-stdio`,
119+`_broker-mux-quic-bootstrap`, and `_broker-mux-quic-proxy` bridge a remote
120+daemonmux socket to stdio or to one freshly authenticated QUIC stream.
121+
128122 Setup is fully bounded: the dialer's one context/deadline covers the Unix
129123 dial, preamble, Register send, and wait for Registered, and the server requires Register within
130124 the same accept-time handshake budget that bounded the preamble and admission.
@@ -12,12 +12,12 @@ wrong-direction frame before any mutation (`sessionwire.ErrWrongDirection`
12 12
13 Client → server: `Hello`, `Input`, `Resize`, `Detach`, `Ping`, `List`,13 Client → server: `Hello`, `Input`, `Resize`, `Detach`, `Ping`, `List`,
14 `Kill`, `Theme`, `Ack`, `ImagePush`, `ClientNotice`, `CommandRequest`,14 `Kill`, `Theme`, `Ack`, `ImagePush`, `ClientNotice`, `CommandRequest`,
15-`OutputResetRequest`, `RemotePreviewRequest`,15+`OutputResetRequest`, `SelectTab`, `RemotePreviewWatch`,
16 `RouteAttentionSubscription`, `SamePeerSwitchRequest`,16 `RouteAttentionSubscription`, `SamePeerSwitchRequest`,
17 `RecentRouteSnapshot`, `RouteNavigationFailure`,17 `RecentRouteSnapshot`, `RouteNavigationFailure`,
18 `SessionCreationFailure`, `UIFence`, `NavigationInventoryRequest`,18 `SessionCreationFailure`, `UIFence`, `NavigationInventoryRequest`,
19 `NavigationInventoryPublication`, `NavigationInventoryFailure`,19 `NavigationInventoryPublication`, `NavigationInventoryFailure`,
20-`PickerClose`, `PickerSelection`, `PickerPreviewRequest`.20+`PickerClose`, `PickerSelection`.
21 21
22 Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`,22 Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`,
23 `Sessions`, `CommandResult`, `AttachTarget`, `RemotePreview`,23 `Sessions`, `CommandResult`, `AttachTarget`, `RemotePreview`,
@@ -26,8 +26,7 @@ Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`,
26 `RouteRetired`, `SamePeerSwitchFailure`, `UIReceipt`, `UIViewUpdate`,26 `RouteRetired`, `SamePeerSwitchFailure`, `UIReceipt`, `UIViewUpdate`,
27 `NavigationInventoryResponse`, `NavigationInventoryDemand`,27 `NavigationInventoryResponse`, `NavigationInventoryDemand`,
28 `NavigationInventorySelection`, `PickerOffer`, `PickerSnapshot`,28 `NavigationInventorySelection`, `PickerOffer`, `PickerSnapshot`,
29-`PickerClosed`, `PickerResult`, `PickerFailure`, `PickerPreview`,29+`PickerClosed`, `PickerResult`, `PickerFailure`, `KillResult`.
30-`KillResult`.
31 30
32 `RouteNavigationFailure` is the one payload that appears in both unions.31 `RouteNavigationFailure` is the one payload that appears in both unions.
33 `sessionwire` conversion switches (`decodeProtoClient`/`decodeProtoServer`,32 `sessionwire` conversion switches (`decodeProtoClient`/`decodeProtoServer`,
@@ -66,9 +65,9 @@ negotiated ceiling.
66 the handshake deadline.65 the handshake deadline.
67 66
68 First frame routing (`daemon.go:handleConn`): `Hello` → full attach;67 First frame routing (`daemon.go:handleConn`): `Hello` → full attach;
69-`List`, `CommandRequest`, `RemotePreviewRequest`,68+`List`, `CommandRequest`, `NavigationInventoryRequest`, and `Kill` are
70-`NavigationInventoryRequest`, `Kill` are one-shot69+one-shot control connections answered and closed. `RemotePreviewWatch`
71-control connections answered and closed. Anything else, including any70+streams `RemotePreview` frames until the client closes. Anything else, including any
72 malformed first frame, gets a typed compatibility response and no session71 malformed first frame, gets a typed compatibility response and no session
73 mutation:72 mutation:
74 73
@@ -83,7 +82,7 @@ mutation:
83 | other / unknown | `ErrorMsg{ErrInternal, "expected hello"}` | no |82 | other / unknown | `ErrorMsg{ErrInternal, "expected hello"}` | no |
84 83
85 Live checks after decode: `Hello.Version`, `CommandRequest.Version`,84 Live checks after decode: `Hello.Version`, `CommandRequest.Version`,
86-`NavigationInventoryRequest.Version`, and `RemotePreviewRequest.Version`85+`NavigationInventoryRequest.Version`, and the watch's `RemotePreviewRequest.Version`
87 (independent catalogue-era schema version `1`) must equal the negotiated86 (independent catalogue-era schema version `1`) must equal the negotiated
88 constant or the request fails87 constant or the request fails
89 with the version-mismatch response above. There is no multi-version88 with the version-mismatch response above. There is no multi-version
@@ -110,21 +109,16 @@ bounded to 4 KiB (`wire.PreambleLimit`, exposed as
110 `brokerwire.CheckPreambleSize`) before scan or allocation.109 `brokerwire.CheckPreambleSize`) before scan or allocation.
111 110
112 Refusals use the shared `PreambleRejectionCode` taxonomy. There is no111 Refusals use the shared `PreambleRejectionCode` taxonomy. There is no
113-capability negotiation in P3.1: nonzero capability bits are refused as112+capability negotiation: nonzero capability bits are refused as code 7
114-code 7 (`limit refused`), and code 6 (`out of order`) is reserved for the113+(`limit refused`), and code 6 (`out of order`) is reserved for the
115-connection dispatcher, which owns framing-order detection and never surfaces114+connection dispatcher.
116-it from the stateless codec. Brokerwire is not activated by production115+
117-composition until P3.2. The P3.3 private local endpoint116+`internal/adapters/brokeripc` carries this conversation over `broker.sock`
118-(`internal/adapters/brokeripc`) carries this conversation over one per-user117+in the per-user runtime directory, with same-user peer-credential admission
119-AF_UNIX socket: `broker.sock` in the per-user runtime directory, with118+and bounded per-connection queues. The remote helpers `_broker-mux-stdio`,
120-same-user peer-credential admission, bounded per-connection queues, and119+`_broker-mux-quic-bootstrap`, and `_broker-mux-quic-proxy` bridge a remote
121-production composition deferred to P3.4 and the P7 cutover. P3.4 adds120+daemonmux socket to stdio or to one freshly authenticated QUIC stream.
122-the hidden, isolated offline sandbox entries -- `_broker-serve`,121+
123-`_broker-launcher`, and `_broker-status` -- over an operator-supplied private
124-root, plus the hidden remote mux helpers `_broker-mux-stdio`,
125-`_broker-mux-quic-bootstrap`, and `_broker-mux-quic-proxy` that bridge a
126-provisioned private daemonmux Unix carriage to stdio or to one freshly
127-authenticated QUIC stream; ordinary production composition still waits for P7.
128 Setup is fully bounded: the dialer's one context/deadline covers the Unix122 Setup is fully bounded: the dialer's one context/deadline covers the Unix
129 dial, preamble, Register send, and wait for Registered, and the server requires Register within123 dial, preamble, Register send, and wait for Registered, and the server requires Register within
130 the same accept-time handshake budget that bounded the preamble and admission.124 the same accept-time handshake budget that bounded the preamble and admission.
M docs/remote-resilience.md +77 -183
@@ -1,187 +1,81 @@
1-# Remote resilience
2-
3-Remote attachments connect directly to the selected vev daemon. QUIC is the
4-normal carriage: SSH bootstraps an authenticated endpoint (ephemeral
5-certificate, token, SHA-256 pin), then the attachment carries the vev
6-protocol over one QUIC stream. Set `VEV_REMOTE_TRANSPORT=stdio` to use an
7-SSH-only carriage explicitly when QUIC is unavailable. vev must be installed on
8-the remote host.
9-
10-The session remains shared across all attachments. It owns PTYs, VT state, tabs,
11-panes, shared PTY content geometry, and ordered mutations. Each attachment keeps
12-its own window size, selected tab and pane, copy mode, overlays, render/output
13-acknowledgements, and reconnect state. Reconnecting one attachment resumes that
14-attachment without replacing its peers or changing their attachment-local
15-windows/views; its latest claim may still update shared PTY geometry.
16-
17-## Remote picker identity
18-
19-The session picker shows remote live, stopped, broken, stale, and unavailable
20-rows using the same expandable session/tab shape as local rows. A row carries a
21-lifecycle ID and stable tab selector separately from its display origin; labels
22-are never parsed back into routes. Cached and stale rows remain navigable, but
23-activation is gated until the host catalog and the exact lifecycle/tab identity
24-are current. A successful picker handoff sends the structured target to the
25-owning daemon without creating a local shadow session. Picker-selected remote
26-attachments use the daemon's environment and persisted session working
27-directory; direct CLI remote attaches retain their client-request semantics.
28-
29-Remote attachments remain direct: the selected remote daemon owns the session,
30-PTYs, rendering, input, resize, effects, and teardown. The client does not
31-proxy remote content through the local daemon. The session picker and command
32-palette read remote session inventory from the daemon's durable catalog cache
33-and refresh it asynchronously while either overlay is open. Cached names appear
34-immediately, but selection still requires a fresh exact lifecycle and tab
35-identity.
36-
37-A single client process separately keeps a bounded, in-memory route history
38-across local, direct remote, and picker-discovered attachments. Catalog targets
39-retain exact endpoints, while route snapshots keep endpoints private; rows from
40-those sources therefore remain separate when their authority cannot be compared
41-exactly. The daemon renders the latest attachment snapshot; active routes are
42-metadata-only. `JRS` and cross-daemon `BCK` transitions send typed
43-key/generation actions back to the client, while `BCK` to a live session on the
44-current authenticated daemon uses an in-band switch. Route history is
45-process-local and is not persisted or shared between clients. The status-bar
46-history and `JRS` ranks list only routes this client has attached to, so a new
47-client starts with an empty history; the palette lists every known route.
48-
49-A picker-selected remote target is a direct handoff with one input pump. Exact
50-lifecycle/name identities are carried separately from display labels, and a
51-successful daemon-local switch publishes a new committed identity before the
52-client republishes its snapshot. These navigation actions use the strict,
53-exact-match protocol version. Attach handshakes may carry an exact target, and
54-successful Welcomes can return the daemon's committed identity.
55-
56-Selecting an ordinary active session row on the daemon already serving the
57-attachment uses an in-band switch. The daemon first offers an endpoint-empty,
58-exact lifecycle target; the client confirms it with its remembered stable tab
59-cursor while holding raw input. The daemon either commits the fenced attachment
60-transition and publishes the committed identity before the rebased full paint,
61-or sends a typed pre-commit rejection and leaves the source attachment usable.
62-No hostname, label, DNS result, or SSH alias authorizes reuse. Stopped and
63-cross-origin targets outside the hybrid QUIC flow retain direct close-and-dial
64-handoff.
65-
66-A QUIC attachment opening the hybrid home picker keeps its authenticated remote
67-transport parked while a transient local connection renders the picker. The
68-remote daemon suspends that attachment's rendered output only after a
69-lease-bound prepare handshake, so the picker has sole terminal ownership and
70-the client does not acknowledge screen updates it did not display. Terminal-
71-external one-shot effects generated while parked, such as OSC 52 clipboard
72-writes, are intentionally suppressed and are not replayed after Back or a
73-switch; applying them later could affect a different terminal owner. Back
74-resumes the parked attachment's current VT state with a full paint. Selecting a
75-live or stopped session from the exact same configured endpoint sends the
76-structured lifecycle/tab target over the parked transport; the serving daemon
77-revalidates it and switches or restores with its persisted working directory
78-and daemon-owned environment. Different endpoints and SSH stdio routes close
79-and dial normally. While any switch takes longer than the short display
80-threshold, the client overlays an animated switching or starting toast until
81-the destination's authoritative output is flushed.
82-
83-Opening the home picker does not change the client's active route or recent
84-session order, on either QUIC or SSH stdio. Its status bar keeps the source
85-session label (for example, `misc@igor`), not the local session hosting the
86-picker. Local backing-session tabs and bar-script output are hidden while
87-this temporary attachment renders the picker. Selecting a destination commits
88-that route; Back returns to the source. Local tab selections also hand off to
89-the client, including selections in the backing session itself: the temporary
90-picker connection never becomes the destination attachment. Protocol version
91-40 carries the explicit local tab in `AttachTarget.PreferredTabID`; it overrides
92-remembered tab state. If that tab disappears before attach, the existing Hello
93-default-tab fallback applies. Upgrade the client and both daemons together.
94-
95-## Durable record compatibility
96-
97-Catalogue record format version 6 stores durable session metadata independently
98-from the live protocol version. At daemon startup, supported version 3–5 records
99-are losslessly converted before socket publication and the original catalogue is
100-preserved as a private `sessions.kv.pre-v6.bak` backup. A wire-protocol change
101-does not reset session state. Unsupported or corrupt records fail closed without
102-modifying the catalogue. Older binaries cannot read version 6, so restore the
103-backup before a rollback.
104-
105-Remote catalogues are bounded and versioned. The current schema is mandatory
106-and contains lifecycle IDs, ordered typed tab records, state, active tab,
107-attachment state, and MRU sequence. Peers without the exact schema are rejected
108-as version-incompatible rather than exposed as partial picker rows.
109-
110-Remote picker previews are bounded styled-cell snapshots fetched without
111-attachment or PTY mutation. They are held in a short-lived in-memory cache only;
112-viewport contents are not persisted or written to diagnostics. Live preview
113-requests use debounce, same-key single-flight, bounded concurrency, TTL
114-stale-while-revalidate, and failure cooldowns. Stopped sessions show a static
115-placeholder, and late or canceled preview responses are discarded when the
116-picker selection changes.
117-
118-## Connection limits
119-
120-- A connection handshake has one 15-second deadline from connection start
121- through Hello, Welcome, and the initial committed publication. The deadline
122- applies to local and remote connections.
123-- Each command request has a 10-second deadline for its result. A timeout
124- abandons only that request; it does not complete a later request with a stale
125- result.
126-- Each parked-route control response and its required full paint have separate
127- 15-second client deadlines. A parked lease expires after 15 minutes and
128- closes only its exact retained transport generation.
129-- Shared PTY content geometry follows the latest valid, non-superseded attach,
130- resume, or resize claim. A later attachment can therefore replace the current
131- shared geometry; if the winning attachment detaches, the most recently
132- claimed remaining valid attachment becomes authoritative.
133-
134-## QUIC operation
135-
136-- The remote host runs one ephemeral QUIC server per bootstrap: fresh
137- certificate, 32-byte token, nonce, ≤4 KiB readiness line, ≤15 s expiry,
138- and atomic one-time token consumption. The client pins the exact SHA-256
139- certificate fingerprint and answers with one bounded auth record.
140-- The QUIC carriage runs over UDP with TLS 1.3, the epoch ALPN, and
141- QUIC-native reliability, ordering, and congestion control; no custom
142- retransmission layer exists. Link health reports transport connectivity
143- without logging payloads, addresses, keys, or identities.
144-- Large full output snapshots use bounded zlib compression when it reduces the
145- wire payload. Incremental output and non-beneficial snapshots retain their
146- canonical encoding. The protocol validates the compression kind, declared
147- decoded length, stream integrity, and trailing bytes before terminal output
148- is applied.
149-- The attachment reconnects with its resume token after a path change. If the
150- attachment resume window expires but its exact session lifecycle still
151- exists, the client opens a fresh exact attachment instead. The remote session
152- and its PTYs remain in place while the attachment is offline.
153-
154-The bootstrap token and ephemeral key live only in memory. They are not
155-written to durable state.
1+# Remote sessions
2+
3+```sh
4+vev attach user@host[:session]
5+```
6+
7+vev must be installed on the remote host. The remote daemon owns the session: it runs the shells, renders the screen, and sends only small diffs to your client. Nothing is proxied through your local daemon.
8+
9+## How the connection works
10+
11+1. vev opens SSH to the host and starts a short-lived QUIC server there.
12+2. SSH hands back a one-time token and the server's certificate fingerprint.
13+3. Your client connects directly over QUIC (UDP), checks the fingerprint, and uses the token once.
14+
15+Terminal traffic then goes over QUIC, not SSH. The token and keys stay in memory only.
16+
17+If UDP is blocked, use SSH only:
18+
19+```sh
20+VEV_REMOTE_TRANSPORT=stdio vev attach user@host
21+```
22+
23+## What survives a network problem
24+
25+- The remote session and its shells keep running while you are offline.
26+- The client reconnects by itself after a network change, a VPN switch, or a laptop sleep.
27+- If the reconnect window has expired but the session still exists, the client opens a fresh attachment to it.
28+- Several clients can attach to the same session. Each keeps its own window, tab, focus, and copy mode.
15629
15730 ## Connection states
15831
15932 | State | Meaning |
160-| --- | --- |
161-| `connected` | Recent authenticated contact and complete frame progress. |
162-| `degraded` | Authenticated packets arrive, but complete-frame or acknowledgement progress is delayed. |
163-| `probing` | Authenticated contact is absent and the carriage is probing for a recovered path. |
164-| `resuming` | This attachment is opening a replacement connection with its resume token. |
165-| `offline` | The attachment is re-establishing the selected remote transport. |
166-| `expired` | The session or attachment resume window is no longer available. |
167-
168-In raw terminal mode, state changes appear as vev status text rather than bytes
169-written into the PTY's output stream.
170-
171-## Diagnostics and checks
172-
173-At debug level, transport-health logs contain only state, elapsed progress ages,
174-and bounded counters. `scripts/debug-remote-attach.sh user@host`
175-collects redacted health lines.
176-
177-Useful checks are:
178-
179-- block and unblock UDP traffic;
180-- move between networks or change VPN state;
181-- suspend and resume the attachment machine;
182-- reconnect to the same named session;
183-- verify that a second attachment retains its independent view while the first
184- reconnects.
185-
186-Keep hostnames, usernames, keys, and local paths out of shared logs and
187-screenshots.
33+|---|---|
34+| `connected` | Everything works. |
35+| `degraded` | Packets arrive, but updates are slow. |
36+| `probing` | No contact; vev is looking for a working path. |
37+| `offline` | No contact for a while; vev is reconnecting. |
38+| `dead` | The connection is gone for good. |
39+
40+States appear in the vev status bar, never inside your shell output.
41+
42+## Hybrid mode: local and remote together
43+
44+Add a host once:
45+
46+```sh
47+vev host add user@host
48+```
49+
50+Then the session picker (`SSP`) shows local and remote sessions in one list. Remote sessions appear as `session@host`, with tags like `down` or `broken` when they can't be opened.
51+
52+- Selecting a session switches to it, local or remote. `BCK` goes back to the previous one; `JRS` jumps to a recent one.
53+- A remote session opened from the picker uses the remote host's environment and saved working directory.
54+- `CNS` asks where to create the new session when several daemons are available.
55+- Session history is per client and is not saved.
56+- Previews of remote sessions are cached in memory only, never written to disk.
57+
58+After you leave a remote, vev keeps its connection warm so going back is instant. See [warm remote transports](configuration.md#warm-remote-transports).
59+
60+## Timeouts
61+
62+| What | Limit |
63+|---|---|
64+| Connect to first screen | 15 s |
65+| `vev cmd` result | 10 s |
66+| A detached remote attachment waiting to resume | 15 min |
67+
68+## Upgrading
69+
70+Client and daemons must run the same protocol version. Upgrade vev on your machine and on every remote host together.
71+
72+## Debugging
73+
74+```sh
75+VEV_LOG=debug vev attach user@host
76+scripts/debug-remote-attach.sh user@host # collects redacted connection health
77+```
78+
79+Useful manual checks: block UDP, switch networks or VPN, suspend the laptop, then reattach to the same session.
80+
81+Keep hostnames, usernames, and keys out of shared logs and screenshots.
@@ -1,187 +1,81 @@
1-# Remote resilience1+# Remote sessions
2-2+
3-Remote attachments connect directly to the selected vev daemon. QUIC is the3+```sh
4-normal carriage: SSH bootstraps an authenticated endpoint (ephemeral4+vev attach user@host[:session]
5-certificate, token, SHA-256 pin), then the attachment carries the vev5+```
6-protocol over one QUIC stream. Set `VEV_REMOTE_TRANSPORT=stdio` to use an6+
7-SSH-only carriage explicitly when QUIC is unavailable. vev must be installed on7+vev must be installed on the remote host. The remote daemon owns the session: it runs the shells, renders the screen, and sends only small diffs to your client. Nothing is proxied through your local daemon.
8-the remote host.8+
9-9+## How the connection works
10-The session remains shared across all attachments. It owns PTYs, VT state, tabs,10+
11-panes, shared PTY content geometry, and ordered mutations. Each attachment keeps11+1. vev opens SSH to the host and starts a short-lived QUIC server there.
12-its own window size, selected tab and pane, copy mode, overlays, render/output12+2. SSH hands back a one-time token and the server's certificate fingerprint.
13-acknowledgements, and reconnect state. Reconnecting one attachment resumes that13+3. Your client connects directly over QUIC (UDP), checks the fingerprint, and uses the token once.
14-attachment without replacing its peers or changing their attachment-local14+
15-windows/views; its latest claim may still update shared PTY geometry.15+Terminal traffic then goes over QUIC, not SSH. The token and keys stay in memory only.
16-16+
17-## Remote picker identity17+If UDP is blocked, use SSH only:
18-18+
19-The session picker shows remote live, stopped, broken, stale, and unavailable19+```sh
20-rows using the same expandable session/tab shape as local rows. A row carries a20+VEV_REMOTE_TRANSPORT=stdio vev attach user@host
21-lifecycle ID and stable tab selector separately from its display origin; labels21+```
22-are never parsed back into routes. Cached and stale rows remain navigable, but22+
23-activation is gated until the host catalog and the exact lifecycle/tab identity23+## What survives a network problem
24-are current. A successful picker handoff sends the structured target to the24+
25-owning daemon without creating a local shadow session. Picker-selected remote25+- The remote session and its shells keep running while you are offline.
26-attachments use the daemon's environment and persisted session working26+- The client reconnects by itself after a network change, a VPN switch, or a laptop sleep.
27-directory; direct CLI remote attaches retain their client-request semantics.27+- If the reconnect window has expired but the session still exists, the client opens a fresh attachment to it.
28-28+- Several clients can attach to the same session. Each keeps its own window, tab, focus, and copy mode.
29-Remote attachments remain direct: the selected remote daemon owns the session,
30-PTYs, rendering, input, resize, effects, and teardown. The client does not
31-proxy remote content through the local daemon. The session picker and command
32-palette read remote session inventory from the daemon's durable catalog cache
33-and refresh it asynchronously while either overlay is open. Cached names appear
34-immediately, but selection still requires a fresh exact lifecycle and tab
35-identity.
36-
37-A single client process separately keeps a bounded, in-memory route history
38-across local, direct remote, and picker-discovered attachments. Catalog targets
39-retain exact endpoints, while route snapshots keep endpoints private; rows from
40-those sources therefore remain separate when their authority cannot be compared
41-exactly. The daemon renders the latest attachment snapshot; active routes are
42-metadata-only. `JRS` and cross-daemon `BCK` transitions send typed
43-key/generation actions back to the client, while `BCK` to a live session on the
44-current authenticated daemon uses an in-band switch. Route history is
45-process-local and is not persisted or shared between clients. The status-bar
46-history and `JRS` ranks list only routes this client has attached to, so a new
47-client starts with an empty history; the palette lists every known route.
48-
49-A picker-selected remote target is a direct handoff with one input pump. Exact
50-lifecycle/name identities are carried separately from display labels, and a
51-successful daemon-local switch publishes a new committed identity before the
52-client republishes its snapshot. These navigation actions use the strict,
53-exact-match protocol version. Attach handshakes may carry an exact target, and
54-successful Welcomes can return the daemon's committed identity.
55-
56-Selecting an ordinary active session row on the daemon already serving the
57-attachment uses an in-band switch. The daemon first offers an endpoint-empty,
58-exact lifecycle target; the client confirms it with its remembered stable tab
59-cursor while holding raw input. The daemon either commits the fenced attachment
60-transition and publishes the committed identity before the rebased full paint,
61-or sends a typed pre-commit rejection and leaves the source attachment usable.
62-No hostname, label, DNS result, or SSH alias authorizes reuse. Stopped and
63-cross-origin targets outside the hybrid QUIC flow retain direct close-and-dial
64-handoff.
65-
66-A QUIC attachment opening the hybrid home picker keeps its authenticated remote
67-transport parked while a transient local connection renders the picker. The
68-remote daemon suspends that attachment's rendered output only after a
69-lease-bound prepare handshake, so the picker has sole terminal ownership and
70-the client does not acknowledge screen updates it did not display. Terminal-
71-external one-shot effects generated while parked, such as OSC 52 clipboard
72-writes, are intentionally suppressed and are not replayed after Back or a
73-switch; applying them later could affect a different terminal owner. Back
74-resumes the parked attachment's current VT state with a full paint. Selecting a
75-live or stopped session from the exact same configured endpoint sends the
76-structured lifecycle/tab target over the parked transport; the serving daemon
77-revalidates it and switches or restores with its persisted working directory
78-and daemon-owned environment. Different endpoints and SSH stdio routes close
79-and dial normally. While any switch takes longer than the short display
80-threshold, the client overlays an animated switching or starting toast until
81-the destination's authoritative output is flushed.
82-
83-Opening the home picker does not change the client's active route or recent
84-session order, on either QUIC or SSH stdio. Its status bar keeps the source
85-session label (for example, `misc@igor`), not the local session hosting the
86-picker. Local backing-session tabs and bar-script output are hidden while
87-this temporary attachment renders the picker. Selecting a destination commits
88-that route; Back returns to the source. Local tab selections also hand off to
89-the client, including selections in the backing session itself: the temporary
90-picker connection never becomes the destination attachment. Protocol version
91-40 carries the explicit local tab in `AttachTarget.PreferredTabID`; it overrides
92-remembered tab state. If that tab disappears before attach, the existing Hello
93-default-tab fallback applies. Upgrade the client and both daemons together.
94-
95-## Durable record compatibility
96-
97-Catalogue record format version 6 stores durable session metadata independently
98-from the live protocol version. At daemon startup, supported version 3–5 records
99-are losslessly converted before socket publication and the original catalogue is
100-preserved as a private `sessions.kv.pre-v6.bak` backup. A wire-protocol change
101-does not reset session state. Unsupported or corrupt records fail closed without
102-modifying the catalogue. Older binaries cannot read version 6, so restore the
103-backup before a rollback.
104-
105-Remote catalogues are bounded and versioned. The current schema is mandatory
106-and contains lifecycle IDs, ordered typed tab records, state, active tab,
107-attachment state, and MRU sequence. Peers without the exact schema are rejected
108-as version-incompatible rather than exposed as partial picker rows.
109-
110-Remote picker previews are bounded styled-cell snapshots fetched without
111-attachment or PTY mutation. They are held in a short-lived in-memory cache only;
112-viewport contents are not persisted or written to diagnostics. Live preview
113-requests use debounce, same-key single-flight, bounded concurrency, TTL
114-stale-while-revalidate, and failure cooldowns. Stopped sessions show a static
115-placeholder, and late or canceled preview responses are discarded when the
116-picker selection changes.
117-
118-## Connection limits
119-
120-- A connection handshake has one 15-second deadline from connection start
121- through Hello, Welcome, and the initial committed publication. The deadline
122- applies to local and remote connections.
123-- Each command request has a 10-second deadline for its result. A timeout
124- abandons only that request; it does not complete a later request with a stale
125- result.
126-- Each parked-route control response and its required full paint have separate
127- 15-second client deadlines. A parked lease expires after 15 minutes and
128- closes only its exact retained transport generation.
129-- Shared PTY content geometry follows the latest valid, non-superseded attach,
130- resume, or resize claim. A later attachment can therefore replace the current
131- shared geometry; if the winning attachment detaches, the most recently
132- claimed remaining valid attachment becomes authoritative.
133-
134-## QUIC operation
135-
136-- The remote host runs one ephemeral QUIC server per bootstrap: fresh
137- certificate, 32-byte token, nonce, ≤4 KiB readiness line, ≤15 s expiry,
138- and atomic one-time token consumption. The client pins the exact SHA-256
139- certificate fingerprint and answers with one bounded auth record.
140-- The QUIC carriage runs over UDP with TLS 1.3, the epoch ALPN, and
141- QUIC-native reliability, ordering, and congestion control; no custom
142- retransmission layer exists. Link health reports transport connectivity
143- without logging payloads, addresses, keys, or identities.
144-- Large full output snapshots use bounded zlib compression when it reduces the
145- wire payload. Incremental output and non-beneficial snapshots retain their
146- canonical encoding. The protocol validates the compression kind, declared
147- decoded length, stream integrity, and trailing bytes before terminal output
148- is applied.
149-- The attachment reconnects with its resume token after a path change. If the
150- attachment resume window expires but its exact session lifecycle still
151- exists, the client opens a fresh exact attachment instead. The remote session
152- and its PTYs remain in place while the attachment is offline.
153-
154-The bootstrap token and ephemeral key live only in memory. They are not
155-written to durable state.
156 29
157 ## Connection states30 ## Connection states
158 31
159 | State | Meaning |32 | State | Meaning |
160-| --- | --- |33+|---|---|
161-| `connected` | Recent authenticated contact and complete frame progress. |34+| `connected` | Everything works. |
162-| `degraded` | Authenticated packets arrive, but complete-frame or acknowledgement progress is delayed. |35+| `degraded` | Packets arrive, but updates are slow. |
163-| `probing` | Authenticated contact is absent and the carriage is probing for a recovered path. |36+| `probing` | No contact; vev is looking for a working path. |
164-| `resuming` | This attachment is opening a replacement connection with its resume token. |37+| `offline` | No contact for a while; vev is reconnecting. |
165-| `offline` | The attachment is re-establishing the selected remote transport. |38+| `dead` | The connection is gone for good. |
166-| `expired` | The session or attachment resume window is no longer available. |39+
167-40+States appear in the vev status bar, never inside your shell output.
168-In raw terminal mode, state changes appear as vev status text rather than bytes41+
169-written into the PTY's output stream.42+## Hybrid mode: local and remote together
170-43+
171-## Diagnostics and checks44+Add a host once:
172-45+
173-At debug level, transport-health logs contain only state, elapsed progress ages,46+```sh
174-and bounded counters. `scripts/debug-remote-attach.sh user@host`47+vev host add user@host
175-collects redacted health lines.48+```
176-49+
177-Useful checks are:50+Then the session picker (`SSP`) shows local and remote sessions in one list. Remote sessions appear as `session@host`, with tags like `down` or `broken` when they can't be opened.
178-51+
179-- block and unblock UDP traffic;52+- Selecting a session switches to it, local or remote. `BCK` goes back to the previous one; `JRS` jumps to a recent one.
180-- move between networks or change VPN state;53+- A remote session opened from the picker uses the remote host's environment and saved working directory.
181-- suspend and resume the attachment machine;54+- `CNS` asks where to create the new session when several daemons are available.
182-- reconnect to the same named session;55+- Session history is per client and is not saved.
183-- verify that a second attachment retains its independent view while the first56+- Previews of remote sessions are cached in memory only, never written to disk.
184- reconnects.57+
185-58+After you leave a remote, vev keeps its connection warm so going back is instant. See [warm remote transports](configuration.md#warm-remote-transports).
186-Keep hostnames, usernames, keys, and local paths out of shared logs and59+
187-screenshots.60+## Timeouts
61+
62+| What | Limit |
63+|---|---|
64+| Connect to first screen | 15 s |
65+| `vev cmd` result | 10 s |
66+| A detached remote attachment waiting to resume | 15 min |
67+
68+## Upgrading
69+
70+Client and daemons must run the same protocol version. Upgrade vev on your machine and on every remote host together.
71+
72+## Debugging
73+
74+```sh
75+VEV_LOG=debug vev attach user@host
76+scripts/debug-remote-attach.sh user@host # collects redacted connection health
77+```
78+
79+Useful manual checks: block UDP, switch networks or VPN, suspend the laptop, then reattach to the same session.
80+
81+Keep hostnames, usernames, and keys out of shared logs and screenshots.
M docs/session-navigation.md +26 -61
@@ -1,72 +1,37 @@
11 # Session navigation
22
3-Palette destinations use bare session names in local-only mode. With remote
4-hosts configured, local sessions use `session@local` and remote sessions use
5-`session@host`, relative to the attaching client regardless of the serving
6-daemon. Offline hosts still enable qualified local labels. Local inventory
7-rows replace history rows for the same exact session;
8-homonyms with different origins or lifecycles remain distinct. A resume
9-credential belongs only to the route currently holding its attachment, so
10-older sessions in history use exact attachment after a same-daemon switch.
3+## Session picker
114
12-The command palette lists sessions from two origins: native results owned by
13-the serving daemon (active, stopped, remote, recent routes) and imported rows
14-relayed from the attaching client's own local daemon. Imported rows carry
15-only opaque keys plus display metadata; resolution happens through a
16-dial-only local control connection in the same interaction namespace.
5+Open it with `SSP` in the palette. It lists local sessions and, if you added hosts, remote sessions as `session@host`.
176
18-## Inventory relay
7+| Key | Action |
8+|---|---|
9+| ↑/↓ or `j`/`k` | move |
10+| Enter | open the session or tab |
11+| `/` | search (Escape leaves search) |
12+| `s` | switch sort: recent first, or named before numbered |
13+| `x` | kill the selected session |
14+| Escape or `q` | close and go back |
1915
20-While attached to a remote daemon, the client runs one relay per attachment
21-when it holds a committed local route. The relay dials only the local
22-control socket: no Hello, no session creation, no attachment, no daemon
23-startup.
16+- Each row can expand to show its tabs, with a live preview of the selected one.
17+- Remote sessions that cannot be opened show a tag such as `down` or `broken`.
18+- While the picker is open, your keys never reach the shell underneath.
2419
25-- The serving palette sends an open demand with an interaction generation
26- when it opens, and a close demand on every successful close (execute,
27- select, or cancel). Demands are capability-gated on both sides: the
28- client advertises inventory in Hello only while a committed local route
29- and the dial-only control source are available.
30-- While open, the relay polls the local source once per second with at most
31- one query in flight plus one coalesced latest update. Only changed
32- snapshots publish, with a monotonic publication generation per
33- interaction. Admitted keys and retirements never cross interactions.
34-- Enter on an imported row sends a selection with the opaque keys and the
35- admitted input cause, which stays zero for ordinary keyboard input.
36- Selection crosses before the close demand on the guarded serving
37- connection.
38-- Resolve revalidates the key against a fresh capture and returns a
39- non-mutating attach target. Endpoint-empty targets resolve against the
40- committed local route as authority; the serving route stays the recovery
41- destination through the inventory transition (settle once, restore on
42- every destination failure).
43-- Failures (stale identity, gone source, incompatible, invalid target,
44- navigation or restore failure) surface as palette feedback with the exact
45- cause while the originating interaction is still open; stale failures
46- from closed interactions drop. Native commands stay usable throughout.
20+## Jumping between sessions
4721
48-## Client-owned picker
22+| Code / key | Action |
23+|---|---|
24+| `BCK` | back to the previous session |
25+| `JRS <rank>` | jump to a recent session by rank |
26+| Alt+a | jump to a session that needs attention (bell) |
27+| `CNS` | create a named session; asks where when several daemons are available |
28+| `CES` | create a numbered session |
4929
50-The navigation picker runs as a typed client interaction when the serving attachment advertises the client-picker capability: the palette command `SSP` (session picker) opens it, and a client with a home route answers that command with a home-picker navigation directive instead, so the picker stays daemon-rendered on the home route.
30+Recent-session history belongs to one client and is not saved. A new client starts with an empty history.
5131
52-The client renders the picker frame from the admitted snapshot rows, moves the cursor and filters with `/` locally, and sends only a typed selection (opaque row key plus displayed revision) or a close. The serving daemon re-resolves the key, revalidates the target lifecycle, and performs the unchanged navigation handoff; a selection must carry exactly the displayed revision, and a stale revision, unknown key, retired target, or failed navigation returns a typed failure instead of committing. Escape closes the interaction without selecting, `s` (sort) and `x` (kill) are unavailable in this mode, and the cursor/search state is local to the interaction.
32+## Move panes and tabs
5333
54-While the picker is open it owns terminal input on every path: physical keys and admitted automation batches reach the client picker and never the session, and the serving daemon drops raw key and mouse input for that attachment for the same window. After a close, the client keeps consuming and dropping input until the daemon's authoritative repaint is displayed, so nothing typed at the picker is replayed into the shell; routing resumes at that boundary. If the attachment generation changes before that repaint (a reconnect), the lease is aborted with the attachment and the new generation starts from normal routing.
34+- `MFP` moves the focused pane to another tab.
35+- `MAT` moves the current tab to another session.
5536
56-## Remote vision
57-
58-Remote-origin rows display availability tags (`down`, `broken`, stopped)
59-inline so they never look attachable. Remote vision is read-only: Enter on
60-a remote row reports feedback and emits no selection, and selections for
61-non-local sources reject before resolve. Non-OK groups project no rows.
62-
63-## Compatibility
64-
65-Demands gate both sides on the negotiated inventory capability: a serving
66-palette that never receives the bit never demands, and a client without a
67-committed local route or control dialer ignores demands that decode. With
68-no demands the relay stays idle with zero control dials, and local
69-attachments ignore demands even when they decode. Unknown frame types
70-stay ignored by the receive pump. Cross-version attachment still fails at
71-the strict-equality handshake; graceful degradation applies only within
72-one negotiated version.
37+Both open a small picker to choose the destination.
@@ -1,72 +1,37 @@
1 # Session navigation1 # Session navigation
2 2
3-Palette destinations use bare session names in local-only mode. With remote3+## Session picker
4-hosts configured, local sessions use `session@local` and remote sessions use
5-`session@host`, relative to the attaching client regardless of the serving
6-daemon. Offline hosts still enable qualified local labels. Local inventory
7-rows replace history rows for the same exact session;
8-homonyms with different origins or lifecycles remain distinct. A resume
9-credential belongs only to the route currently holding its attachment, so
10-older sessions in history use exact attachment after a same-daemon switch.
11 4
12-The command palette lists sessions from two origins: native results owned by5+Open it with `SSP` in the palette. It lists local sessions and, if you added hosts, remote sessions as `session@host`.
13-the serving daemon (active, stopped, remote, recent routes) and imported rows
14-relayed from the attaching client's own local daemon. Imported rows carry
15-only opaque keys plus display metadata; resolution happens through a
16-dial-only local control connection in the same interaction namespace.
17 6
18-## Inventory relay7+| Key | Action |
8+|---|---|
9+| ↑/↓ or `j`/`k` | move |
10+| Enter | open the session or tab |
11+| `/` | search (Escape leaves search) |
12+| `s` | switch sort: recent first, or named before numbered |
13+| `x` | kill the selected session |
14+| Escape or `q` | close and go back |
19 15
20-While attached to a remote daemon, the client runs one relay per attachment16+- Each row can expand to show its tabs, with a live preview of the selected one.
21-when it holds a committed local route. The relay dials only the local17+- Remote sessions that cannot be opened show a tag such as `down` or `broken`.
22-control socket: no Hello, no session creation, no attachment, no daemon18+- While the picker is open, your keys never reach the shell underneath.
23-startup.
24 19
25-- The serving palette sends an open demand with an interaction generation20+## Jumping between sessions
26- when it opens, and a close demand on every successful close (execute,
27- select, or cancel). Demands are capability-gated on both sides: the
28- client advertises inventory in Hello only while a committed local route
29- and the dial-only control source are available.
30-- While open, the relay polls the local source once per second with at most
31- one query in flight plus one coalesced latest update. Only changed
32- snapshots publish, with a monotonic publication generation per
33- interaction. Admitted keys and retirements never cross interactions.
34-- Enter on an imported row sends a selection with the opaque keys and the
35- admitted input cause, which stays zero for ordinary keyboard input.
36- Selection crosses before the close demand on the guarded serving
37- connection.
38-- Resolve revalidates the key against a fresh capture and returns a
39- non-mutating attach target. Endpoint-empty targets resolve against the
40- committed local route as authority; the serving route stays the recovery
41- destination through the inventory transition (settle once, restore on
42- every destination failure).
43-- Failures (stale identity, gone source, incompatible, invalid target,
44- navigation or restore failure) surface as palette feedback with the exact
45- cause while the originating interaction is still open; stale failures
46- from closed interactions drop. Native commands stay usable throughout.
47 21
48-## Client-owned picker22+| Code / key | Action |
23+|---|---|
24+| `BCK` | back to the previous session |
25+| `JRS <rank>` | jump to a recent session by rank |
26+| Alt+a | jump to a session that needs attention (bell) |
27+| `CNS` | create a named session; asks where when several daemons are available |
28+| `CES` | create a numbered session |
49 29
50-The navigation picker runs as a typed client interaction when the serving attachment advertises the client-picker capability: the palette command `SSP` (session picker) opens it, and a client with a home route answers that command with a home-picker navigation directive instead, so the picker stays daemon-rendered on the home route.30+Recent-session history belongs to one client and is not saved. A new client starts with an empty history.
51 31
52-The client renders the picker frame from the admitted snapshot rows, moves the cursor and filters with `/` locally, and sends only a typed selection (opaque row key plus displayed revision) or a close. The serving daemon re-resolves the key, revalidates the target lifecycle, and performs the unchanged navigation handoff; a selection must carry exactly the displayed revision, and a stale revision, unknown key, retired target, or failed navigation returns a typed failure instead of committing. Escape closes the interaction without selecting, `s` (sort) and `x` (kill) are unavailable in this mode, and the cursor/search state is local to the interaction.32+## Move panes and tabs
53 33
54-While the picker is open it owns terminal input on every path: physical keys and admitted automation batches reach the client picker and never the session, and the serving daemon drops raw key and mouse input for that attachment for the same window. After a close, the client keeps consuming and dropping input until the daemon's authoritative repaint is displayed, so nothing typed at the picker is replayed into the shell; routing resumes at that boundary. If the attachment generation changes before that repaint (a reconnect), the lease is aborted with the attachment and the new generation starts from normal routing.34+- `MFP` moves the focused pane to another tab.
35+- `MAT` moves the current tab to another session.
55 36
56-## Remote vision37+Both open a small picker to choose the destination.
57-
58-Remote-origin rows display availability tags (`down`, `broken`, stopped)
59-inline so they never look attachable. Remote vision is read-only: Enter on
60-a remote row reports feedback and emits no selection, and selections for
61-non-local sources reject before resolve. Non-OK groups project no rows.
62-
63-## Compatibility
64-
65-Demands gate both sides on the negotiated inventory capability: a serving
66-palette that never receives the bit never demands, and a client without a
67-committed local route or control dialer ignores demands that decode. With
68-no demands the relay stays idle with zero control dials, and local
69-attachments ignore demands even when they decode. Unknown frame types
70-stay ignored by the receive pump. Cross-version attachment still fails at
71-the strict-equality handshake; graceful degradation applies only within
72-one negotiated version.
M docs/terminal.md +34 -70
@@ -1,102 +1,66 @@
11 # Terminal support
22
3-## Color
3+## Colors
44
5-Panes receive `TERM=xterm-direct` when the pane host can look up that terminfo entry, otherwise `TERM=xterm-256color`. Both profiles receive `COLORTERM=truecolor` and `TERM_PROGRAM=vev`. The profile describes vev's virtual terminal, not the terminal attached outside vev.
5+Panes get truecolor when the pane host has the `xterm-direct` terminfo entry.
66
7-Before each PTY launch, vev runs `infocmp -x xterm-direct` on the daemon's host with the child's environment and working directory. This honors terminfo search settings such as `TERMINFO`, `TERMINFO_DIRS`, and `HOME`. The helper is resolved through the daemon's `PATH`. A missing helper, failed lookup, or one-second probe timeout selects the 256-color compatibility profile. Existing panes keep their original environment; newly opened or restored panes check again.
7+| Pane host has `xterm-direct`? | `TERM` in panes |
8+|---|---|
9+| yes | `xterm-direct` |
10+| no | `xterm-256color` |
811
9-`xterm-direct` advertises 16,777,216 colors through terminfo and uses direct RGB color setters. With `xterm-256color`, applications need to recognize `COLORTERM=truecolor` or otherwise enable RGB output; `tput colors` reports only the entry's 256 indexed colors.
12+Both set `COLORTERM=truecolor` and `TERM_PROGRAM=vev`.
1013
11-Each attachment independently detects its output color support from terminal environment signals. vev preserves RGB pane state and converts it to the xterm 256-color palette only for attachments that do not advertise truecolor. An attachment reporting a 256-color terminal receives a one-time warning before its first paint. Environment signals are advisory—especially through nested multiplexers—and do not advertise Kitty graphics support.
14+- vev checks with `infocmp -x xterm-direct` before each new pane (1 second timeout). Existing panes keep their `TERM`.
15+- With `xterm-256color`, apps must read `COLORTERM` to use RGB colors.
16+- If your outer terminal does not support truecolor, vev converts colors to 256 and shows a one-time warning.
1217
13-### Installing the direct-color entry
18+### Install `xterm-direct`
1419
15-Check availability on the machine where the pane runs, including each remote vev host:
20+Check on each machine that runs panes, including remote vev hosts:
1621
1722 ```sh
18-infocmp -x xterm-direct
19-tput -T xterm-direct colors
20-# Expected color count: 16777216
23+tput -T xterm-direct colors # expect 16777216
2124 ```
2225
23-On Arch Linux and CachyOS, the `ncurses` package supplies the entry and the `infocmp`, `tput`, and `tic` tools:
26+On Arch Linux: `sudo pacman -S ncurses`. On other distributions, install the ncurses terminfo package.
2427
25-```sh
26-sudo pacman -S ncurses
27-```
28-
29-Other distributions may package extended terminfo entries separately. Install their terminfo database and ncurses utilities, then repeat the checks above.
30-
31-For a user-local installation, export the entry on a machine that has it:
32-
33-```sh
34-infocmp -x xterm-direct > xterm-direct.terminfo
35-```
36-
37-Copy the file to the destination and compile it there:
28+Without root, copy the entry from a machine that has it:
3829
3930 ```sh
40-tic -x -o ~/.terminfo xterm-direct.terminfo
31+infocmp -x xterm-direct > xterm-direct.terminfo # on the source machine
32+tic -x -o ~/.terminfo xterm-direct.terminfo # on the target machine
4133 ```
4234
43-Open a new pane to use the installed entry. The daemon also needs `infocmp` available in its `PATH`; restart the daemon if that search path needs to change. SSH destinations and containers entered from a pane need their own entry if they inherit `TERM=xterm-direct`. Installation on the pane host does not install it inside those environments.
35+Then open a new pane. SSH hosts and containers entered from a pane need the entry too.
4436
4537 ## Kitty graphics
4638
47-Kitty graphics support is attachment-local and explicitly declared only after a
48-bounded Kitty query and DA1 probe run through the client's existing terminal
49-input pump. Environment variables never enable graphics by themselves. The
50-probe preserves unrelated input, and late probe replies are quarantined rather
51-than sent to a pane.
52-
53-A capable attachment renders the supported static/direct subset: current
54-streamed `kitten icat` PNG input, JPEG input after `kitten icat` converts it to
55-RGB/RGBA, bounded RGB/RGBA/PNG assets, ordinary placements, crop, offsets,
56-z-index, and supported deletes. vev allocates attachment-owned Kitty IDs, never
57-sends global deletes, clips placements to pane and attachment content, and
58-replays authoritative graphics through the same ordered output state chain as
59-ANSI text. Unsupported attachments retain text output and receive one bounded
60-warning when graphics are suppressed.
61-
62-File, temporary-file, shared-memory, composition, relative placements, Unicode
63-placeholders, and graphics scrollback are unsupported. Animated images retain
64-their first frame as a static fallback.
39+Images work when your outer terminal supports the Kitty graphics protocol. vev detects this with a short query at attach time; environment variables never enable it.
6540
66-## Environment
41+Supported:
6742
68-Local and direct CLI remote attachments supply the session's environment for
69-future PTY children. A later client-owned attachment updates that shared
70-environment; already-running processes keep their original environment. vev
71-replaces `TERM`, `COLORTERM`, `TERM_PROGRAM`, and `VEV`; `SHELL` selects the
72-command used to start each PTY.
43+- `kitten icat` with PNG and JPEG images.
44+- Placement, cropping, offsets, z-index, and deletes.
7345
74-Picker-selected remote attachments do not replace the session's environment
75-with the client's exports. When restarting a stopped remote session, vev uses
76-the destination daemon's environment and the persisted working directory. A
77-`CNS` handoff to another remote daemon also uses that daemon's environment,
78-starting in its home directory rather than the client's working directory. This
79-keeps `HOME`, `SHELL`, `PATH`, and XDG paths local to the destination host.
46+Not supported: file or shared-memory transfer, animations (first frame only), Unicode placeholders, and images in scrollback.
8047
81-## VT compatibility
48+Terminals without Kitty graphics get text only and one warning.
8249
83-vev keeps a server-side VT screen per pane: scroll regions (DECOM), insert mode, cursor and mode-query reports, alternate screen, bracketed paste, mouse tracking, synchronized updates, OSC 9/777 notifications, OSC 52 clipboard, and double-width cells. Resizes repair double-width cells so CJK and emoji halves are never left behind.
50+## Environment
8451
85-## Session content size
52+- New panes inherit the environment of the client that last attached. Running processes keep theirs.
53+- vev always sets `TERM`, `COLORTERM`, `TERM_PROGRAM`, and `VEV`. `SHELL` picks the shell.
54+- A remote session opened from the picker uses the remote host's own environment, not yours. This keeps `HOME`, `PATH`, and `SHELL` correct on that host.
8655
87-The session's PTY content size follows the latest valid attach, resume, or
88-resize claim. Invalid or stale claims do not change shared PTY geometry. Each
89-attachment may have a different outer window and viewport; the winning claim
90-changes shared PTYs while preserving the other attachments' own presentations.
91-If the winning attachment detaches, the most recently claimed remaining valid
92-attachment becomes authoritative.
56+## VT features
9357
94-## Recovery
58+Each pane has a server-side VT screen with scroll regions, alternate screen, bracketed paste, mouse tracking, synchronized updates, OSC 9/777 notifications, OSC 52 clipboard, and wide (CJK and emoji) characters.
9559
96-Named-session checkpoints preserve bounded scrollback and a recovery transcript containing the primary viewport followed by the active alternate viewport, when present. Each viewport trims only trailing untouched blank rows; leading, internal, styled, and otherwise touched blank rows remain.
60+## Pane size with several clients
9761
98-On restore, vev appends the transcript after bounded scrollback as copy-mode history on a fresh blank VT screen. A resumed allowlisted process can redraw the live screen without erasing that recovered history. Recovery does not replay the PTY stream or restore the previous cursor or terminal modes.
62+When several clients attach to one session, pane size follows the client that attached or resized last. Each client keeps its own window, tab, and focus. When that client leaves, the previous one takes over.
9963
100-## Alt+Space caveat
64+## Alt+Space
10165
102-Some terminals encode Alt+Space as a non-breaking space instead of `ESC Space`. If the palette does not open, rebind `open-palette`.
66+Some terminals send a non-breaking space for Alt+Space. If the palette does not open, rebind `open-palette` in the [configuration](configuration.md).
@@ -1,102 +1,66 @@
1 # Terminal support1 # Terminal support
2 2
3-## Color3+## Colors
4 4
5-Panes receive `TERM=xterm-direct` when the pane host can look up that terminfo entry, otherwise `TERM=xterm-256color`. Both profiles receive `COLORTERM=truecolor` and `TERM_PROGRAM=vev`. The profile describes vev's virtual terminal, not the terminal attached outside vev.5+Panes get truecolor when the pane host has the `xterm-direct` terminfo entry.
6 6
7-Before each PTY launch, vev runs `infocmp -x xterm-direct` on the daemon's host with the child's environment and working directory. This honors terminfo search settings such as `TERMINFO`, `TERMINFO_DIRS`, and `HOME`. The helper is resolved through the daemon's `PATH`. A missing helper, failed lookup, or one-second probe timeout selects the 256-color compatibility profile. Existing panes keep their original environment; newly opened or restored panes check again.7+| Pane host has `xterm-direct`? | `TERM` in panes |
8+|---|---|
9+| yes | `xterm-direct` |
10+| no | `xterm-256color` |
8 11
9-`xterm-direct` advertises 16,777,216 colors through terminfo and uses direct RGB color setters. With `xterm-256color`, applications need to recognize `COLORTERM=truecolor` or otherwise enable RGB output; `tput colors` reports only the entry's 256 indexed colors.12+Both set `COLORTERM=truecolor` and `TERM_PROGRAM=vev`.
10 13
11-Each attachment independently detects its output color support from terminal environment signals. vev preserves RGB pane state and converts it to the xterm 256-color palette only for attachments that do not advertise truecolor. An attachment reporting a 256-color terminal receives a one-time warning before its first paint. Environment signals are advisory—especially through nested multiplexers—and do not advertise Kitty graphics support.14+- vev checks with `infocmp -x xterm-direct` before each new pane (1 second timeout). Existing panes keep their `TERM`.
15+- With `xterm-256color`, apps must read `COLORTERM` to use RGB colors.
16+- If your outer terminal does not support truecolor, vev converts colors to 256 and shows a one-time warning.
12 17
13-### Installing the direct-color entry18+### Install `xterm-direct`
14 19
15-Check availability on the machine where the pane runs, including each remote vev host:20+Check on each machine that runs panes, including remote vev hosts:
16 21
17 ```sh22 ```sh
18-infocmp -x xterm-direct23+tput -T xterm-direct colors # expect 16777216
19-tput -T xterm-direct colors
20-# Expected color count: 16777216
21 ```24 ```
22 25
23-On Arch Linux and CachyOS, the `ncurses` package supplies the entry and the `infocmp`, `tput`, and `tic` tools:26+On Arch Linux: `sudo pacman -S ncurses`. On other distributions, install the ncurses terminfo package.
24 27
25-```sh28+Without root, copy the entry from a machine that has it:
26-sudo pacman -S ncurses
27-```
28-
29-Other distributions may package extended terminfo entries separately. Install their terminfo database and ncurses utilities, then repeat the checks above.
30-
31-For a user-local installation, export the entry on a machine that has it:
32-
33-```sh
34-infocmp -x xterm-direct > xterm-direct.terminfo
35-```
36-
37-Copy the file to the destination and compile it there:
38 29
39 ```sh30 ```sh
40-tic -x -o ~/.terminfo xterm-direct.terminfo31+infocmp -x xterm-direct > xterm-direct.terminfo # on the source machine
32+tic -x -o ~/.terminfo xterm-direct.terminfo # on the target machine
41 ```33 ```
42 34
43-Open a new pane to use the installed entry. The daemon also needs `infocmp` available in its `PATH`; restart the daemon if that search path needs to change. SSH destinations and containers entered from a pane need their own entry if they inherit `TERM=xterm-direct`. Installation on the pane host does not install it inside those environments.35+Then open a new pane. SSH hosts and containers entered from a pane need the entry too.
44 36
45 ## Kitty graphics37 ## Kitty graphics
46 38
47-Kitty graphics support is attachment-local and explicitly declared only after a39+Images work when your outer terminal supports the Kitty graphics protocol. vev detects this with a short query at attach time; environment variables never enable it.
48-bounded Kitty query and DA1 probe run through the client's existing terminal
49-input pump. Environment variables never enable graphics by themselves. The
50-probe preserves unrelated input, and late probe replies are quarantined rather
51-than sent to a pane.
52-
53-A capable attachment renders the supported static/direct subset: current
54-streamed `kitten icat` PNG input, JPEG input after `kitten icat` converts it to
55-RGB/RGBA, bounded RGB/RGBA/PNG assets, ordinary placements, crop, offsets,
56-z-index, and supported deletes. vev allocates attachment-owned Kitty IDs, never
57-sends global deletes, clips placements to pane and attachment content, and
58-replays authoritative graphics through the same ordered output state chain as
59-ANSI text. Unsupported attachments retain text output and receive one bounded
60-warning when graphics are suppressed.
61-
62-File, temporary-file, shared-memory, composition, relative placements, Unicode
63-placeholders, and graphics scrollback are unsupported. Animated images retain
64-their first frame as a static fallback.
65 40
66-## Environment41+Supported:
67 42
68-Local and direct CLI remote attachments supply the session's environment for43+- `kitten icat` with PNG and JPEG images.
69-future PTY children. A later client-owned attachment updates that shared44+- Placement, cropping, offsets, z-index, and deletes.
70-environment; already-running processes keep their original environment. vev
71-replaces `TERM`, `COLORTERM`, `TERM_PROGRAM`, and `VEV`; `SHELL` selects the
72-command used to start each PTY.
73 45
74-Picker-selected remote attachments do not replace the session's environment46+Not supported: file or shared-memory transfer, animations (first frame only), Unicode placeholders, and images in scrollback.
75-with the client's exports. When restarting a stopped remote session, vev uses
76-the destination daemon's environment and the persisted working directory. A
77-`CNS` handoff to another remote daemon also uses that daemon's environment,
78-starting in its home directory rather than the client's working directory. This
79-keeps `HOME`, `SHELL`, `PATH`, and XDG paths local to the destination host.
80 47
81-## VT compatibility48+Terminals without Kitty graphics get text only and one warning.
82 49
83-vev keeps a server-side VT screen per pane: scroll regions (DECOM), insert mode, cursor and mode-query reports, alternate screen, bracketed paste, mouse tracking, synchronized updates, OSC 9/777 notifications, OSC 52 clipboard, and double-width cells. Resizes repair double-width cells so CJK and emoji halves are never left behind.50+## Environment
84 51
85-## Session content size52+- New panes inherit the environment of the client that last attached. Running processes keep theirs.
53+- vev always sets `TERM`, `COLORTERM`, `TERM_PROGRAM`, and `VEV`. `SHELL` picks the shell.
54+- A remote session opened from the picker uses the remote host's own environment, not yours. This keeps `HOME`, `PATH`, and `SHELL` correct on that host.
86 55
87-The session's PTY content size follows the latest valid attach, resume, or56+## VT features
88-resize claim. Invalid or stale claims do not change shared PTY geometry. Each
89-attachment may have a different outer window and viewport; the winning claim
90-changes shared PTYs while preserving the other attachments' own presentations.
91-If the winning attachment detaches, the most recently claimed remaining valid
92-attachment becomes authoritative.
93 57
94-## Recovery58+Each pane has a server-side VT screen with scroll regions, alternate screen, bracketed paste, mouse tracking, synchronized updates, OSC 9/777 notifications, OSC 52 clipboard, and wide (CJK and emoji) characters.
95 59
96-Named-session checkpoints preserve bounded scrollback and a recovery transcript containing the primary viewport followed by the active alternate viewport, when present. Each viewport trims only trailing untouched blank rows; leading, internal, styled, and otherwise touched blank rows remain.60+## Pane size with several clients
97 61
98-On restore, vev appends the transcript after bounded scrollback as copy-mode history on a fresh blank VT screen. A resumed allowlisted process can redraw the live screen without erasing that recovered history. Recovery does not replay the PTY stream or restore the previous cursor or terminal modes.62+When several clients attach to one session, pane size follows the client that attached or resized last. Each client keeps its own window, tab, and focus. When that client leaves, the previous one takes over.
99 63
100-## Alt+Space caveat64+## Alt+Space
101 65
102-Some terminals encode Alt+Space as a non-breaking space instead of `ESC Space`. If the palette does not open, rebind `open-palette`.66+Some terminals send a non-breaking space for Alt+Space. If the palette does not open, rebind `open-palette` in the [configuration](configuration.md).
M docs/ui-driver.md +59 -120
@@ -1,159 +1,98 @@
11 # UI driver
22
3-`vev --ui-driver` exposes one opt-in attachment as a bounded JSON Lines stream. It is intentionally a hidden headless entry point for tests and local automation that need the rendered terminal, not a second terminal implementation.
3+`vev --ui-driver` lets a script or test read the rendered screen and send keys, over JSON Lines on stdin/stdout. It uses the normal vev client, so what you capture is what a user sees.
44
5-## Headless attachment
6-
7-Start a new numbered ephemeral session with the default 80x24 viewport:
5+## Start
86
97 ```sh
10-vev --ui-driver
11-```
12-
13-`--ui-driver` uses the per-user broker of the process' own XDG context. It never
14-provisions, reconfigures, or destroys a daemon, a session-scoped root, or an
15-endpoint: creation is expressed as one initial navigation through the broker, so
16-two drivers can share one per-user broker without either owning it.
17-
18-The closed option set is:
19-
20-- `--session NAME` creates that named local session instead of an ephemeral one.
21-- `--remote ENDPOINT` targets that configured remote endpoint: with `--session`
22-it resolves that exact remote session, without it it creates a remote ephemeral
23-session. Every remote identity comes from the broker's committed publication,
24-never from a hostname or an environment value.
25-- `--cols N --rows N` set the viewport, at most 512x256.
26-- `--picker` starts with no initial navigation at all and is exclusive of
27-`--session` and `--remote`.
28-- `--socket ABS` is the JSONL bridge to an already running opted-in client, and
29-is exclusive of every headless option.
30-
31-`--launch-config` no longer exists. It is an unknown option and is refused with
32-the usage exit code before any terminal, broker, or filesystem interaction; a
33-driver that needs an isolated endpoint provisions it through the broker's own
34-configuration instead.
35-
36-The first line on stdout is the discovery response. It is emitted as soon as the
37-client has published its initial state, which does not require a broker or a
38-daemon:
39-
40-```json
41-{"version":1,"id":0,"result":{"attachment":"<opaque-handle>","generation":0,"control":true,"status":"picker"}}
8+vev --ui-driver # new numbered session, 80x24
9+vev --ui-driver --session work # named local session
10+vev --ui-driver --remote user@host # remote session on a configured host
11+vev --ui-driver --picker # start in the session picker
4212 ```
4313
44-`status` is a closed union of exactly three presentations:
45-
46-| `status` | meaning | `generation` | session metadata in `capture`/`wait` | `keys`/`text` |
47-|---|---|---:|---|---|
48-| `picker` | the local picker; ready without a broker and without a daemon | `0` | all zero | refused (`unavailable`) |
49-| `connecting` | an attachment attempt that has not committed its first frame | `0` | all zero | refused (`unavailable`) |
50-| `attached` | a committed attachment | nonzero real action generation | the validated session identity and committed boundary | admitted under the current binding |
51-
52-The former `detached`, `reconnecting`, and `transitioning` states no longer
53-exist and are not aliases of these three: a reconnect is `connecting`, a detach
54-is `picker`, and terminating closes the service instead of becoming a fourth
55-persistent presentation. A driver that never publishes anything is a real local
56-error; a destination that is refused, times out, or is lost leaves a published
57-`picker` and the same JSONL service running.
58-
59-The `attachment` handle is the UI service of this run, never a session identity,
60-and it is stable for the whole run. `generation` is zero until a committed
61-attachment publishes one; a client must wait for `status` to become `attached`
62-and use the generation that publication reports, and never assume
63-`generation == 1`. A broker that is absent or incompatible is not fatal: the
64-driver keeps answering `capture` and `wait`. A run that requested a session
65-stays `connecting` while it retries and never shows the picker first; a
66-`--picker` run, a refused destination, or a failure retrying cannot fix
67-presents `picker` with a bounded notice.
68-
69-The stream then accepts one JSON object per line. Every request has `version`, a nonzero `id`, `op`, and the discovered `attachment`:
14+| Option | Meaning |
15+|---|---|
16+| `--session NAME` | create that named session (with `--remote`: open that remote session) |
17+| `--remote HOST` | target a configured remote host |
18+| `--cols N --rows N` | viewport size, max 512×256 |
19+| `--picker` | start in the picker (not with `--session`/`--remote`) |
20+| `--socket PATH` | connect to an existing client instead (see below) |
7021
71-```json
72-{"version":1,"id":1,"op":"capture","attachment":"<opaque-handle>","format":"both"}
73-```
22+The driver uses your normal broker and daemon. It never starts, stops, or reconfigures them. Closing stdin detaches; sessions keep running.
7423
75-`format` is `text` (the default), `cells`, or `both`. Text contains exactly one newline-separated string per row, including trailing blank rows. Cells are row-major and contain `text`, `width`, `continuation`, and the rendered style. Captures also contain the revision, coherent route/session/focus context, geometry, and cursor.
24+## Protocol
7625
77-Input targets the exact current `generation`:
26+The first line is the discovery response:
7827
7928 ```json
80-{"version":1,"id":2,"op":"keys","attachment":"<opaque-handle>","generation":1,"keys":["Alt+Space"]}
81-{"version":1,"id":3,"op":"keys","attachment":"<opaque-handle>","generation":1,"keys":["Escape"]}
82-{"version":1,"id":4,"op":"text","attachment":"<opaque-handle>","generation":1,"text":"printf 'driver ok'"}
83-{"version":1,"id":5,"op":"keys","attachment":"<opaque-handle>","generation":1,"keys":["Enter"]}
84-{"version":1,"id":6,"op":"wait","attachment":"<opaque-handle>","after_action":5,"expect":{"text_contains":"driver ok"}}
29+{"version":1,"id":0,"result":{"attachment":"<handle>","generation":0,"control":true,"status":"picker"}}
8530 ```
8631
87-`keys` accepts only the documented finite grammar: `Enter`, `Escape`, `Tab`, `Backspace`, `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`, `PageDown`, `Space`, one printable ASCII character, `Ctrl+` with the supported ASCII control set, and `Alt+` followed by one supported unmodified ASCII character. Cursor navigation follows the terminal's application-cursor mode. Raw escape notation, keypad keys, combined modifiers, and unknown names are rejected.
88-
89-`text` is literal printable UTF-8. It does not mean paste and cannot contain C0/C1 control characters; use an explicit key for Enter, Tab, or Escape.
32+`status` is one of:
9033
91-## Completion and waits
34+| Status | Meaning | Keys accepted? |
35+|---|---|---|
36+| `picker` | in the local picker | no |
37+| `connecting` | attaching | no |
38+| `attached` | attached to a session | yes |
9239
93-An accepted action is sent through the normal client input owner. A `processed` action means that its input batch reached the normal dispatch boundary and its required render/publication boundary. It does not mean that a child process has echoed the input or completed a command. A local palette action uses the client event-loop boundary instead of a fabricated daemon fence.
40+Wait for `attached`, then use the `generation` it reports for input. Never assume `generation == 1`.
9441
95-Navigation actions remain pending until the existing handoff reaches a validated, written, and published full destination view. If the handoff or transport cannot be attributed safely, the action reports `outcome_unknown`; it is never replayed into a new generation. A failed route reports `navigation_failed`.
42+- A run that asked for a session stays `connecting` while it retries, even if the broker is missing. It never shows the picker first.
43+- `picker` appears for `--picker` runs, a refused destination, or a failure that retrying cannot fix, with a short notice.
44+- `capture` and `wait` keep working in every state.
9645
97-A request timeout only ends that request. If input was already accepted, the response has `accepted:true` and an `action_id`; the input is not rolled back. Use `wait` with `after_action` to retrieve the later result. An evicted action reports `action_expired`.
98-
99-`wait` requires at least one predicate. All supplied predicates are conjunctive:
46+Every request has `version`, a nonzero `id`, `op`, and `attachment`:
10047
10148 ```json
102-{"version":1,"id":6,"op":"wait","attachment":"<opaque-handle>","after_action":4,"timeout_ms":5000,"expect":{"text_contains":"driver ok","status":"attached"}}
49+{"version":1,"id":1,"op":"capture","attachment":"<handle>","format":"both"}
50+{"version":1,"id":2,"op":"text","attachment":"<handle>","generation":1,"text":"printf 'driver ok'"}
51+{"version":1,"id":3,"op":"keys","attachment":"<handle>","generation":1,"keys":["Enter"]}
52+{"version":1,"id":4,"op":"wait","attachment":"<handle>","after_action":3,"expect":{"text_contains":"driver ok"}}
10353 ```
10454
105-Supported predicates are literal `text_contains`, an exact `session` (`lifecycle_id` and `session_name`), an exact `focus` (`tab_id` and `pane_id`), and `status` (`picker`, `connecting`, or `attached`). Without `after_action`, a wait may match the current published snapshot. With it, only the action's confirmed publication boundary and later snapshots are eligible. This is a postcondition check, not a claim that the action alone caused the text.
106-
107-## Limits and errors
55+### Operations
10856
109-- Requests are at most 64 KiB. A complete final JSON object without a newline is accepted; truncated JSON is not.
110-- Input is at most 16 KiB and a `keys` request has 1–256 tokens.
111-- Headless viewports are at most 512 columns by 256 rows.
112-- The default and maximum request timeout are 5 seconds and 30 seconds.
113-- Each attachment allows at most four socket connections, one admitted action across all controllers, four active waits, and 64 retained action records.
114-- Each connection has one response being written and at most two queued responses. A slow reader is closed after the bounded write deadline or queue overrun.
115-- Serialized response memory is bounded to 128 MiB per attachment. An oversized capture returns `capture_too_large`; it never resizes an interactive terminal.
57+- **`capture`**: `format` is `text` (default), `cells`, or `both`. Returns one string per row, plus cursor, geometry, and focus.
58+- **`text`**: literal printable UTF-8. No control characters; use `keys` for Enter or Tab.
59+- **`keys`**: `Enter`, `Escape`, `Tab`, `Backspace`, `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`, `PageDown`, `Space`, one printable ASCII character, `Ctrl+<char>`, or `Alt+<char>`.
60+- **`wait`**: waits until all conditions match. Conditions: `text_contains`, `session`, `focus`, `status`. With `after_action`, only screens after that action count.
11661
117-Errors are structured and sanitized. Stable codes include `invalid_request`, `unsupported_version`, `permission_denied`, `stale_attachment`, `unavailable`, `timeout`, `outcome_unknown`, `capture_too_large`, `action_expired`, `input_busy`, `busy`, `navigation_failed`, and `endpoint_not_configured`. Error messages contain only the code; screen contents, credentials, endpoint internals, and environment values are not logged or returned.
62+### Completion
11863
119-EOF detaches this driver attachment and closes its access stream. It does not kill shells or sessions, does not stop the shared broker, and does not remove any root: the broker, its daemon, and every session stay alive for the other clients, and the pool's own idle shutdown remains the only thing that ends the daemon.
64+- An action is `processed` when vev has handled and rendered it. That does not mean the shell has finished running the command: use `wait` for that.
65+- If a request times out after the input was accepted, the reply has `accepted:true` and an `action_id`. The input is not undone. Use `wait` with `after_action` to follow up.
12066
121-## Isolation
67+## Limits
12268
123-The driver owns no endpoint root, no daemon, and no process. An isolated endpoint
124-is provisioned through the broker's own configuration before the driver starts,
125-exactly like any other broker route: the broker is the single authority over
126-binary, root, environment, trust, and launch policy, and those values are never a
127-payload of a JSONL request, a selected hostname, or a client environment variable.
128-There is no per-invocation root creation, no owner token, and no remote cleanup
129-step at driver exit.
69+| Limit | Value |
70+|---|---|
71+| Request size | 64 KiB |
72+| Input per request | 16 KiB, 1–256 keys |
73+| Request timeout | 5 s default, 30 s max |
74+| Connections per attachment | 4 |
75+| Active waits | 4 |
13076
131-Directory isolation is not a process, filesystem, or network sandbox; environment
132-files and binaries remain sensitive.
77+Error codes include `invalid_request`, `stale_attachment`, `unavailable`, `timeout`, `outcome_unknown`, `capture_too_large`, `input_busy`, and `navigation_failed`. Errors never contain screen content or credentials.
13378
134-## Existing interactive clients
79+## Observe an interactive client
13580
136-Observation and control are disabled by default. Enable them before an attach command:
81+Observation is off by default. Enable it when you attach:
13782
13883 ```sh
139-vev --ui-observe attach work
140-vev --ui-control new work
84+vev --ui-observe attach work # read-only
85+vev --ui-control new work # read and send input
14186 ```
14287
143-`--ui-control` implies observation. The client prints one socket path to stderr, or uses the supplied private path:
144-
145-```sh
146-vev --ui-observe --ui-socket /absolute/private/ui.sock attach work
147-```
148-
149-The socket parent is private (`0700`) and the socket is private (`0600`). On Linux, the server also requires the connecting peer's UID to match the client UID. A path collision is rejected and never unlinks an existing file. Read-only observation rejects `keys` and `text` before they reach the input owner.
150-
151-The bridge form connects JSONL stdio to an already-running opted-in client; it does not create another attachment:
88+The client prints a private socket path to stderr (or use `--ui-socket /abs/path`). Connect to it:
15289
15390 ```sh
154-vev --ui-driver --socket /absolute/private/ui.sock
91+vev --ui-driver --socket /abs/path/ui.sock
15592 ```
15693
157-The physical terminal remains authoritative for geometry and terminal query responses. Mirroring observes bytes at the serialized terminal writer and publishes only after a successful physical flush. Capture and wait never write to the terminal, request a redraw, change focus, claim PTY geometry, send Resize, or acknowledge protocol output independently. If mirroring fails, capture becomes unavailable while the interactive client continues running. Disconnecting an observer leaves the interactive attachment and its shells running.
94+- The socket is private (`0600`), and on Linux only your user can connect.
95+- `--ui-observe` rejects `keys` and `text`.
96+- Observers never resize, redraw, or slow down the real terminal. Disconnecting leaves the client running.
15897
159-The driver intentionally does not provide image capture, mouse/paste/resize commands, subscriptions, scenarios, replay, automatic artifact export, public TCP access, or sandbox management.
98+Not supported: image capture, mouse, paste, resize commands, and TCP access.
@@ -1,159 +1,98 @@
1 # UI driver1 # UI driver
2 2
3-`vev --ui-driver` exposes one opt-in attachment as a bounded JSON Lines stream. It is intentionally a hidden headless entry point for tests and local automation that need the rendered terminal, not a second terminal implementation.3+`vev --ui-driver` lets a script or test read the rendered screen and send keys, over JSON Lines on stdin/stdout. It uses the normal vev client, so what you capture is what a user sees.
4 4
5-## Headless attachment5+## Start
6-
7-Start a new numbered ephemeral session with the default 80x24 viewport:
8 6
9 ```sh7 ```sh
10-vev --ui-driver8+vev --ui-driver # new numbered session, 80x24
11-```9+vev --ui-driver --session work # named local session
12-10+vev --ui-driver --remote user@host # remote session on a configured host
13-`--ui-driver` uses the per-user broker of the process' own XDG context. It never11+vev --ui-driver --picker # start in the session picker
14-provisions, reconfigures, or destroys a daemon, a session-scoped root, or an
15-endpoint: creation is expressed as one initial navigation through the broker, so
16-two drivers can share one per-user broker without either owning it.
17-
18-The closed option set is:
19-
20-- `--session NAME` creates that named local session instead of an ephemeral one.
21-- `--remote ENDPOINT` targets that configured remote endpoint: with `--session`
22-it resolves that exact remote session, without it it creates a remote ephemeral
23-session. Every remote identity comes from the broker's committed publication,
24-never from a hostname or an environment value.
25-- `--cols N --rows N` set the viewport, at most 512x256.
26-- `--picker` starts with no initial navigation at all and is exclusive of
27-`--session` and `--remote`.
28-- `--socket ABS` is the JSONL bridge to an already running opted-in client, and
29-is exclusive of every headless option.
30-
31-`--launch-config` no longer exists. It is an unknown option and is refused with
32-the usage exit code before any terminal, broker, or filesystem interaction; a
33-driver that needs an isolated endpoint provisions it through the broker's own
34-configuration instead.
35-
36-The first line on stdout is the discovery response. It is emitted as soon as the
37-client has published its initial state, which does not require a broker or a
38-daemon:
39-
40-```json
41-{"version":1,"id":0,"result":{"attachment":"<opaque-handle>","generation":0,"control":true,"status":"picker"}}
42 ```12 ```
43 13
44-`status` is a closed union of exactly three presentations:14+| Option | Meaning |
45-15+|---|---|
46-| `status` | meaning | `generation` | session metadata in `capture`/`wait` | `keys`/`text` |16+| `--session NAME` | create that named session (with `--remote`: open that remote session) |
47-|---|---|---:|---|---|17+| `--remote HOST` | target a configured remote host |
48-| `picker` | the local picker; ready without a broker and without a daemon | `0` | all zero | refused (`unavailable`) |18+| `--cols N --rows N` | viewport size, max 512×256 |
49-| `connecting` | an attachment attempt that has not committed its first frame | `0` | all zero | refused (`unavailable`) |19+| `--picker` | start in the picker (not with `--session`/`--remote`) |
50-| `attached` | a committed attachment | nonzero real action generation | the validated session identity and committed boundary | admitted under the current binding |20+| `--socket PATH` | connect to an existing client instead (see below) |
51-
52-The former `detached`, `reconnecting`, and `transitioning` states no longer
53-exist and are not aliases of these three: a reconnect is `connecting`, a detach
54-is `picker`, and terminating closes the service instead of becoming a fourth
55-persistent presentation. A driver that never publishes anything is a real local
56-error; a destination that is refused, times out, or is lost leaves a published
57-`picker` and the same JSONL service running.
58-
59-The `attachment` handle is the UI service of this run, never a session identity,
60-and it is stable for the whole run. `generation` is zero until a committed
61-attachment publishes one; a client must wait for `status` to become `attached`
62-and use the generation that publication reports, and never assume
63-`generation == 1`. A broker that is absent or incompatible is not fatal: the
64-driver keeps answering `capture` and `wait`. A run that requested a session
65-stays `connecting` while it retries and never shows the picker first; a
66-`--picker` run, a refused destination, or a failure retrying cannot fix
67-presents `picker` with a bounded notice.
68-
69-The stream then accepts one JSON object per line. Every request has `version`, a nonzero `id`, `op`, and the discovered `attachment`:
70 21
71-```json22+The driver uses your normal broker and daemon. It never starts, stops, or reconfigures them. Closing stdin detaches; sessions keep running.
72-{"version":1,"id":1,"op":"capture","attachment":"<opaque-handle>","format":"both"}
73-```
74 23
75-`format` is `text` (the default), `cells`, or `both`. Text contains exactly one newline-separated string per row, including trailing blank rows. Cells are row-major and contain `text`, `width`, `continuation`, and the rendered style. Captures also contain the revision, coherent route/session/focus context, geometry, and cursor.24+## Protocol
76 25
77-Input targets the exact current `generation`:26+The first line is the discovery response:
78 27
79 ```json28 ```json
80-{"version":1,"id":2,"op":"keys","attachment":"<opaque-handle>","generation":1,"keys":["Alt+Space"]}29+{"version":1,"id":0,"result":{"attachment":"<handle>","generation":0,"control":true,"status":"picker"}}
81-{"version":1,"id":3,"op":"keys","attachment":"<opaque-handle>","generation":1,"keys":["Escape"]}
82-{"version":1,"id":4,"op":"text","attachment":"<opaque-handle>","generation":1,"text":"printf 'driver ok'"}
83-{"version":1,"id":5,"op":"keys","attachment":"<opaque-handle>","generation":1,"keys":["Enter"]}
84-{"version":1,"id":6,"op":"wait","attachment":"<opaque-handle>","after_action":5,"expect":{"text_contains":"driver ok"}}
85 ```30 ```
86 31
87-`keys` accepts only the documented finite grammar: `Enter`, `Escape`, `Tab`, `Backspace`, `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`, `PageDown`, `Space`, one printable ASCII character, `Ctrl+` with the supported ASCII control set, and `Alt+` followed by one supported unmodified ASCII character. Cursor navigation follows the terminal's application-cursor mode. Raw escape notation, keypad keys, combined modifiers, and unknown names are rejected.32+`status` is one of:
88-
89-`text` is literal printable UTF-8. It does not mean paste and cannot contain C0/C1 control characters; use an explicit key for Enter, Tab, or Escape.
90 33
91-## Completion and waits34+| Status | Meaning | Keys accepted? |
35+|---|---|---|
36+| `picker` | in the local picker | no |
37+| `connecting` | attaching | no |
38+| `attached` | attached to a session | yes |
92 39
93-An accepted action is sent through the normal client input owner. A `processed` action means that its input batch reached the normal dispatch boundary and its required render/publication boundary. It does not mean that a child process has echoed the input or completed a command. A local palette action uses the client event-loop boundary instead of a fabricated daemon fence.40+Wait for `attached`, then use the `generation` it reports for input. Never assume `generation == 1`.
94 41
95-Navigation actions remain pending until the existing handoff reaches a validated, written, and published full destination view. If the handoff or transport cannot be attributed safely, the action reports `outcome_unknown`; it is never replayed into a new generation. A failed route reports `navigation_failed`.42+- A run that asked for a session stays `connecting` while it retries, even if the broker is missing. It never shows the picker first.
43+- `picker` appears for `--picker` runs, a refused destination, or a failure that retrying cannot fix, with a short notice.
44+- `capture` and `wait` keep working in every state.
96 45
97-A request timeout only ends that request. If input was already accepted, the response has `accepted:true` and an `action_id`; the input is not rolled back. Use `wait` with `after_action` to retrieve the later result. An evicted action reports `action_expired`.46+Every request has `version`, a nonzero `id`, `op`, and `attachment`:
98-
99-`wait` requires at least one predicate. All supplied predicates are conjunctive:
100 47
101 ```json48 ```json
102-{"version":1,"id":6,"op":"wait","attachment":"<opaque-handle>","after_action":4,"timeout_ms":5000,"expect":{"text_contains":"driver ok","status":"attached"}}49+{"version":1,"id":1,"op":"capture","attachment":"<handle>","format":"both"}
50+{"version":1,"id":2,"op":"text","attachment":"<handle>","generation":1,"text":"printf 'driver ok'"}
51+{"version":1,"id":3,"op":"keys","attachment":"<handle>","generation":1,"keys":["Enter"]}
52+{"version":1,"id":4,"op":"wait","attachment":"<handle>","after_action":3,"expect":{"text_contains":"driver ok"}}
103 ```53 ```
104 54
105-Supported predicates are literal `text_contains`, an exact `session` (`lifecycle_id` and `session_name`), an exact `focus` (`tab_id` and `pane_id`), and `status` (`picker`, `connecting`, or `attached`). Without `after_action`, a wait may match the current published snapshot. With it, only the action's confirmed publication boundary and later snapshots are eligible. This is a postcondition check, not a claim that the action alone caused the text.55+### Operations
106-
107-## Limits and errors
108 56
109-- Requests are at most 64 KiB. A complete final JSON object without a newline is accepted; truncated JSON is not.57+- **`capture`**: `format` is `text` (default), `cells`, or `both`. Returns one string per row, plus cursor, geometry, and focus.
110-- Input is at most 16 KiB and a `keys` request has 1–256 tokens.58+- **`text`**: literal printable UTF-8. No control characters; use `keys` for Enter or Tab.
111-- Headless viewports are at most 512 columns by 256 rows.59+- **`keys`**: `Enter`, `Escape`, `Tab`, `Backspace`, `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`, `PageDown`, `Space`, one printable ASCII character, `Ctrl+<char>`, or `Alt+<char>`.
112-- The default and maximum request timeout are 5 seconds and 30 seconds.60+- **`wait`**: waits until all conditions match. Conditions: `text_contains`, `session`, `focus`, `status`. With `after_action`, only screens after that action count.
113-- Each attachment allows at most four socket connections, one admitted action across all controllers, four active waits, and 64 retained action records.
114-- Each connection has one response being written and at most two queued responses. A slow reader is closed after the bounded write deadline or queue overrun.
115-- Serialized response memory is bounded to 128 MiB per attachment. An oversized capture returns `capture_too_large`; it never resizes an interactive terminal.
116 61
117-Errors are structured and sanitized. Stable codes include `invalid_request`, `unsupported_version`, `permission_denied`, `stale_attachment`, `unavailable`, `timeout`, `outcome_unknown`, `capture_too_large`, `action_expired`, `input_busy`, `busy`, `navigation_failed`, and `endpoint_not_configured`. Error messages contain only the code; screen contents, credentials, endpoint internals, and environment values are not logged or returned.62+### Completion
118 63
119-EOF detaches this driver attachment and closes its access stream. It does not kill shells or sessions, does not stop the shared broker, and does not remove any root: the broker, its daemon, and every session stay alive for the other clients, and the pool's own idle shutdown remains the only thing that ends the daemon.64+- An action is `processed` when vev has handled and rendered it. That does not mean the shell has finished running the command: use `wait` for that.
65+- If a request times out after the input was accepted, the reply has `accepted:true` and an `action_id`. The input is not undone. Use `wait` with `after_action` to follow up.
120 66
121-## Isolation67+## Limits
122 68
123-The driver owns no endpoint root, no daemon, and no process. An isolated endpoint69+| Limit | Value |
124-is provisioned through the broker's own configuration before the driver starts,70+|---|---|
125-exactly like any other broker route: the broker is the single authority over71+| Request size | 64 KiB |
126-binary, root, environment, trust, and launch policy, and those values are never a72+| Input per request | 16 KiB, 1–256 keys |
127-payload of a JSONL request, a selected hostname, or a client environment variable.73+| Request timeout | 5 s default, 30 s max |
128-There is no per-invocation root creation, no owner token, and no remote cleanup74+| Connections per attachment | 4 |
129-step at driver exit.75+| Active waits | 4 |
130 76
131-Directory isolation is not a process, filesystem, or network sandbox; environment77+Error codes include `invalid_request`, `stale_attachment`, `unavailable`, `timeout`, `outcome_unknown`, `capture_too_large`, `input_busy`, and `navigation_failed`. Errors never contain screen content or credentials.
132-files and binaries remain sensitive.
133 78
134-## Existing interactive clients79+## Observe an interactive client
135 80
136-Observation and control are disabled by default. Enable them before an attach command:81+Observation is off by default. Enable it when you attach:
137 82
138 ```sh83 ```sh
139-vev --ui-observe attach work84+vev --ui-observe attach work # read-only
140-vev --ui-control new work85+vev --ui-control new work # read and send input
141 ```86 ```
142 87
143-`--ui-control` implies observation. The client prints one socket path to stderr, or uses the supplied private path:88+The client prints a private socket path to stderr (or use `--ui-socket /abs/path`). Connect to it:
144-
145-```sh
146-vev --ui-observe --ui-socket /absolute/private/ui.sock attach work
147-```
148-
149-The socket parent is private (`0700`) and the socket is private (`0600`). On Linux, the server also requires the connecting peer's UID to match the client UID. A path collision is rejected and never unlinks an existing file. Read-only observation rejects `keys` and `text` before they reach the input owner.
150-
151-The bridge form connects JSONL stdio to an already-running opted-in client; it does not create another attachment:
152 89
153 ```sh90 ```sh
154-vev --ui-driver --socket /absolute/private/ui.sock91+vev --ui-driver --socket /abs/path/ui.sock
155 ```92 ```
156 93
157-The physical terminal remains authoritative for geometry and terminal query responses. Mirroring observes bytes at the serialized terminal writer and publishes only after a successful physical flush. Capture and wait never write to the terminal, request a redraw, change focus, claim PTY geometry, send Resize, or acknowledge protocol output independently. If mirroring fails, capture becomes unavailable while the interactive client continues running. Disconnecting an observer leaves the interactive attachment and its shells running.94+- The socket is private (`0600`), and on Linux only your user can connect.
95+- `--ui-observe` rejects `keys` and `text`.
96+- Observers never resize, redraw, or slow down the real terminal. Disconnecting leaves the client running.
158 97
159-The driver intentionally does not provide image capture, mouse/paste/resize commands, subscriptions, scenarios, replay, automatic artifact export, public TCP access, or sandbox management.98+Not supported: image capture, mouse, paste, resize commands, and TCP access.
M docs/web-terminal.md +45 -39
@@ -4,74 +4,80 @@
44 vev --web-daemon
55 ```
66
7-The command starts a detached web gateway on **127.0.0.1:8778** and prints a private access link. Open that link in a browser. Running the command again with the same settings prints the same link when that gateway is reachable. Another process or development profile using the selected address and port prevents startup.
7+This starts a background web server on `127.0.0.1:8778` and prints a private link. Open it in a browser to get the normal vev UI: palette, tabs, and splits.
88
9-The gateway connects ordinary client attachments to the vev daemon. The daemon owns sessions, tabs, panes, palettes and PTYs. Each browser connection has its own terminal view. Closing the page detaches that view without killing its session shells. Reloading or choosing Reconnect creates a new ephemeral session; previous sessions remain available through the session picker (`SSP`). Repeated reloads can therefore accumulate shells: close unwanted sessions explicitly. Automatic reattachment is not implemented, and input is never automatically replayed.
9+- Each browser tab is its own client. Closing the page detaches; shells keep running.
10+- Reloading creates a **new** session. Find old ones with the session picker (`SSP`) and close the ones you don't need.
11+- Running the command again prints the same link.
1012
11-## Listener and public origin
13+## Access from another machine
1214
13-The gateway serves HTTP. A reverse proxy, such as a NetBird app, owns HTTPS and certificates. For a proxy on the same machine:
15+vev serves plain HTTP. For HTTPS, put a reverse proxy in front of it (for example Caddy, nginx, or a NetBird app).
1416
1517 ```sh
1618 vev --web-daemon --web-listen 127.0.0.1:8778 \
1719 --web-origin https://terminal.example.internal
1820 ```
1921
20-Equivalent persistent settings in vev's flat configuration file (`$XDG_CONFIG_HOME/vev/config`, or `~/.config/vev/config`):
22+Or in `~/.config/vev/config`:
2123
2224 ```ini
2325 web.listen = 127.0.0.1:8778
2426 web.origin = https://terminal.example.internal
2527 ```
2628
27-Flags override individual config values. Settings apply at gateway startup, not through live config reload. To change a running gateway's settings, stop that gateway and start it again; session shells survive. Token renewal uses the running gateway's origin even if the config file changes.
29+- `web.listen`: IP and port to listen on, such as `100.64.0.10:8778` or `[::1]:8778`. No hostnames.
30+- `web.origin`: the URL users type in the browser, without a path. Required when not listening on loopback.
31+- Flags override config. Restart the gateway to apply changes; shells survive.
2832
29-- `web.listen` accepts a literal IPv4 or bracketed IPv6 address and port, for example `100.64.0.10:8778` or `[::1]:8778`. Select the local NetBird IP when the proxy reaches VEV through NetBird. Interface names and DNS names are not listener addresses.
30-- `web.origin` is the browser-facing HTTP(S) origin, including a non-default port if needed. Paths, credentials, query strings and fragments are not accepted. A trailing `/` and default ports are normalized.
31-- Without settings, VEV uses `127.0.0.1:8778` and `http://127.0.0.1:8778`. A loopback listener can derive its HTTP origin; any non-loopback or wildcard listener requires an explicit origin.
32-- The proxy must preserve the public **Host** and browser **Origin**, forward WebSocket upgrades, and route the entire origin to VEV without a path prefix. VEV ignores `Forwarded` and `X-Forwarded-*`. HTTPS origins use `Secure` cookies and `wss://` in the browser.
33+The proxy must:
3334
34-Prefer loopback for a co-located proxy, or one specific private IP. `0.0.0.0` and `[::]` deliberately expose broader interfaces; use firewall and NetBird access rules to restrict reachability. HTTP access on a private encrypted network is possible, but remote HTTP does not provide a browser secure context. VEV neither provisions TLS nor verifies your proxy or network policy. Its readiness probe checks the local HTTP listener, not public DNS, certificates or proxy routing.
35+- keep the original `Host` and `Origin` headers,
36+- forward WebSocket upgrades,
37+- serve vev at the root path (no `/vev/` prefix).
38+
39+> [!WARNING]
40+> Access to this page is access to your shell. Prefer loopback or one private IP. Avoid `0.0.0.0` unless a firewall restricts access.
3541
3642 ## Controls
3743
38-The header shows a connection-state indicator and the number of connected browser terminal views, refreshed every two seconds. Background session shells are not counted. Button icons come from Lucide (ISC license, available at `/icons-license`).
44+| Input | Action |
45+|---|---|
46+| Alt+Space or **Palette** button | command palette |
47+| Alt+h/j/k/l | focus pane |
48+| Alt+1…9 | switch tab |
49+| Click | focus pane |
50+| Shift + wheel | scroll 10× faster |
51+| Ctrl+Shift+F6 | move focus out of the terminal to the Palette button |
3952
40-- Type directly into the terminal, including composed text and pasted plain text.
41-- Use **Alt+Space**, or the **Palette** button, for commands such as `CNT`, `SPR`, `SPD` and `SSP`.
42-- Use **Alt+h/j/k/l** to focus panes and **Alt+1…9** to switch tabs.
43-- Click panes to focus them. Mouse reports and wheel events follow the multiplexer input path. One wheel notch sends one wheel report; hold **Shift** for ×10 fast scrolling.
44-- The grid fits the browser window, up to 512 columns and 256 rows.
45-- On touch devices, **Select** preserves native text selection without terminal mouse capture; **Keys** enables interaction and opens the keyboard. The initial view does not force the keyboard open.
46-- **Ctrl+Shift+F6** moves focus out of terminal input to the Palette button for keyboard navigation.
53+- The terminal fits the window, up to 512×256 cells.
54+- On touch devices, **Select** allows native text selection and **Keys** opens the keyboard.
55+- Your browser or window manager may catch some shortcuts first. The Palette button avoids the common Alt+Space conflict.
4756
48-Touch controls have 44px targets. The layout accounts for safe areas and the visual viewport while preserving pinch zoom. Real-device keyboard, composition and selection behavior still requires validation on iOS Safari and Android Chrome; desktop device emulation is not a substitute. Browser scrollback and touch-to-terminal scrolling are not implemented.
57+Not supported yet: browser scrollback, touch scrolling, copy-mode clipboard export, Kitty images, OSC 52, and full emoji fidelity.
4958
50-Browser and window-manager shortcuts can intercept keys before vev receives them. The Palette button avoids the common Alt+Space window-menu conflict. Native clipboard shortcuts remain available; terminal copy-mode clipboard export is not implemented by the web adapter.
59+## Security
5160
52-## Isolated visual testing
61+- The access token is random, kept only in memory, and gone when the gateway stops.
62+- The link carries it once; the page then swaps it for an HttpOnly cookie.
63+- `vev --web-renew-token` revokes all links and browser sessions and prints a new link. Shells keep running.
64+- Host and Origin checks, a strict Content Security Policy, and input limits protect the endpoint.
65+- Everything is embedded in the binary. No CDN or frontend framework.
5366
54-From a source checkout:
67+## Stop the gateway
5568
56-```sh
57-go build -o .dev/vev-web .
58-env VEV_ENV=web-visual .dev/vev-web --web-daemon
59-```
69+The gateway is a separate process. Stopping the session daemon does not stop it. Send it `SIGTERM`.
70+
71+## Testing a build
6072
61-This uses the checkout's `.dev/web-visual` configuration, state and runtime directories instead of the normal installation. Keep the printed credential out of screenshots and reports. To inspect these test sessions from a physical terminal:
73+Use an isolated profile so tests don't touch your real sessions:
6274
6375 ```sh
64-env VEV_ENV=web-visual .dev/vev-web ls
76+go build -o .dev/vev-web .
77+VEV_ENV=web-visual .dev/vev-web --web-daemon
78+VEV_ENV=web-visual .dev/vev-web ls
6579 ```
6680
67-Useful visual checks: palette search, horizontal and vertical splits, tab switching, pane focus, shell input, full-screen applications, paste, scrolling, window resize and reload. Record browser/version, viewport size, action and expected result when reporting a problem.
68-
69-`scripts/web-visual-smoke.cjs` exercises the local gateway with an installed Playwright module. Set `PLAYWRIGHT_MODULE` to that module's absolute path, `VEV_BINARY` to the built executable and `VEV_ENV=web-visual`, and optionally `CHROMIUM_PATH` to an installed Chromium executable. It creates test sessions and panes; run it only against an isolated profile. `scripts/web-access-smoke.cjs` uses the same variables to check the view counter and token rotation; it revokes all browser access on that isolated gateway.
70-
71-## Security and implementation
72-
73-The listener binds IPv4 loopback by default. Network exposure requires explicit configuration. A fresh random 256-bit credential lives only in gateway memory and expires when that process stops. `vev --web-renew-token` replaces it on a running gateway, invalidates old links and cookies, disconnects browser views, and prints the new link without stopping session shells. The CLI retrieves or renews access through a profile-scoped local control Unix socket restricted by same-user peer credentials (Linux abstract socket; private 0600 socket file on macOS); no credential file is written. The access link carries it in a URL fragment; the page removes the fragment and exchanges it for an HttpOnly SameSite cookie. HTTP Host and Origin checks, authenticated WebSockets, a restrictive Content Security Policy, bounded input and connection limits protect the local terminal endpoint. Do not publish the credential. Treat access to this endpoint as access to your shell; restrict any proxy and upstream listener to trusted clients.
74-
75-The browser uses the dependency-free `vev-vt` DOM renderer and a small native JavaScript transport adapter. Go owns rendering state and terminal input encoding. Assets are embedded in the binary; there is no CDN, bundler, HTMX or frontend framework.
81+Keep the printed link out of screenshots.
7682
77-The web gateway is a separate long-lived process; stopping the session daemon does not stop the gateway. Send SIGTERM to the gateway process to stop it. The initial web frontend does not render Kitty graphics, export OSC 52 clipboard requests or guarantee full grapheme/emoji fidelity. Remote session navigation uses the existing client path but is not yet covered by the browser visual smoke checks.
83+`scripts/web-visual-smoke.cjs` and `scripts/web-access-smoke.cjs` run Playwright checks. Set `PLAYWRIGHT_MODULE`, `VEV_BINARY`, `VEV_ENV=web-visual`, and optionally `CHROMIUM_PATH`. Run them only against an isolated profile: they create sessions and revoke access.
@@ -4,74 +4,80 @@
4 vev --web-daemon4 vev --web-daemon
5 ```5 ```
6 6
7-The command starts a detached web gateway on **127.0.0.1:8778** and prints a private access link. Open that link in a browser. Running the command again with the same settings prints the same link when that gateway is reachable. Another process or development profile using the selected address and port prevents startup.7+This starts a background web server on `127.0.0.1:8778` and prints a private link. Open it in a browser to get the normal vev UI: palette, tabs, and splits.
8 8
9-The gateway connects ordinary client attachments to the vev daemon. The daemon owns sessions, tabs, panes, palettes and PTYs. Each browser connection has its own terminal view. Closing the page detaches that view without killing its session shells. Reloading or choosing Reconnect creates a new ephemeral session; previous sessions remain available through the session picker (`SSP`). Repeated reloads can therefore accumulate shells: close unwanted sessions explicitly. Automatic reattachment is not implemented, and input is never automatically replayed.9+- Each browser tab is its own client. Closing the page detaches; shells keep running.
10+- Reloading creates a **new** session. Find old ones with the session picker (`SSP`) and close the ones you don't need.
11+- Running the command again prints the same link.
10 12
11-## Listener and public origin13+## Access from another machine
12 14
13-The gateway serves HTTP. A reverse proxy, such as a NetBird app, owns HTTPS and certificates. For a proxy on the same machine:15+vev serves plain HTTP. For HTTPS, put a reverse proxy in front of it (for example Caddy, nginx, or a NetBird app).
14 16
15 ```sh17 ```sh
16 vev --web-daemon --web-listen 127.0.0.1:8778 \18 vev --web-daemon --web-listen 127.0.0.1:8778 \
17 --web-origin https://terminal.example.internal19 --web-origin https://terminal.example.internal
18 ```20 ```
19 21
20-Equivalent persistent settings in vev's flat configuration file (`$XDG_CONFIG_HOME/vev/config`, or `~/.config/vev/config`):22+Or in `~/.config/vev/config`:
21 23
22 ```ini24 ```ini
23 web.listen = 127.0.0.1:877825 web.listen = 127.0.0.1:8778
24 web.origin = https://terminal.example.internal26 web.origin = https://terminal.example.internal
25 ```27 ```
26 28
27-Flags override individual config values. Settings apply at gateway startup, not through live config reload. To change a running gateway's settings, stop that gateway and start it again; session shells survive. Token renewal uses the running gateway's origin even if the config file changes.29+- `web.listen`: IP and port to listen on, such as `100.64.0.10:8778` or `[::1]:8778`. No hostnames.
30+- `web.origin`: the URL users type in the browser, without a path. Required when not listening on loopback.
31+- Flags override config. Restart the gateway to apply changes; shells survive.
28 32
29-- `web.listen` accepts a literal IPv4 or bracketed IPv6 address and port, for example `100.64.0.10:8778` or `[::1]:8778`. Select the local NetBird IP when the proxy reaches VEV through NetBird. Interface names and DNS names are not listener addresses.33+The proxy must:
30-- `web.origin` is the browser-facing HTTP(S) origin, including a non-default port if needed. Paths, credentials, query strings and fragments are not accepted. A trailing `/` and default ports are normalized.
31-- Without settings, VEV uses `127.0.0.1:8778` and `http://127.0.0.1:8778`. A loopback listener can derive its HTTP origin; any non-loopback or wildcard listener requires an explicit origin.
32-- The proxy must preserve the public **Host** and browser **Origin**, forward WebSocket upgrades, and route the entire origin to VEV without a path prefix. VEV ignores `Forwarded` and `X-Forwarded-*`. HTTPS origins use `Secure` cookies and `wss://` in the browser.
33 34
34-Prefer loopback for a co-located proxy, or one specific private IP. `0.0.0.0` and `[::]` deliberately expose broader interfaces; use firewall and NetBird access rules to restrict reachability. HTTP access on a private encrypted network is possible, but remote HTTP does not provide a browser secure context. VEV neither provisions TLS nor verifies your proxy or network policy. Its readiness probe checks the local HTTP listener, not public DNS, certificates or proxy routing.35+- keep the original `Host` and `Origin` headers,
36+- forward WebSocket upgrades,
37+- serve vev at the root path (no `/vev/` prefix).
38+
39+> [!WARNING]
40+> Access to this page is access to your shell. Prefer loopback or one private IP. Avoid `0.0.0.0` unless a firewall restricts access.
35 41
36 ## Controls42 ## Controls
37 43
38-The header shows a connection-state indicator and the number of connected browser terminal views, refreshed every two seconds. Background session shells are not counted. Button icons come from Lucide (ISC license, available at `/icons-license`).44+| Input | Action |
45+|---|---|
46+| Alt+Space or **Palette** button | command palette |
47+| Alt+h/j/k/l | focus pane |
48+| Alt+1…9 | switch tab |
49+| Click | focus pane |
50+| Shift + wheel | scroll 10× faster |
51+| Ctrl+Shift+F6 | move focus out of the terminal to the Palette button |
39 52
40-- Type directly into the terminal, including composed text and pasted plain text.53+- The terminal fits the window, up to 512×256 cells.
41-- Use **Alt+Space**, or the **Palette** button, for commands such as `CNT`, `SPR`, `SPD` and `SSP`.54+- On touch devices, **Select** allows native text selection and **Keys** opens the keyboard.
42-- Use **Alt+h/j/k/l** to focus panes and **Alt+1…9** to switch tabs.55+- Your browser or window manager may catch some shortcuts first. The Palette button avoids the common Alt+Space conflict.
43-- Click panes to focus them. Mouse reports and wheel events follow the multiplexer input path. One wheel notch sends one wheel report; hold **Shift** for ×10 fast scrolling.
44-- The grid fits the browser window, up to 512 columns and 256 rows.
45-- On touch devices, **Select** preserves native text selection without terminal mouse capture; **Keys** enables interaction and opens the keyboard. The initial view does not force the keyboard open.
46-- **Ctrl+Shift+F6** moves focus out of terminal input to the Palette button for keyboard navigation.
47 56
48-Touch controls have 44px targets. The layout accounts for safe areas and the visual viewport while preserving pinch zoom. Real-device keyboard, composition and selection behavior still requires validation on iOS Safari and Android Chrome; desktop device emulation is not a substitute. Browser scrollback and touch-to-terminal scrolling are not implemented.57+Not supported yet: browser scrollback, touch scrolling, copy-mode clipboard export, Kitty images, OSC 52, and full emoji fidelity.
49 58
50-Browser and window-manager shortcuts can intercept keys before vev receives them. The Palette button avoids the common Alt+Space window-menu conflict. Native clipboard shortcuts remain available; terminal copy-mode clipboard export is not implemented by the web adapter.59+## Security
51 60
52-## Isolated visual testing61+- The access token is random, kept only in memory, and gone when the gateway stops.
62+- The link carries it once; the page then swaps it for an HttpOnly cookie.
63+- `vev --web-renew-token` revokes all links and browser sessions and prints a new link. Shells keep running.
64+- Host and Origin checks, a strict Content Security Policy, and input limits protect the endpoint.
65+- Everything is embedded in the binary. No CDN or frontend framework.
53 66
54-From a source checkout:67+## Stop the gateway
55 68
56-```sh69+The gateway is a separate process. Stopping the session daemon does not stop it. Send it `SIGTERM`.
57-go build -o .dev/vev-web .70+
58-env VEV_ENV=web-visual .dev/vev-web --web-daemon71+## Testing a build
59-```
60 72
61-This uses the checkout's `.dev/web-visual` configuration, state and runtime directories instead of the normal installation. Keep the printed credential out of screenshots and reports. To inspect these test sessions from a physical terminal:73+Use an isolated profile so tests don't touch your real sessions:
62 74
63 ```sh75 ```sh
64-env VEV_ENV=web-visual .dev/vev-web ls76+go build -o .dev/vev-web .
77+VEV_ENV=web-visual .dev/vev-web --web-daemon
78+VEV_ENV=web-visual .dev/vev-web ls
65 ```79 ```
66 80
67-Useful visual checks: palette search, horizontal and vertical splits, tab switching, pane focus, shell input, full-screen applications, paste, scrolling, window resize and reload. Record browser/version, viewport size, action and expected result when reporting a problem.81+Keep the printed link out of screenshots.
68-
69-`scripts/web-visual-smoke.cjs` exercises the local gateway with an installed Playwright module. Set `PLAYWRIGHT_MODULE` to that module's absolute path, `VEV_BINARY` to the built executable and `VEV_ENV=web-visual`, and optionally `CHROMIUM_PATH` to an installed Chromium executable. It creates test sessions and panes; run it only against an isolated profile. `scripts/web-access-smoke.cjs` uses the same variables to check the view counter and token rotation; it revokes all browser access on that isolated gateway.
70-
71-## Security and implementation
72-
73-The listener binds IPv4 loopback by default. Network exposure requires explicit configuration. A fresh random 256-bit credential lives only in gateway memory and expires when that process stops. `vev --web-renew-token` replaces it on a running gateway, invalidates old links and cookies, disconnects browser views, and prints the new link without stopping session shells. The CLI retrieves or renews access through a profile-scoped local control Unix socket restricted by same-user peer credentials (Linux abstract socket; private 0600 socket file on macOS); no credential file is written. The access link carries it in a URL fragment; the page removes the fragment and exchanges it for an HttpOnly SameSite cookie. HTTP Host and Origin checks, authenticated WebSockets, a restrictive Content Security Policy, bounded input and connection limits protect the local terminal endpoint. Do not publish the credential. Treat access to this endpoint as access to your shell; restrict any proxy and upstream listener to trusted clients.
74-
75-The browser uses the dependency-free `vev-vt` DOM renderer and a small native JavaScript transport adapter. Go owns rendering state and terminal input encoding. Assets are embedded in the binary; there is no CDN, bundler, HTMX or frontend framework.
76 82
77-The web gateway is a separate long-lived process; stopping the session daemon does not stop the gateway. Send SIGTERM to the gateway process to stop it. The initial web frontend does not render Kitty graphics, export OSC 52 clipboard requests or guarantee full grapheme/emoji fidelity. Remote session navigation uses the existing client path but is not yet covered by the browser visual smoke checks.83+`scripts/web-visual-smoke.cjs` and `scripts/web-access-smoke.cjs` run Playwright checks. Set `PLAYWRIGHT_MODULE`, `VEV_BINARY`, `VEV_ENV=web-visual`, and optionally `CHROMIUM_PATH`. Run them only against an isolated profile: they create sessions and revoke access.
M internal/usecase/daemon/performance_benchmark_test.go +1 -2
@@ -832,8 +832,7 @@ func benchmarkReportMetrics(b *testing.B, metrics performanceMetrics, operations
832832 b.ReportMetric(float64(historyRows), "historyrows/pane")
833833 }
834834
835-// Compact-page budgets are measured against the dense baseline in
836-// docs/compact-storage-benchmarks.md. Count and byte limits are separate:
835+// Compact-page budgets were measured against the dense VT baseline. Count and byte limits are separate:
837836 // small page dictionaries cost allocations but replace much larger cell copies.
838837 // The Protobuf envelope cutover added a measured ~47 allocations per warm
839838 // paint (generated message construction plus deterministic marshal); the
@@ -832,8 +832,7 @@ func benchmarkReportMetrics(b *testing.B, metrics performanceMetrics, operations
832 b.ReportMetric(float64(historyRows), "historyrows/pane")832 b.ReportMetric(float64(historyRows), "historyrows/pane")
833 }833 }
834 834
835-// Compact-page budgets are measured against the dense baseline in835+// Compact-page budgets were measured against the dense VT baseline. Count and byte limits are separate:
836-// docs/compact-storage-benchmarks.md. Count and byte limits are separate:
837 // small page dictionaries cost allocations but replace much larger cell copies.836 // small page dictionaries cost allocations but replace much larger cell copies.
838 // The Protobuf envelope cutover added a measured ~47 allocations per warm837 // The Protobuf envelope cutover added a measured ~47 allocations per warm
839 // paint (generated message construction plus deterministic marshal); the838 // paint (generated message construction plus deterministic marshal); the

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close