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 Unverified
B5690EEEBB952194The key is unknown
13 files · +491 -1247
M README.md +52 -38
| @@ -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 | 37 | ## Install | |
| 19 | 38 | ||
| 20 | 39 | ```sh | |
| @@ -28,30 +47,23 @@ Releases support Linux x86_64/arm64 and macOS Apple silicon. | |||
| 28 | 47 | ## Usage | |
| 29 | 48 | ||
| 30 | 49 | ```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 | |
| 35 | 54 | 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 | |
| 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). | |
| 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. | |
| 50 | 64 | ||
| 51 | 65 | ## Keys and palette | |
| 52 | 66 | ||
| 53 | - | No prefix key; bindings use Alt. | |
| 54 | - | ||
| 55 | 67 | | Key | Action | | |
| 56 | 68 | |---|---| | |
| 57 | 69 | | Alt+Space | command palette | | |
| @@ -70,29 +82,28 @@ Type a code or fuzzy-search the palette. Common default codes: | |||
| 70 | 82 | | `MFP` / `MAT` | move pane / tab | `SSP` | session picker | | |
| 71 | 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 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 | |
| 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 | 104 | ## 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. | |
| 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). | |
| 96 | 107 | ||
| 97 | 108 | ## Scripting | |
| 98 | 109 | ||
| @@ -104,16 +115,17 @@ vev cmd toast -l warn "build failed" | |||
| 104 | 115 | 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 | 120 | ## Documentation | |
| 110 | 121 | ||
| 111 | 122 | - [Configuration](docs/configuration.md): bindings, palette, theme, overlays, and bar anchors | |
| 112 | 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 | 126 | - [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) | |
| 117 | 129 | ||
| 118 | 130 | ## Development | |
| 119 | 131 | ||
| @@ -125,7 +137,7 @@ VEV_ENV=dev go run . kill --sessions | |||
| 125 | 137 | 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 | 142 | ```sh | |
| 131 | 143 | make test # go test ./... -race | |
| @@ -133,3 +145,5 @@ make lint # goimports check, go vet | |||
| 133 | 145 | make mocks # regenerate mocks | |
| 134 | 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). | |
| @@ -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 | ## Install | 37 | ## Install | ||
| 19 | 38 | ||||
| 20 | ```sh | 39 | ```sh | ||
| @@ -28,30 +47,23 @@ Releases support Linux x86_64/arm64 and macOS Apple silicon. | |||||
| 28 | ## Usage | 47 | ## Usage | ||
| 29 | 48 | ||||
| 30 | ```text | 49 | ```text | ||
| 31 | - | vev create an ephemeral session | 50 | + | vev new numbered session |
| 32 | - | vev new <name> create a named session | 51 | + | 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 daemon | 53 | + | vev attach user@host[:session] attach to a remote host |
| 35 | vev ls [<host>|--all] list sessions | 54 | vev ls [<host>|--all] list sessions | ||
| 36 | - | vev host add|rm <host> manage remote hosts | 55 | + | vev host add|rm|list manage remote hosts |
| 37 | - | vev host list list remote hosts | 56 | + | vev kill <name>|--sessions kill one or all sessions |
| 38 | - | vev kill <name>|--sessions kill one or every session | 57 | + | 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 palette | 65 | ## 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 attach | 93 | + | ```text |
| 80 | - | 94 | + | theme = auto # auto, dark, or light | |
| 81 | - | ```sh | 95 | + | 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 terminal | 104 | ## 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 | ## Scripting | 108 | ## Scripting | ||
| 98 | 109 | ||||
| @@ -104,16 +115,17 @@ vev cmd toast -l warn "build failed" | |||||
| 104 | vev cmd list-panes --json | 115 | 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 | ## Documentation | 120 | ## Documentation | ||
| 110 | 121 | ||||
| 111 | - [Configuration](docs/configuration.md): bindings, palette, theme, overlays, and bar anchors | 122 | - [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 observation | 127 | + | - [Browser terminal](docs/web-terminal.md) |
| 116 | - | - [Performance](docs/performance.md) | 128 | + | - [UI driver](docs/ui-driver.md) |
| 117 | 129 | ||||
| 118 | ## Development | 130 | ## Development | ||
| 119 | 131 | ||||
| @@ -125,7 +137,7 @@ VEV_ENV=dev go run . kill --sessions | |||||
| 125 | rm -rf .dev/dev | 137 | 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 | ```sh | 142 | ```sh | ||
| 131 | make test # go test ./... -race | 143 | make test # go test ./... -race | ||
| @@ -133,3 +145,5 @@ make lint # goimports check, go vet | |||||
| 133 | make mocks # regenerate mocks | 145 | 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 | |||
| 10 | 10 | | `failed` | A definite failure, including validation/cancellation before dispatch. | code is nonzero; output is empty. | | |
| 11 | 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 | 15 | ## Package ownership | |
| 16 | 16 | ||
| @@ -31,38 +31,14 @@ Every result retains the request ID for strict correlation. Version negotiation | |||
| 31 | 31 | (PreambleRequest first, PreambleResponse acceptance, exact version, | |
| 32 | 32 | negotiated ceilings, one absolute deadline), direction checks, and | |
| 33 | 33 | 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`. | |
| 54 | 36 | - `internal/adapters/quic`: one-stream QUIC carriage (TLS 1.3, epoch ALPN, | |
| 55 | 37 | exact SHA-256 pin, single bidirectional stream, bounded admission) plus | |
| 56 | 38 | the short-lived SSH bootstrap (ephemeral certificate, 32-byte token, | |
| 57 | 39 | nonce, ≤4 KiB readiness, ≤15 s expiry, atomic one-time consumption). | |
| 58 | 40 | 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`. | |
| 66 | 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 | 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 | 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 | 129 | - Implement I/O, queues, workers, environment integration, or technology selection in an adapter or `internal/app`. | |
| 154 | 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) | |
| 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 ownership | 15 | ## 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, and | 32 | negotiated ceilings, one absolute deadline), direction checks, and | ||
| 33 | decode-failure classification. | 33 | decode-failure classification. | ||
| 34 | - | - `internal/adapters/brokerwire`: translates between typed broker messages | 34 | + | - `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) plus | 37 | 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 broker | 41 | + | - `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 Protobuf | 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`). |
| 159 | - | conversation end to end: client tags 101-113 and server tags 201-210 are | 135 | + | |
| 160 | - | disjoint from every session tag, and each direction is its own closed | 136 | + | ```text |
| 161 | - | directional `oneof` union. A broker connection opens with exactly one | 137 | + | client ──broker.sock──▶ broker ──daemonmux──▶ local daemon |
| 162 | - | broker preamble (roles 3/4) that carries the same magic, epoch, exact | 138 | + | │ |
| 163 | - | `protocol.Version`, and 1 MiB..16 MiB / 1..64 KiB ceilings as the session | 139 | + | └──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 or | 141 | + | |
| 166 | - | allocation. The broker negotiates no optional capabilities, so nonzero | 142 | + | 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 state | 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`). |
| 170 | - | (ready/close, one subscription generation series, one assembler), multipart | 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`. |
| 171 | - | snapshot assembly with an 80 MiB staged ceiling that charges at least 64 | 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. |
| 172 | - | bytes per tab, and bounded operation/stream trackers. Every worker method | 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. |
| 173 | - | holds the connection lock across both the ready check and the tracker | 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. |
| 174 | - | mutation, so a racing close can never admit work after shutdown. The codec | 150 | + | |
| 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 @@ | |||
| 1 | 1 | # 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 | 12 | ```text | |
| 6 | 13 | # Browser gateway: HTTP listener and browser-facing origin, read at startup. | |
| @@ -41,6 +48,8 @@ focus-pane-down = alt+j | |||
| 41 | 48 | # Optional pane rearrangement actions are unbound by default. | |
| 42 | 49 | # consume-or-expel-pane-left = alt+H | |
| 43 | 50 | # 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 | 53 | switch-tab-1 = alt+1 | |
| 45 | 54 | # ... through switch-tab-9 = alt+9 | |
| 46 | 55 | ||
| @@ -60,6 +69,7 @@ bar.interval = 5s | |||
| 60 | 69 | # Command palette codes: 2-3 letters or digits. | |
| 61 | 70 | code.new-tab = CNT | |
| 62 | 71 | code.new-session = CNS | |
| 72 | + | code.create-ephemeral-session = CES | |
| 63 | 73 | code.close-tab = CLT | |
| 64 | 74 | code.split-right = SPR | |
| 65 | 75 | code.split-left = SPL | |
| @@ -74,6 +84,14 @@ code.focus-pane-left = FPL | |||
| 74 | 84 | code.focus-pane-right = FPR | |
| 75 | 85 | code.focus-pane-up = FPU | |
| 76 | 86 | 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 | 95 | code.next-tab = NXT | |
| 78 | 96 | code.previous-tab = PVT | |
| 79 | 97 | code.back-session = BCK | |
| @@ -84,39 +102,25 @@ code.toggle-floating-pane = TFP | |||
| 84 | 102 | code.rename-session = RNS | |
| 85 | 103 | code.rename-tab = RNT | |
| 86 | 104 | 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 | 109 | ## Scrollback | |
| 92 | 110 | ||
| 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. | |
| 116 | 120 | ||
| 117 | 121 | ## 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 | 125 | ```sh | |
| 122 | 126 | VEV_ENV=dev go run . | |
| @@ -124,25 +128,29 @@ VEV_ENV=dev go run . kill --sessions | |||
| 124 | 128 | 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. | |
| 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. | |
| 132 | 136 | ||
| 133 | 137 | ## Remote hosts | |
| 134 | 138 | ||
| 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 | + | ``` | |
| 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 | 149 | ### 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 | 155 | ```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. | |
| 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. | |
| 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 | 169 | ## 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). | |
| 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/` | | |
| 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 | 182 | ## 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`. | |
| 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. | | |
| 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 | 196 | ## 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 | 205 | ## 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 | 213 | ## 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 | 222 | ## 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, 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 | | |
| 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 | 235 | ```ini | |
| 213 | 236 | # Unicode whitespace always separates words. | |
| @@ -217,34 +240,32 @@ copy.word-separators = " -_@" | |||
| 217 | 240 | copy.reduce-motion = off | |
| 218 | 241 | ``` | |
| 219 | 242 | ||
| 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. | |
| 229 | 245 | ||
| 230 | 246 | ## 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. | |
| 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. | |
| 235 | 249 | ||
| 236 | 250 | ## 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 | 256 | ## 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 | 262 | ## 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. | |
| @@ -1,6 +1,13 @@ | |||||
| 1 | # Configuration | 1 | # 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 | ```text | 12 | ```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+H | 49 | # consume-or-expel-pane-left = alt+H | ||
| 43 | # consume-or-expel-pane-right = alt+L | 50 | # 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+1 | 53 | switch-tab-1 = alt+1 | ||
| 45 | # ... through switch-tab-9 = alt+9 | 54 | # ... 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 = CNT | 70 | code.new-tab = CNT | ||
| 62 | code.new-session = CNS | 71 | code.new-session = CNS | ||
| 72 | + | code.create-ephemeral-session = CES | |||
| 63 | code.close-tab = CLT | 73 | code.close-tab = CLT | ||
| 64 | code.split-right = SPR | 74 | code.split-right = SPR | ||
| 65 | code.split-left = SPL | 75 | code.split-left = SPL | ||
| @@ -74,6 +84,14 @@ code.focus-pane-left = FPL | |||||
| 74 | code.focus-pane-right = FPR | 84 | code.focus-pane-right = FPR | ||
| 75 | code.focus-pane-up = FPU | 85 | code.focus-pane-up = FPU | ||
| 76 | code.focus-pane-down = FPD | 86 | 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 = NXT | 95 | code.next-tab = NXT | ||
| 78 | code.previous-tab = PVT | 96 | code.previous-tab = PVT | ||
| 79 | code.back-session = BCK | 97 | code.back-session = BCK | ||
| @@ -84,39 +102,25 @@ code.toggle-floating-pane = TFP | |||||
| 84 | code.rename-session = RNS | 102 | code.rename-session = RNS | ||
| 85 | code.rename-tab = RNT | 103 | code.rename-tab = RNT | ||
| 86 | code.detach = DET | 104 | 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 | ## Scrollback | 109 | ## Scrollback | ||
| 92 | 110 | ||||
| 93 | - | `vev` chooses the retention policy; `vev-vt` enforces it. The PTY transports | 111 | + | | 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 the | 115 | + | |
| 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 uncompressed | 118 | + | - 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 environments | 121 | ## 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 | ```sh | 125 | ```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/dev | 128 | 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 hosts | 137 | ## 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 transports | 149 | ### 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 | ```json | 155 | ```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 state | 169 | ## 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 | ## Theme | 182 | ## 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 | ## Bindings | 196 | ## 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 expel | 205 | ## 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 overflow | 213 | ## 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 mode | 222 | ## 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, independently | 226 | + | | 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 ms | 228 | + | | `h` `j` `k` `l` | move | |
| 206 | - | intervals. Reversing direction cancels the old tail. Keyboard input and mouse | 229 | + | | `w` `b` `e` | word motions | |
| 207 | - | selection stop the animation; selection scrolling remains immediate. Returning | 230 | + | | `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 | ```ini | 235 | ```ini | ||
| 213 | # Unicode whitespace always separates words. | 236 | # Unicode whitespace always separates words. | ||
| @@ -217,34 +240,32 @@ copy.word-separators = " -_@" | |||||
| 217 | copy.reduce-motion = off | 240 | 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 setting | 244 | + | - `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 overlays | 246 | ## 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 anchor | 250 | ## 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 terminal | 256 | ## 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 anchors | 262 | ## 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 @@ | |||
| 1 | 1 | # 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 states | |
| 5 | + | ## 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 purge | |
| 12 | + | 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 commands | |
| 16 | + | ## 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 upgrades | |
| 27 | + | ```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 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. | |
| 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 | - | ## Retention | |
| 49 | + | 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 startup | |
| 53 | + | | 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. | |
| 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 recovery | 1 | # 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 states | 5 | + | ## 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 purge | 12 | + | 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 commands | 16 | + | ## 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 upgrades | 27 | + | ```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 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. | |||
| 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 | - | ## Retention | 49 | + | 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 startup | 53 | + | | 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 @@ | |||
| 1 | 1 | # Performance measurement | |
| 2 | 2 | ||
| 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. | |
| 6 | 15 | ||
| 7 | 16 | ## Canonical matrix | |
| 8 | 17 | ||
| @@ -208,37 +217,3 @@ go test ./internal/usecase/daemon -run '^$' \ | |||
| 208 | 217 | go test ./internal/usecase/daemon ./internal/usecase/copy -race \ | |
| 209 | 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. | |
| @@ -1,8 +1,17 @@ | |||||
| 1 | # Performance measurement | 1 | # Performance measurement | ||
| 2 | 2 | ||||
| 3 | - | vev measures the current session and attachment stack with process-local runtime | 3 | + | 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 alter | 4 | + | |
| 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 matrix | 16 | ## 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` | |||
| 12 | 12 | ||
| 13 | 13 | Client → server: `Hello`, `Input`, `Resize`, `Detach`, `Ping`, `List`, | |
| 14 | 14 | `Kill`, `Theme`, `Ack`, `ImagePush`, `ClientNotice`, `CommandRequest`, | |
| 15 | - | `OutputResetRequest`, `RemotePreviewRequest`, | |
| 15 | + | `OutputResetRequest`, `SelectTab`, `RemotePreviewWatch`, | |
| 16 | 16 | `RouteAttentionSubscription`, `SamePeerSwitchRequest`, | |
| 17 | 17 | `RecentRouteSnapshot`, `RouteNavigationFailure`, | |
| 18 | 18 | `SessionCreationFailure`, `UIFence`, `NavigationInventoryRequest`, | |
| 19 | 19 | `NavigationInventoryPublication`, `NavigationInventoryFailure`, | |
| 20 | - | `PickerClose`, `PickerSelection`, `PickerPreviewRequest`. | |
| 20 | + | `PickerClose`, `PickerSelection`. | |
| 21 | 21 | ||
| 22 | 22 | Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`, | |
| 23 | 23 | `Sessions`, `CommandResult`, `AttachTarget`, `RemotePreview`, | |
| @@ -26,8 +26,7 @@ Server → client: `Welcome`, `ErrorMsg`, `Output`, `Detached`, `Pong`, | |||
| 26 | 26 | `RouteRetired`, `SamePeerSwitchFailure`, `UIReceipt`, `UIViewUpdate`, | |
| 27 | 27 | `NavigationInventoryResponse`, `NavigationInventoryDemand`, | |
| 28 | 28 | `NavigationInventorySelection`, `PickerOffer`, `PickerSnapshot`, | |
| 29 | - | `PickerClosed`, `PickerResult`, `PickerFailure`, `PickerPreview`, | |
| 30 | - | `KillResult`. | |
| 29 | + | `PickerClosed`, `PickerResult`, `PickerFailure`, `KillResult`. | |
| 31 | 30 | ||
| 32 | 31 | `RouteNavigationFailure` is the one payload that appears in both unions. | |
| 33 | 32 | `sessionwire` conversion switches (`decodeProtoClient`/`decodeProtoServer`, | |
| @@ -66,9 +65,9 @@ negotiated ceiling. | |||
| 66 | 65 | the handshake deadline. | |
| 67 | 66 | ||
| 68 | 67 | 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 | |
| 72 | 71 | malformed first frame, gets a typed compatibility response and no session | |
| 73 | 72 | mutation: | |
| 74 | 73 | ||
| @@ -83,7 +82,7 @@ mutation: | |||
| 83 | 82 | | other / unknown | `ErrorMsg{ErrInternal, "expected hello"}` | no | | |
| 84 | 83 | ||
| 85 | 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 | 86 | (independent catalogue-era schema version `1`) must equal the negotiated | |
| 88 | 87 | constant or the request fails | |
| 89 | 88 | with the version-mismatch response above. There is no multi-version | |
| @@ -110,21 +109,16 @@ bounded to 4 KiB (`wire.PreambleLimit`, exposed as | |||
| 110 | 109 | `brokerwire.CheckPreambleSize`) before scan or allocation. | |
| 111 | 110 | ||
| 112 | 111 | 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 | + | ||
| 128 | 122 | Setup is fully bounded: the dialer's one context/deadline covers the Unix | |
| 129 | 123 | dial, preamble, Register send, and wait for Registered, and the server requires Register within | |
| 130 | 124 | 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-shot | 69 | + | one-shot control connections answered and closed. `RemotePreviewWatch` |
| 71 | - | control connections answered and closed. Anything else, including any | 70 | + | streams `RemotePreview` frames until the client closes. Anything else, including any |
| 72 | malformed first frame, gets a typed compatibility response and no session | 71 | 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 negotiated | 86 | (independent catalogue-era schema version `1`) must equal the negotiated | ||
| 88 | constant or the request fails | 87 | constant or the request fails | ||
| 89 | with the version-mismatch response above. There is no multi-version | 88 | 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 no | 111 | Refusals use the shared `PreambleRejectionCode` taxonomy. There is no | ||
| 113 | - | capability negotiation in P3.1: nonzero capability bits are refused as | 112 | + | capability negotiation: nonzero capability bits are refused as code 7 |
| 114 | - | code 7 (`limit refused`), and code 6 (`out of order`) is reserved for the | 113 | + | (`limit refused`), and code 6 (`out of order`) is reserved for the |
| 115 | - | connection dispatcher, which owns framing-order detection and never surfaces | 114 | + | connection dispatcher. |
| 116 | - | it from the stateless codec. Brokerwire is not activated by production | 115 | + | |
| 117 | - | composition until P3.2. The P3.3 private local endpoint | 116 | + | `internal/adapters/brokeripc` carries this conversation over `broker.sock` |
| 118 | - | (`internal/adapters/brokeripc`) carries this conversation over one per-user | 117 | + | in the per-user runtime directory, with same-user peer-credential admission |
| 119 | - | AF_UNIX socket: `broker.sock` in the per-user runtime directory, with | 118 | + | and bounded per-connection queues. The remote helpers `_broker-mux-stdio`, |
| 120 | - | same-user peer-credential admission, bounded per-connection queues, and | 119 | + | `_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 adds | 120 | + | 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 Unix | 122 | 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 within | 123 | 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. | |
| 156 | 29 | ||
| 157 | 30 | ## Connection states | |
| 158 | 31 | ||
| 159 | 32 | | 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 resilience | 1 | + | # Remote sessions |
| 2 | - | 2 | + | ||
| 3 | - | Remote attachments connect directly to the selected vev daemon. QUIC is the | 3 | + | ```sh |
| 4 | - | normal carriage: SSH bootstraps an authenticated endpoint (ephemeral | 4 | + | vev attach user@host[:session] |
| 5 | - | certificate, token, SHA-256 pin), then the attachment carries the vev | 5 | + | ``` |
| 6 | - | protocol over one QUIC stream. Set `VEV_REMOTE_TRANSPORT=stdio` to use an | 6 | + | |
| 7 | - | SSH-only carriage explicitly when QUIC is unavailable. vev must be installed on | 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 | - | 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 keeps | 11 | + | 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/output | 12 | + | 2. SSH hands back a one-time token and the server's certificate fingerprint. |
| 13 | - | acknowledgements, and reconnect state. Reconnecting one attachment resumes that | 13 | + | 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-local | 14 | + | |
| 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 identity | 17 | + | If UDP is blocked, use SSH only: |
| 18 | - | 18 | + | ||
| 19 | - | The session picker shows remote live, stopped, broken, stale, and unavailable | 19 | + | ```sh |
| 20 | - | rows using the same expandable session/tab shape as local rows. A row carries a | 20 | + | VEV_REMOTE_TRANSPORT=stdio vev attach user@host |
| 21 | - | lifecycle ID and stable tab selector separately from its display origin; labels | 21 | + | ``` |
| 22 | - | are never parsed back into routes. Cached and stale rows remain navigable, but | 22 | + | |
| 23 | - | activation is gated until the host catalog and the exact lifecycle/tab identity | 23 | + | ## What survives a network problem |
| 24 | - | are current. A successful picker handoff sends the structured target to the | 24 | + | |
| 25 | - | owning daemon without creating a local shadow session. Picker-selected remote | 25 | + | - The remote session and its shells keep running while you are offline. |
| 26 | - | attachments use the daemon's environment and persisted session working | 26 | + | - 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 states | 30 | ## 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 bytes | 41 | + | |
| 169 | - | written into the PTY's output stream. | 42 | + | ## Hybrid mode: local and remote together |
| 170 | - | 43 | + | ||
| 171 | - | ## Diagnostics and checks | 44 | + | 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 first | 56 | + | - 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 and | 59 | + | |
| 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 @@ | |||
| 1 | 1 | # Session navigation | |
| 2 | 2 | ||
| 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 | |
| 11 | 4 | ||
| 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`. | |
| 17 | 6 | ||
| 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 | | |
| 19 | 15 | ||
| 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. | |
| 24 | 19 | ||
| 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 | |
| 47 | 21 | ||
| 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 | | |
| 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 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 navigation | 1 | # Session navigation | ||
| 2 | 2 | ||||
| 3 | - | Palette destinations use bare session names in local-only mode. With remote | 3 | + | ## 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 by | 5 | + | 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 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 | | |||
| 19 | 15 | ||||
| 20 | - | While attached to a remote daemon, the client runs one relay per attachment | 16 | + | - 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 local | 17 | + | - 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 daemon | 18 | + | - 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 generation | 20 | + | ## 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 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 | | |||
| 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 vision | 37 | + | 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 @@ | |||
| 1 | 1 | # Terminal support | |
| 2 | 2 | ||
| 3 | - | ## Color | |
| 3 | + | ## 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 entry | |
| 18 | + | ### 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 | 22 | ```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 | |
| 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 | - | ```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: | |
| 38 | 29 | ||
| 39 | 30 | ```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 | |
| 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 | 37 | ## Kitty graphics | |
| 46 | 38 | ||
| 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. | |
| 65 | 40 | ||
| 66 | - | ## Environment | |
| 41 | + | Supported: | |
| 67 | 42 | ||
| 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. | |
| 73 | 45 | ||
| 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. | |
| 80 | 47 | ||
| 81 | - | ## VT compatibility | |
| 48 | + | 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 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. | |
| 86 | 55 | ||
| 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 | |
| 93 | 57 | ||
| 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. | |
| 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 caveat | |
| 64 | + | ## 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). | |
| @@ -1,102 +1,66 @@ | |||||
| 1 | # Terminal support | 1 | # Terminal support | ||
| 2 | 2 | ||||
| 3 | - | ## Color | 3 | + | ## 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 entry | 18 | + | ### 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 | ```sh | 22 | ```sh | ||
| 18 | - | infocmp -x xterm-direct | 23 | + | 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 | - | ```sh | 28 | + | 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 | ```sh | 30 | ```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 | |||
| 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 graphics | 37 | ## Kitty graphics | ||
| 46 | 38 | ||||
| 47 | - | Kitty graphics support is attachment-local and explicitly declared only after a | 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. |
| 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 | - | ## Environment | 41 | + | Supported: |
| 67 | 42 | ||||
| 68 | - | Local and direct CLI remote attachments supply the session's environment for | 43 | + | - `kitten icat` with PNG and JPEG images. |
| 69 | - | future PTY children. A later client-owned attachment updates that shared | 44 | + | - 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 environment | 46 | + | 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 compatibility | 48 | + | 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 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. | |||
| 86 | 55 | ||||
| 87 | - | The session's PTY content size follows the latest valid attach, resume, or | 56 | + | ## 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 | - | ## 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. |
| 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 caveat | 64 | + | ## 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 @@ | |||
| 1 | 1 | # 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 attachment | |
| 6 | - | ||
| 7 | - | Start a new numbered ephemeral session with the default 80x24 viewport: | |
| 5 | + | ## Start | |
| 8 | 6 | ||
| 9 | 7 | ```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 | |
| 42 | 12 | ``` | |
| 43 | 13 | ||
| 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) | | |
| 70 | 21 | ||
| 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. | |
| 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 | 28 | ```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"}} | |
| 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. | |
| 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: | |
| 90 | 33 | ||
| 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 | | |
| 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`. | |
| 98 | - | ||
| 99 | - | `wait` requires at least one predicate. All supplied predicates are conjunctive: | |
| 46 | + | Every request has `version`, a nonzero `id`, `op`, and `attachment`: | |
| 100 | 47 | ||
| 101 | 48 | ```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. | |
| 106 | - | ||
| 107 | - | ## Limits and errors | |
| 55 | + | ### Operations | |
| 108 | 56 | ||
| 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. | |
| 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 | - | ## Isolation | |
| 67 | + | ## Limits | |
| 122 | 68 | ||
| 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 | | |
| 130 | 76 | ||
| 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. | |
| 133 | 78 | ||
| 134 | - | ## Existing interactive clients | |
| 79 | + | ## 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 | 83 | ```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 | |
| 141 | 86 | ``` | |
| 142 | 87 | ||
| 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: | |
| 152 | 89 | ||
| 153 | 90 | ```sh | |
| 154 | - | vev --ui-driver --socket /absolute/private/ui.sock | |
| 91 | + | 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. | |
| @@ -1,159 +1,98 @@ | |||||
| 1 | # UI driver | 1 | # 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 attachment | 5 | + | ## Start |
| 6 | - | ||||
| 7 | - | Start a new numbered ephemeral session with the default 80x24 viewport: | |||
| 8 | 6 | ||||
| 9 | ```sh | 7 | ```sh | ||
| 10 | - | vev --ui-driver | 8 | + | 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 never | 11 | + | 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 | - | ```json | 22 | + | 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 | ```json | 28 | ```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 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 | | |||
| 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 | ```json | 48 | ```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 | - | ## Isolation | 67 | + | ## Limits |
| 122 | 68 | ||||
| 123 | - | The driver owns no endpoint root, no daemon, and no process. An isolated endpoint | 69 | + | | 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 over | 71 | + | | Request size | 64 KiB | |
| 126 | - | binary, root, environment, trust, and launch policy, and those values are never a | 72 | + | | 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 cleanup | 74 | + | | 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; environment | 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. |
| 132 | - | files and binaries remain sensitive. | |||
| 133 | 78 | ||||
| 134 | - | ## Existing interactive clients | 79 | + | ## 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 | ```sh | 83 | ```sh | ||
| 139 | - | vev --ui-observe attach work | 84 | + | vev --ui-observe attach work # read-only |
| 140 | - | vev --ui-control new work | 85 | + | 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 | ```sh | 90 | ```sh | ||
| 154 | - | vev --ui-driver --socket /absolute/private/ui.sock | 91 | + | 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 @@ | |||
| 4 | 4 | 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 origin | |
| 13 | + | ## 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 | 17 | ```sh | |
| 16 | 18 | vev --web-daemon --web-listen 127.0.0.1:8778 \ | |
| 17 | 19 | --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 | 24 | ```ini | |
| 23 | 25 | web.listen = 127.0.0.1:8778 | |
| 24 | 26 | 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. | |
| 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: | |
| 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 | 42 | ## 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. | |
| 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. | |
| 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 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. | |
| 53 | 66 | ||
| 54 | - | From a source checkout: | |
| 67 | + | ## Stop the gateway | |
| 55 | 68 | ||
| 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 | |
| 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 | 75 | ```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 | |
| 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. | |
| 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. | |
| 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. | |
| @@ -4,74 +4,80 @@ | |||||
| 4 | vev --web-daemon | 4 | 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 origin | 13 | + | ## 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 | ```sh | 17 | ```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.internal | 19 | --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 | ```ini | 24 | ```ini | ||
| 23 | web.listen = 127.0.0.1:8778 | 25 | web.listen = 127.0.0.1:8778 | ||
| 24 | web.origin = https://terminal.example.internal | 26 | 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 | ## Controls | 42 | ## 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 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. | |||
| 53 | 66 | ||||
| 54 | - | From a source checkout: | 67 | + | ## Stop the gateway |
| 55 | 68 | ||||
| 56 | - | ```sh | 69 | + | 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-daemon | 71 | + | ## 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 | ```sh | 75 | ```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 | |||
| 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 | |||
| 832 | 832 | b.ReportMetric(float64(historyRows), "historyrows/pane") | |
| 833 | 833 | } | |
| 834 | 834 | ||
| 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: | |
| 837 | 836 | // small page dictionaries cost allocations but replace much larger cell copies. | |
| 838 | 837 | // The Protobuf envelope cutover added a measured ~47 allocations per warm | |
| 839 | 838 | // 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 in | 835 | + | // 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 warm | 837 | // The Protobuf envelope cutover added a measured ~47 allocations per warm | ||
| 839 | // paint (generated message construction plus deterministic marshal); the | 838 | // paint (generated message construction plus deterministic marshal); the | ||