@vibelord/lanes 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/lanes/README.md +92 -7
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +928 -150
- package/payload/lanes/client/ghostty/common.conf +3 -0
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +25 -0
- package/payload/lanes/client/ghostty/lanes-palette.conf +25 -0
- package/payload/lanes/client/ghostty/omarchy.conf +3 -2
- package/payload/lanes/client/lanes-launch +62 -5
- package/payload/lanes/client/lanes-window +17 -1
- package/payload/lanes/container/env.sh +8 -0
- package/payload/lanes/container/hooks/claude-settings.json +10 -0
- package/payload/lanes/container/hooks/codex-hooks.json +22 -0
- package/payload/lanes/container/lane +326 -25
- package/payload/lanes/container/lane-signal +173 -0
- package/payload/lanes/docs/ARCHITECTURE.md +158 -6
- package/payload/lanes/docs/DESIGN.md +250 -0
- package/payload/lanes/docs/INSTALL.md +39 -0
- package/payload/lanes/docs/RUNBOOK.md +507 -24
- package/payload/lanes/docs/SHARING.md +1 -1
- package/payload/lanes/holders/exec.sh +2 -5
- package/payload/lanes/holders/ssh.sh +18 -0
- package/payload/lanes/holders.lock +3 -0
- package/payload/lanes/install/enable-phone.sh +15 -6
- package/payload/lanes/install/enable-web.sh +19 -6
- package/payload/lanes/install/host-install.sh +10 -1
- package/payload/lanes/install/services.py +30 -3
- package/payload/lanes/install.sh +108 -3
- package/payload/lanes/libexec/agent_state.py +246 -0
- package/payload/lanes/libexec/connection.py +178 -0
- package/payload/lanes/libexec/cursor_sync.py +147 -6
- package/payload/lanes/libexec/device_sync.py +134 -0
- package/payload/lanes/libexec/economy_poll.py +685 -0
- package/payload/lanes/libexec/guard.py +27 -1
- package/payload/lanes/libexec/lanes_lib.py +436 -36
- package/payload/lanes/libexec/notify.py +317 -0
- package/payload/lanes/libexec/registrations.py +148 -0
- package/payload/lanes/libexec/session_actions.py +53 -22
- package/payload/lanes/libexec/sidebar.py +1190 -186
- package/payload/lanes/libexec/statusd.py +352 -90
- package/payload/lanes/libexec/tokens.py +310 -0
- package/payload/lanes/libexec/tokens.sh +11 -0
- package/payload/lanes/libexec/view.sh +22 -11
- package/payload/lanes/systemd/lanes-economy.service +20 -0
- package/payload/lanes/systemd/lanes-notify.service +19 -0
- package/payload/lanes/systemd/lanes-sync.service +3 -0
- package/payload/lanes/web/README.md +96 -2
- package/payload/lanes/web/browser.css +22 -0
- package/payload/lanes/web/browser.js +73 -0
- package/payload/lanes/web/index.html +2 -1
- package/payload/lanes/web/mobile.css +108 -221
- package/payload/lanes/web/mobile.js +105 -55
- package/payload/lanes/workspaces.example/README.md +9 -3
- package/payload/lanes/zellij/config.kdl +128 -19
- package/payload/manifest.json +7 -3
- package/payload/lanes/libexec/legacy_sessions.py +0 -130
|
@@ -14,22 +14,63 @@ original names and measurements; substitute your account, containers and paths i
|
|
|
14
14
|
| Wide view: 2 or 3 viewports side by side per workspace | `lanes-window w2` / `w3` (anarchy: `Super+Shift+Alt+L`). In the sidebar `Enter` opens in the first viewport, `2` / `3` open the lane under the cursor in the second / third; `²` `³` mark what they show |
|
|
15
15
|
| One lane, no multiplexer, native Ghostty scrollback | `lanes-launch lane <lane-id>` (ids: `lanes-launch ls`) |
|
|
16
16
|
| Switch workspace | `Ctrl-Alt-←/→` (zellij tabs) |
|
|
17
|
-
| Next / previous lane | `Ctrl-Alt-↓` / `Ctrl-Alt-↑` (works from inside the lane
|
|
17
|
+
| Next / previous lane | `Ctrl-Alt-↓` / `Ctrl-Alt-↑` (works from inside the lane: `lanes next\|prev --focused` in a floating pane that closes at once points the FOCUSED viewport — the first one when the sidebar has the focus — at the next / previous lane of its workspace in registry order, wrapping; nothing is typed and the focus stays where it was; on a tab without a workspace, e.g. Host, it does nothing). In the sidebar `J` / `K` still step through the list as it is drawn |
|
|
18
|
+
| Lock the view | `Ctrl-Alt-g` locks (every key reaches the lane; the mode ribbon shows LOCKED in on-accent-urgent) and unlocks. `Ctrl-g` is never bound: it always reaches the lane |
|
|
18
19
|
| Find a lane by name | `Ctrl-Alt-/`, type a few letters, `Enter` |
|
|
19
20
|
| Browse the sidebar | click it (or `Ctrl-Alt-p` then `←`), `↑/↓` or `j/k`, `Enter`; selecting a lane focuses it again |
|
|
21
|
+
| Go to the next lane that needs you | `Tab` in the sidebar opens the next lane in **Needs you** (`Shift-Tab` the previous), cycling from the lane this pane shows — or from the cursor lane when the shown one does not need you; ` nothing needs you` when the section is empty |
|
|
20
22
|
| Scroll a lane | mouse wheel / tmux copy-mode as before (history lives in the lane, not in the viewer) |
|
|
21
23
|
| Throttle monitor | its own full-size tab, `Throttle` (next to `Host`); it starts when you first open the tab and restarts itself if it exits. It is no longer an entry in every board's sidebar. |
|
|
22
24
|
| Detach | `Ctrl-Alt-o` then `d` — or just close the window. Nothing on the server stops. |
|
|
23
25
|
|
|
24
|
-
Sidebar glyphs:
|
|
25
|
-
|
|
26
|
-
state shown (see "When the host is overloaded")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
Sidebar glyphs: see the legend in [`docs/DESIGN.md`](DESIGN.md) (agent column `◆ ◇ ▸ ✓ ✗ _ ·`, terminal column
|
|
27
|
+
`● ○ – ! ✗ · ?`, every glyph with its ASCII twin, the token table and the modes); a dimmed glyph with `?` in column 34
|
|
28
|
+
means not observed right now, last known state shown (see "When the host is overloaded"). By row column (spec §4.2;
|
|
29
|
+
every row is 36 cells and column 36 is never written):
|
|
30
|
+
|
|
31
|
+
| Column | Shows |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| 1 | `▌` the cursor (no underline) |
|
|
34
|
+
| 2 | the agent, from `status.json`'s additive `agent{}` (never from `state`): `◆` approval prompt blocking · `✗` errored · `◇` turn finished, waiting for your prompt · `▸` working · `✓` done, until you open the lane from this device (`clients/<device>.seen.json`) · `_` harness ended into a shell · `·` harness up, no turn · blank = a plain shell; dimmed when the lane is stale |
|
|
35
|
+
| 3 | `›` the lane this pane shows — never on the cursor row, where the bold name says it · `²` / `³` shown in pane 2 / 3 of a wide view |
|
|
36
|
+
| 4–27 | the name in its identity colour, bold while this pane shows it; up to three `·` (other sessions view it) are reserved first, and a longer name ends in `…` |
|
|
37
|
+
| 28 | `+` on an unregistered tmux session that can be added to a workspace (click it, or `a`) |
|
|
38
|
+
| 29–31 | the age: the agent's `since`, else the last output — `45s`, ` 3m`, ` 2h`, ` 4d` (at most `99d`); blank when nothing is known; amber inside **Needs you** |
|
|
39
|
+
| 33 | the stream: `●` output in the last 20 s · `○` quiet · `–` absent (no such session right now, e.g. a finished orchestrator turn) · `!` pane dead · `✗` container down · `·` helper · `?` never observed; dimmed when an agent glyph sits in column 2 (`▸` + `○` is a long tool call, not a stuck lane) or when stale |
|
|
40
|
+
| 34 | one tail cell by priority: `?` not observed right now, last known state shown (see "When the host is overloaded") > `⚠` starving (the pane is `active` but the proxy saw no request from the lane for over 600 s, or a 429 in its recent-request ring) > `↓` downgraded in the last 5 min > `$` more than half its recent requests paid. The three economy cells come from `economy.json` ("Economy projection" below); a lane the poller has not joined — or no poller — has a blank cell |
|
|
41
|
+
| 35 | `×` close, on unregistered sessions only: it asks ` › close ‘name’? y/n` first; `y` closes, any other key cancels |
|
|
42
|
+
|
|
43
|
+
The cursor is a lane, not a row: when a lane changes section (it starts needing you, its turn finishes, you open a
|
|
44
|
+
`done` lane) the cursor and the scroll position go with it. A list that fits the pane is never scrolled: its
|
|
45
|
+
first rows stay on screen.
|
|
46
|
+
|
|
47
|
+
Title and notifications (spec §4.9): the sidebar pane's title is `WORKSPACE · ◆1 ◇2` (the header counts, rewritten
|
|
48
|
+
only when they change; a workspace without agents never sets one) — the phone is built to read its counts from it.
|
|
49
|
+
Since plan Task 26 the phone's picker pane has no fixed layout name, so Zellij can report that title — but only in a
|
|
50
|
+
session laid out after Task 26: an existing phone device keeps the old picker pane until `lanes rebuild <device>`
|
|
51
|
+
(`upgrade-session` and `ensure` add missing tabs and rename titles, they never re-lay-out a tab; `lanes doctor` names
|
|
52
|
+
such a device). Whether it
|
|
53
|
+
reaches the phone is still unchecked in a browser (DEF-1450). A lane that
|
|
54
|
+
STARTS needing you (`◆` `✗` `◇`) raises one desktop notification, `name: task`, through OSC 9 — once per transition,
|
|
55
|
+
never for the lane this pane shows, never on a phone, and not at all under `LANES_NOTIFY=0`. A sidebar that starts
|
|
56
|
+
(or restarts) while lanes already need you announces each of them once. Both are bytes in the frame the sidebar
|
|
57
|
+
already writes: no process is started. Ghostty shows OSC 9 only while the lanes window is NOT focused
|
|
58
|
+
(`desktop-notifications = true` in `client/ghostty/common.conf`); on a terminal or Zellij build that drops OSC 9 the
|
|
59
|
+
notification is a silent no-op and nothing else changes. Mac check: not yet run (the plan's Task 25 step 4 asks for one
|
|
60
|
+
on the Mac — record the Zellij and Ghostty versions and the outcome here).
|
|
61
|
+
|
|
62
|
+
Header: the workspace title, then `◆1 ◇2 ▸6 ✓1` in the column-2 glyphs (zeros omitted; `N lanes` without agents;
|
|
63
|
+
dimmed while the footer warns that status is old). Sections: **Needs you** (◆, ✗, ◇ — oldest first), **Working**
|
|
64
|
+
(oldest first), **Done** (newest first), then the workspace's other lanes under its own header — only when an agent
|
|
65
|
+
section exists; a filter flattens them and keeps column 2. A list longer than the pane ends in `⋮ N more`; moving onto
|
|
66
|
+
that row, or clicking it, scrolls instead of opening the lane behind it. **Unregistered** lists every observed tmux
|
|
67
|
+
session associated with this workspace but absent from its registry — nothing alive is ever hidden, and a session
|
|
68
|
+
whose NAME could not be declared (parentheses, `/` and `,` are outside the lane grammar) is listed, opened and closed
|
|
69
|
+
exactly like any other ghost: the grammar governs what may be DECLARED, never what may be SEEN. (`status.json`
|
|
70
|
+
carries a `note` on such a row saying why it cannot be declared; no surface draws it yet.) A session on a socket
|
|
71
|
+
SEVERAL boards declare, whose name claims no board and whose directory is under nobody's roots, is listed by EVERY
|
|
72
|
+
owner of that socket rather than dropped — one id, one row, several sidebars. Design of record:
|
|
73
|
+
`docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md` §3–§4.
|
|
33
74
|
A `·` after a lane's name (up to three) = other sessions VIEW that lane: any session GROUPED onto it, a Throttle
|
|
34
75
|
Studio browser attach (`view-<studio>-<ch>`) included — counted in the lane's `attached`/`viewers` in `status.json`,
|
|
35
76
|
never listed as unregistered (`statusd.viewer_of`; a ghost carries its viewers too, so a browser on an undeclared
|
|
@@ -37,7 +78,7 @@ lane is counted there). The `view-` NAME decides nothing: an ungrouped `view-not
|
|
|
37
78
|
killed under it, are alive and ARE listed — but when a killed lane left SEVERAL grouped survivors, the oldest is
|
|
38
79
|
the lane and the later ones are its viewers, the same choice the Studio makes (`statusd.group_leaders`,
|
|
39
80
|
`inventory::group_leader`). A declared lane is always its own row, whatever it is grouped onto. A Studio lane the
|
|
40
|
-
Studio itself started is `st_<board>_<slug>` on the board's socket and IS
|
|
81
|
+
Studio itself started is `st_<board>_<slug>` on the board's socket and IS listed under **Unregistered** until declared, on a
|
|
41
82
|
socket some board declares or not: discovery probes every live socket in the container.
|
|
42
83
|
|
|
43
84
|
**Unregistered terminals:** every workspace has an **Unregistered** section, including sessions on shared and
|
|
@@ -52,10 +93,23 @@ process running under the survivor, which returns next cycle as a new `👻` (DE
|
|
|
52
93
|
have no close control. `+` creates a shell from the keyboard. New and Close are explicit user actions; discovery,
|
|
53
94
|
reattachment, and reconnects never create or close a session.
|
|
54
95
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
96
|
+
To keep an unregistered tmux session in a workspace's regular lane list, click/tap **+** beside it (or select it
|
|
97
|
+
and press `a`), then choose a workspace. Only workspaces using its container are offered. **Cancel** or Esc leaves
|
|
98
|
+
it unregistered. This preserves its name, running programs, scrollback and active views. It appears immediately
|
|
99
|
+
and survives sync and reconnects. Registered lanes have no close control.
|
|
100
|
+
|
|
101
|
+
From the server CLI, use `lanes ls --unregistered` to find the ID, then
|
|
102
|
+
`lanes register <lane-id> [workspace-id]`; omitting the workspace keeps the current assignment.
|
|
103
|
+
The site's version-1 `registered-sessions.json` is durable configuration: back it up with the catalog.
|
|
104
|
+
To undo registration, remove its record from `sessions` and run `lanes sync`; the terminal keeps running.
|
|
105
|
+
If publication fails after saving, `lanes sync` finishes it. Invalid/newer file versions are refused without
|
|
106
|
+
overwriting the file. These registrations have no restart recipe: an ended session stays absent until the operator
|
|
107
|
+
starts it. Add a repository terminal declaration when editor integration and a saved create recipe are wanted.
|
|
108
|
+
|
|
109
|
+
The tracker for plain Docker shells from before persistent terminals (**Legacy** rows) is retired (plan Task 19):
|
|
110
|
+
statusd no longer reads `~/.local/state/lanes/legacy-sessions/` and the sidebar no longer lists or closes those
|
|
111
|
+
shells. `lanes doctor` names any record still there (`⚠ legacy-sessions/ holds N records …`); end that shell
|
|
112
|
+
inside its container by hand, then delete the record.
|
|
59
113
|
|
|
60
114
|
## Server (as the workspace account)
|
|
61
115
|
|
|
@@ -64,14 +118,64 @@ lanes doctor full health check lanes status per-wor
|
|
|
64
118
|
lanes sync re-read registries now lanes ls [--json] lanes with live status
|
|
65
119
|
lanes rebuild <device> recreate a view session lanes ensure recreate missing view sessions
|
|
66
120
|
lanes invariant save|check prove no live pane changed (pid + window size)
|
|
121
|
+
lanes device list the devices ensure keeps lanes device forget <name> stop recreating one
|
|
67
122
|
systemctl --user status lanes-statusd lanes-sync.timer lanes-ensure.timer lanes-web
|
|
68
123
|
```
|
|
69
124
|
|
|
125
|
+
- **What `lanes doctor` says about the boards' recipes (plan Task 18):** one line counts the tmux lanes per create
|
|
126
|
+
recipe class — `idempotent` (a `tmux new -A…`: creates, or attaches when the session exists; the canonical recipe),
|
|
127
|
+
`attach-only` (`attach-session` and no `new`), `null` (no recipe), `external` (a script or anything else, run as
|
|
128
|
+
written) and `retired-path` (names the retired in-container `~/.config/coder-launch`). One line says how many of
|
|
129
|
+
them create with their identity (`-e LANE_ID=…`, DEF-1478). Warnings (`⚠`, never a `✗`, the exit code is
|
|
130
|
+
unchanged): lanes that `lane open` cannot recreate once they end (attach-only, null, retired-path — fix with
|
|
131
|
+
`lanes cursor-sync --canonicalize`), lanes bound to a numeric session name (tmux numbers unnamed sessions itself,
|
|
132
|
+
so a recreated one gets another number), more than 4 lanes on one container's tmux socket `default` (the server every
|
|
133
|
+
plain `tmux` typed in that container talks to), and tmux socket files in a container's `/tmp/tmux-1000` that no
|
|
134
|
+
running tmux server (read from the container's cgroup) owns. That last one is **report only**: the host never
|
|
135
|
+
removes a file in a container. Inside the container, `~/.lz/bin/lane gc --sockets` probes each socket file with
|
|
136
|
+
`tmux -N -S <path> list-sessions` and, where tmux fails, connects to the file once itself: only ECONNREFUSED (the
|
|
137
|
+
file is there, nobody listens) unlinks it, printing each path; a server that answers (even with no session), does
|
|
138
|
+
not answer within 5 s, or refuses for another reason (EACCES, …) keeps its file. The errno decides, never tmux's
|
|
139
|
+
wording: tmux 3.4 prints `no server running on <path>` for ECONNREFUSED and `error connecting to <path>
|
|
140
|
+
(<strerror>)` only for other errnos (DEF-1459).
|
|
141
|
+
- **A device you no longer use (an old laptop, a phone you replaced):** `lanes device forget <name>` removes
|
|
142
|
+
`devices/<name>.json`, so `lanes ensure` (`lanes-ensure.timer`) stops recreating its view session and `sync` /
|
|
143
|
+
`upgrade-session` stop updating it. A view session that is running is left alone and says so; once it ends, nothing
|
|
144
|
+
brings it back. `lanes attach <name>` / `lanes prepare <name>` creates the device again. `lanes device list` shows
|
|
145
|
+
each device, phone or desktop (viewports), and whether its view session runs.
|
|
146
|
+
|
|
70
147
|
- **A view looks wrong / stuck:** `lanes rebuild mac`. Views are disposable; agents are untouched; the client reconnects.
|
|
71
148
|
- **A lane was added/renamed/removed in a repo's `.vscode/terminals.json`:** nothing to do. `lanes-sync.timer`
|
|
72
|
-
re-reads registries every 2 minutes and the sidebar follows. The zellij layout is never edited live.
|
|
149
|
+
re-reads registries every 2 minutes and the sidebar follows. The zellij layout is never edited live. A sync that
|
|
150
|
+
finds nothing new leaves `registry.json` (and its `generated_at`) untouched and only records the time in
|
|
151
|
+
`registry.synced`; the unit is bounded at `TimeoutStartSec=120`.
|
|
152
|
+
- **`lanes gc` (daily `lanes-gc.timer`):** truncates zmx logs over 5 MB and removes a `resolve.dynamic/<id>` file
|
|
153
|
+
only when that id is in none of `status.json`'s `lanes`, `unregistered` or `zmx` AND the file is over an hour old.
|
|
154
|
+
statusd lists every `unregistered` and `zmx` row id under `lanes` as well; reading the other two keys is a
|
|
155
|
+
defence, not a second source. The mtime alone is no liveness signal: an unregistered session's file is rewritten only when its target or name
|
|
156
|
+
changes, so a live one is as old as the session. `removed stale <id>` for a session you can see is a defect.
|
|
157
|
+
- **Give a registered lane a different display name:** sidebar `R`, or `lanes rename <lane-id> <name>`. It writes
|
|
158
|
+
`repositories[i].displayNames` in the board's `workspaces.<board>.json` (a `.bak-<UTC>` copy is kept on the first
|
|
159
|
+
write) and syncs; the lane id, its resolve file and Cursor's `lane open <id>` stay the same. Renaming back to the
|
|
160
|
+
terminal's own title removes the override. A session added through Add to workspace / `lanes register` is renamed
|
|
161
|
+
in its `registered-sessions.json` record instead (same id, same binding); an unregistered session in its
|
|
162
|
+
`user-sessions/<id>.json`.
|
|
163
|
+
- **Promote (`P`) vs Add to workspace (`a`):** `P` declares an UNREGISTERED tmux session in the board's single
|
|
164
|
+
repository `terminals.json` (it then gets a repository lane id, `<ws>--<slug>`); `a` keeps the session's id. `P` is
|
|
165
|
+
refused on any lane that is already registered — a repository lane or an added session. It asks first, in the
|
|
166
|
+
footer's one prompt shape: ` › Register ‘name’? y/n`.
|
|
167
|
+
- **The sidebar footer** (bottom-up: footer, rule, strip row, upload strip; spec §4.6) shows ONE line by priority: the
|
|
168
|
+
close confirm ` › close ‘name’? y/n` > ` › Rename: …█` > ` › Register ‘name’? y/n` > the filter ` /abc · 3 matches` >
|
|
169
|
+
the health banner > ` ⚠ registry: …` > the last message > the legend ` ⏎ open · / find · x close · ? keys`. `?` shows
|
|
170
|
+
every key in place of the list until the next key. A filter with no hits says ` no lane matches ‘abc’ · Esc`; a
|
|
171
|
+
workspace with no lanes says ` no lanes declared · + New shell`. Above the rule, when the pane has 18 rows or more,
|
|
172
|
+
the strip row is the fleet economy line when `economy.json` knows a fleet fact (` $8.5k/16k 53% · F2 S1 · spill 76%`,
|
|
173
|
+
see "Economy projection"); otherwise it names the OTHER workspaces whose agents need you (` ◆1 Worlds · ◇2 Code`,
|
|
174
|
+
from the status.json already read). Under `LANES_GLYPHS=ascii` every glyph, separator and quote has a seven-bit twin (` > close 'sh'? y/n`,
|
|
175
|
+
` Enter open - / find - x close - ? keys`).
|
|
73
176
|
- **A newer layout added a tab (e.g. `Throttle`):** `lanes upgrade-session` adds it to the live sessions without a
|
|
74
|
-
rebuild and leaves you on the tab you were on.
|
|
177
|
+
rebuild and leaves you on the tab you were on. It never changes an existing tab's panes: a changed pane (e.g. the
|
|
178
|
+
phone picker after plan Task 26, which `lanes doctor` flags) needs `lanes rebuild <device>`.
|
|
75
179
|
- **A new workspace:** add it to `~/.config/lanes/workspaces/catalog.json` + a manifest, `lanes sync`, then
|
|
76
180
|
`lanes rebuild <device>` (the only time a layout changes).
|
|
77
181
|
- **Registry invalid:** the workspace keeps its previous lanes; the sidebar footer and `lanes doctor` show why.
|
|
@@ -93,7 +197,7 @@ fails first. A question that got no answer is never published as "the lane is go
|
|
|
93
197
|
| Viewport: **THIS VIEWER DISCONNECTED — the lane's tmux server is still running** | The viewer's tmux client was dropped or could not attach in time; the lane's socket is still there. That is ALL this headline claims: the retry below it is what learns whether the session itself survived, and when the holder then answers `is not provisioned`, nothing says the lane is untouched (a lane killed elsewhere — a Throttle Studio `Stop`, an operator's `kill-session` — leaves the server up, and the old headline claimed the lane still ran, DEF-965). The reason and a health hint follow; it retries by itself. | Nothing, unless the retries keep saying `is not provisioned` — then the lane is gone: pick another in the sidebar. |
|
|
94
198
|
| Viewport status screen WITHOUT that line | The lane's tmux socket is really gone. | "A dead lane, or a blind harness?" below. |
|
|
95
199
|
| Client window: `connecting…`, later a hold screen with a countdown | `lanes-launch` never closes the window by itself. ssh failed, or the server dropped a slow client (zellij does that with exit 0), or the server refused (exit 64–78): it holds, re-attaches when the countdown ends, backs off up to 60 s. `Enter` = now; closing the window = quit. Log: `~/.local/state/lanes-client/launch-<target>.log`. | Leave it open. |
|
|
96
|
-
| Upload box `no upload helper` while windows were closing | The helper is a per-machine service now (launchd `ai.beyondwork.lanes-helper` / `systemctl --user status lanes-helper`) and survives the window. It counts as serving while `~/.local/state/lanes-client/helper.alive` is younger than 90 s. | `./install.sh --check` on the client. |
|
|
200
|
+
| Upload strip ` ✗ no upload helper` (the old upload box: `no upload helper`) while windows were closing | The helper is a per-machine service now (launchd `ai.beyondwork.lanes-helper` / `systemctl --user status lanes-helper`) and survives the window. It counts as serving while `~/.local/state/lanes-client/helper.alive` is younger than 90 s. | `./install.sh --check` on the client. |
|
|
97
201
|
| A live test suite prints `SKIP host is busy: …`, exit 75 | `tests/*.py` refuse to start above load 2 × cores or memory pressure (some avg10) 20 %: they add load and would fail for the wrong reason. | Run it later (`LANES_TESTS_FORCE=1` only on purpose). |
|
|
98
202
|
|
|
99
203
|
### Look, without adding load
|
|
@@ -103,6 +207,8 @@ lanes pressure memory / io / cpu pressure, swap, load, per-container
|
|
|
103
207
|
shmem) and /tmp fill. Plain files + statvfs: safe on a thrashing host. Exit 1 unless level ok.
|
|
104
208
|
cat ~/.local/state/lanes/health what statusd wrote last; at= and status_at= are epochs — compare with `date +%s`
|
|
105
209
|
lanes doctor every external call is bounded; "N shown with their last known state" = blind, not dead
|
|
210
|
+
`preview renderer:` pinned glow | system glow (<path>) | less -R — looked up on the view
|
|
211
|
+
server's PATH, the one the Ctrl-Alt-m pane sees, never your shell's
|
|
106
212
|
lanes invariant check 1 s: every live pane's pid and size against the last `invariant save`
|
|
107
213
|
```
|
|
108
214
|
|
|
@@ -165,6 +271,20 @@ refused. Everything removed is logged in `~/.local/state/lanes/tmpscan/deleted.l
|
|
|
165
271
|
touch. Socket there and the server's pid alive = the lane lives and only the harness is blind.
|
|
166
272
|
5. `health` older than 30 s (`at=` against `date +%s`) = statusd itself is starved: the sidebar shows history.
|
|
167
273
|
|
|
274
|
+
How statusd looks, and why one wedged server cannot freeze the board: it finds the live tmux servers in each
|
|
275
|
+
workspace container's OWN cgroup (`cgroup.procs` + `comm` + `cmdline` under `/proc`, the same walk as `lanes guard`;
|
|
276
|
+
no `ps`, no fork), probes at most 8 servers at once, and stops waiting `PROBE_TIMEOUT + 2` = 7 s after the round
|
|
277
|
+
began. A probe still running then is published as `"observed": "timeout"` (last state, `stale`), the other servers'
|
|
278
|
+
answers go out on time, and the next round starts only once that probe's own bound has passed (another 7 s). A
|
|
279
|
+
server the deadline cut carries `"cut": true` in `status.json` `servers` and is probed FIRST next round, so with more
|
|
280
|
+
servers than 8 the same tail is not cut every cycle (and never drifts to `unknown` while it answers in time). A
|
|
281
|
+
container whose recorded init pid cannot be walked (not a docker cgroup, a recycled pid) makes the round treat THAT
|
|
282
|
+
container's socket files as possibly live — nothing of it is concluded gone; the other containers keep their real
|
|
283
|
+
live sets. In `journalctl --user -u lanes-statusd`: `observe: … still running at the 7 s cycle deadline (first next
|
|
284
|
+
round): <container>/<socket> …` names the abandoned probes, and `live servers <container>: pid <n>: …` names a
|
|
285
|
+
container that became unwalkable (logged once, when it starts). `lanes invariant` stays all-or-nothing: one
|
|
286
|
+
unwalkable container makes it refuse rather than save a partial pane list.
|
|
287
|
+
|
|
168
288
|
A lane is dead only on a POSITIVE observation: its server answered without that session (`–`), its pane died (`!`),
|
|
169
289
|
or its container is down (`✖`).
|
|
170
290
|
|
|
@@ -194,6 +314,11 @@ Coexistence mode can be turned back on at any time with `touch ~/.local/state/la
|
|
|
194
314
|
- Terminal: Ghostty in a dedicated instance (`~/.config/lanes/ghostty.conf`, inherits `~/.config/ghostty/config`, so
|
|
195
315
|
Omarchy theme switches follow). Foot stays the system default; without Ghostty the window falls back to Foot with
|
|
196
316
|
the same class. Install Ghostty only: `omarchy-pkg-add ghostty` (`omarchy-default-terminal` is not touched).
|
|
317
|
+
- Font: the Omarchy default font; a Nerd Font is not required by lanes — every glyph is plain Unicode
|
|
318
|
+
(`LANES_GLYPHS=ascii` for any font; the blocks a font needs are listed under "TUIs and the terminal's colours"
|
|
319
|
+
below). The Zellij bar runs with `simplified_ui true`, so it draws no Private-Use-Area chevrons either.
|
|
320
|
+
- Look of the window: `lanes-window --ascii`, `--light`, `--dark` (combinable with a suffix: `lanes-window --ascii w2`)
|
|
321
|
+
— see "TUIs and the terminal's colours" below.
|
|
197
322
|
- Hyprland: one line `require("hypr.lanes")` at the end of `~/.config/hypr/hyprland.lua` + `~/.config/hypr/lanes.lua`
|
|
198
323
|
(validated with `hyprctl configerrors`; backup `hyprland.lua.pre-lanes.*`). Pin it to a workspace by uncommenting
|
|
199
324
|
the `o.window` line in `lanes.lua`.
|
|
@@ -223,6 +348,67 @@ night sky of single-dot Braille "stars". Started with no client attached it pain
|
|
|
223
348
|
- It stops recurring when the clients agree on brightness: the same light/dark choice in Cursor's terminal and in the
|
|
224
349
|
lanes window (Ghostty `theme = light:…,dark:…` follows the OS and therefore flips twice a day).
|
|
225
350
|
|
|
351
|
+
The sidebar's own colours and glyphs come from one token table (`libexec/tokens.py`; spec
|
|
352
|
+
`docs/superpowers/specs/2026-10-03-lanes-cockpit-design.md` §2.4) and three switches, resolved per device:
|
|
353
|
+
|
|
354
|
+
| Switch | Values | Default |
|
|
355
|
+
|---|---|---|
|
|
356
|
+
| `LANES_COLOR` / `--color` | `truecolor` · `16` · `none` | `truecolor` when `COLORTERM` is `truecolor`/`24bit`, else `16` (your terminal's own palette); `NO_COLOR` set ⇒ `none` (glyphs only) |
|
|
357
|
+
| `LANES_THEME` / `--theme` | `dark` · `light` · `auto` | `auto`: the sidebar asks the terminal's background once at start (OSC 11) and paints dark until the answer; no answer within 0.3 s ⇒ dark |
|
|
358
|
+
| `LANES_GLYPHS` / `--ascii`, `--glyphs` | `unicode` · `ascii` · `emoji` | `unicode`; `ascii` when none of `LC_ALL`/`LC_CTYPE`/`LANG` names UTF-8 |
|
|
359
|
+
| `LANES_DIM` | `90` · `2` | `90`; 16-colour mode only: `2` is the escape hatch for a palette whose colour 8 is near the background |
|
|
360
|
+
|
|
361
|
+
- Set it per device, live: `lanes attach DEVICE --ascii` (or `--glyphs M`, `--theme T`, `--color C`) records the
|
|
362
|
+
choice in `~/.local/state/lanes/devices/<device>.json` (only for a device that already has that file; a bad value
|
|
363
|
+
refuses before anything is created). Every running sidebar of that device re-reads the file within one tick — no
|
|
364
|
+
restart. The choice survives a recreated session (`lanes ensure` after a reboot, `lanes rebuild`). Environment
|
|
365
|
+
variables never reach a running sidebar: Zellij panes keep the server's environment from the moment the session
|
|
366
|
+
was created. The device file wins over the environment.
|
|
367
|
+
- The sidebar publishes what it resolved: `clients/<device>.theme` (`dark`/`light`, on change or when absent; for
|
|
368
|
+
`glow`) and `clients/<device>.glyphs` (`unicode`/`ascii`/`emoji`, on change or when absent; read by `view.sh`).
|
|
369
|
+
- The terminal's answer to the OSC 11 question is taken out of the input before any key is read, also when it is
|
|
370
|
+
split across two reads, so it never cancels a filter or a prompt.
|
|
371
|
+
- Dim text is SGR 90 in 16-colour mode (never SGR 2, which several terminals render as invisible). A palette whose
|
|
372
|
+
colour 8 is close to the background (classic Solarized dark) can set `LANES_DIM=2` as the escape hatch.
|
|
373
|
+
- From the Mac or Omarchy window: `lanes-window --ascii`, `--light` or `--dark` (before an optional suffix:
|
|
374
|
+
`lanes-window --dark w2`) hands `--ascii` / `--theme light|dark` to `lanes-launch`, which passes them to
|
|
375
|
+
`lanes attach` — the same per-device record as above. `lanes-launch DEVICE --ascii|--theme T|--glyphs M|--color C`
|
|
376
|
+
takes them directly; a bad value refuses before any connection, an unknown option is named and dropped.
|
|
377
|
+
- The viewport's own status screens (`view.sh`: "THIS VIEWER DISCONNECTED", "No lane selected", "retrying in …") use
|
|
378
|
+
the same tokens (`libexec/tokens.sh`, sourced once, no fork): a bold lead with `⚠` — `!` when
|
|
379
|
+
`clients/<device>.glyphs` says `ascii`, read on every screen, so `lanes attach DEVICE --ascii` reaches the next
|
|
380
|
+
status screen without a restart — and the hint line in SGR 90 instead of SGR 2.
|
|
381
|
+
- Fonts: no Nerd Font is needed. Any monospace font with Latin-1, Box Drawing (U+2500 `─`), Geometric Shapes
|
|
382
|
+
(U+25A0–25FF `● ○ ◆ ◇ ▸ ▌ █`), Dingbats (`✓ ✗`), Arrows (`↑ ↓`) and `⋮ ⏎ ⚠ …` renders the cockpit (JetBrains Mono,
|
|
383
|
+
SF Mono, Menlo, Cascadia, DejaVu Sans Mono all do); `--ascii` is the complete remedy for any other font.
|
|
384
|
+
- The lanes window inherits your everyday Ghostty look (fonts, theme, padding: `client/ghostty/common.conf` never sets
|
|
385
|
+
them). Opt in to the cockpit's own palette with `./install.sh --palette` (client install): it installs
|
|
386
|
+
`~/.config/lanes/ghostty-palette.conf` — generated from the token table (`client/ghostty/lanes-palette.conf` and
|
|
387
|
+
`lanes-palette-light.conf`, `libexec/tokens.py --ghostty-dark|--ghostty-light`; slots 1–4 are the four tones, so
|
|
388
|
+
16-colour mode under it equals truecolor mode) — and includes it last in `~/.config/lanes/ghostty.conf`. On Ghostty
|
|
389
|
+
1.1 or newer (`ghostty +version`) that file is `theme = light:…,dark:…` over two installed theme files, so the
|
|
390
|
+
window still flips with the OS; on an older or unknown Ghostty it is the dark palette alone (the light one is
|
|
391
|
+
installed next to it as `ghostty-palette-light.conf`). Ghostty lays a `theme` UNDER every colour key
|
|
392
|
+
(`background`, `foreground`, `palette = N=…`, cursor/selection colours) set explicitly anywhere in the loaded
|
|
393
|
+
configuration, whatever the file order — and the lanes window loads your everyday config first. So the installer
|
|
394
|
+
walks the everyday config and every file it reaches through `config-file`: if any sets a colour key (Omarchy does —
|
|
395
|
+
its config includes the current theme's `ghostty.conf`), a theme pair would silently keep your everyday colours, and
|
|
396
|
+
it installs the explicit dark palette instead and says which file forced that (`dark only: … sets colours
|
|
397
|
+
explicitly`). Ghostty also loads `config-file` includes breadth-first (one first-in-first-out list:
|
|
398
|
+
`Config.zig` `loadRecursiveFiles`), so a file two includes deep — Omarchy's theme `ghostty.conf` — loads after
|
|
399
|
+
every direct include of the lanes `ghostty.conf`, the palette's included. The installer therefore records the
|
|
400
|
+
everyday chain's deepest include and places the palette keys (or the theme pair) behind as many one-line hop files
|
|
401
|
+
(`ghostty-palette.1.conf`, …) as it takes to sit at that depth, where they load last; on Omarchy that is one hop,
|
|
402
|
+
and the `say` line names it. A plain `./install.sh` re-run without `--palette` removes the include and all palette
|
|
403
|
+
files: inherit-first is the default. NOT yet looked at on a real Ghostty (2026-10-04: no Mac or Omarchy client was
|
|
404
|
+
used; the installer was exercised against a fake `ghostty` that reports a version, fake everyday configs and a
|
|
405
|
+
model of Ghostty's load order read from its source). DEF-1466 stays open until that is seen: run `--palette` on a
|
|
406
|
+
Mac (Ghostty ≥ 1.1) and on Omarchy, and the window background must be `#101318` (the dark `surface` token).
|
|
407
|
+
- OSC 11 on a real Ghostty view is NOT yet verified (2026-10-04: Task 22 ran on the server with no Mac client
|
|
408
|
+
attached, and no live session was touched). To check: with the Mac in light mode, `lanes attach mac` from the Mac,
|
|
409
|
+
then on the server `cat ~/.local/state/lanes/clients/mac.theme` — `light` means the answer arrived and was parsed;
|
|
410
|
+
`dark` means no answer within 0.3 s (the sidebar painted dark). Record the result here.
|
|
411
|
+
|
|
226
412
|
### "tmux server … is not running" on every lane at once (deleted sockets)
|
|
227
413
|
|
|
228
414
|
Seen 2026-09-20 04:24: something emptied `coder-dev-work:/tmp`, and with it `/tmp/tmux-1000` — the directory of every
|
|
@@ -236,7 +422,8 @@ Cursor terminals need a reopen. Never `rm -rf /tmp/*` in a workspace container;
|
|
|
236
422
|
## Cursor / VS Code terminals = the same lanes
|
|
237
423
|
|
|
238
424
|
Cursor uses the Terminals Manager extension, which opens what `<folder>/.vscode/terminals.json` lists. Those files
|
|
239
|
-
stay the source of truth for
|
|
425
|
+
stay the source of truth for editor-defined lanes; sidebar registrations supplement the Lanes view without
|
|
426
|
+
changing the editor's terminal list. `lanes cursor-sync`
|
|
240
427
|
only normalizes HOW a terminal reaches its lane:
|
|
241
428
|
|
|
242
429
|
- every `command` becomes `exec /home/coder/.lz/bin/lane open <lane-id>` — attach; if the lane is missing, run its
|
|
@@ -247,6 +434,20 @@ only normalizes HOW a terminal reaches its lane:
|
|
|
247
434
|
`lanes cursor-sync <ws> --write` applies (backup under `~/.local/state/lanes/cursor-sync-backups/`). The repo then
|
|
248
435
|
shows an ordinary git diff to review and commit. Boards that keep their registry outside a repository
|
|
249
436
|
are additionally projected into the file Cursor reads (`"cursor"` in `workspaces/*.json`).
|
|
437
|
+
- `lanes cursor-sync <ws> --canonicalize` (dry run; add `--write` to apply) replaces every tmux lane's recorded
|
|
438
|
+
create recipe that cannot create the lane — none, attach-only, the retired `~/.config/coder-launch` path (the three
|
|
439
|
+
classes `lanes doctor` warns about, DEF-1460), or a script missing in the container — with the
|
|
440
|
+
canonical `tmux new -ADs … -e LANE_ID=… -e LANE_WS=… bash -l` recipe (old value kept as `createPrevious`).
|
|
441
|
+
`--identity` adds the same `-e` flags to a working plain `tmux -L S new -ADs N [-c D] CMD` recipe, keeping CMD's
|
|
442
|
+
argv; any other working recipe is left alone and its lane carries no identity. These `-e` flags are the ONLY way a
|
|
443
|
+
tmux lane gets `LANE_ID`/the trace: `lane open` deliberately exports none (DEF-1478 — the first client on a shared
|
|
444
|
+
board socket would otherwise hand its identity to every later lane there). Both change the file only: a RUNNING
|
|
445
|
+
lane keeps its environment and gains the identity when it is next created. Before enabling it on a live board,
|
|
446
|
+
read DEF-1479: a `lanes/<ws>/<id>` trace makes the whole lane — main session, subagents, fan-out — one proxy
|
|
447
|
+
fairness bucket. Applying `--canonicalize --write` or `--identity --write` to a live workspace is an operator gate
|
|
448
|
+
(plan "Operator gates"): it rewrites operator-declared recipes in the repo's `.vscode/terminals.json` (old value kept
|
|
449
|
+
as `createPrevious`, file backup under `cursor-sync-backups/<ts>/` as above) and produces the first live `lanes/<ws>/<id>` trace — decide DEF-1479 lane-level
|
|
450
|
+
vs per-session fairness first.
|
|
250
451
|
- Size policy "the lanes view wins": lanes-view tmux clients run with `TERM=xterm-lanesview` (a user-level terminfo
|
|
251
452
|
alias of xterm-256color). While such a client shows a lane, every other client of that lane (Cursor terminals,
|
|
252
453
|
ad-hoc attaches) is in tmux `ignore-size`; `lanes-statusd` reconciles every few seconds and gives editors the size
|
|
@@ -254,6 +455,207 @@ only normalizes HOW a terminal reaches its lane:
|
|
|
254
455
|
included, and both halves apply there: an `ignore-size` an operator set by hand on such a socket is given
|
|
255
456
|
back like any other once no view shows the lane (DEF-982).
|
|
256
457
|
|
|
458
|
+
## Installing agent hooks (opt-in, per container, dry run first)
|
|
459
|
+
|
|
460
|
+
Without hooks the sidebar's agent glyph comes from Claude's session files and Codex's pane title. Hooks make it
|
|
461
|
+
event-driven: Claude Code or Codex runs `~/.lz/bin/lane-signal <status>` on a turn event, which writes
|
|
462
|
+
`~/.lz/state/hooks/<lane id>.json` in the container, and `lanes-statusd` prefers that over the other signals while
|
|
463
|
+
its agent runs and its last event is under an hour old. Nothing installs a hook for you. (`~/.lz/state/<name>.json`
|
|
464
|
+
itself is `lane-run`'s phase record of a zmx lane; the hook never writes there. A hooked agent in a zmx lane writes
|
|
465
|
+
its sidecar under the zmx session name, which the sidebar does not show yet — DEF-1437.)
|
|
466
|
+
|
|
467
|
+
1. `lanes install-container <name>` — puts `lane-signal` in `~/.lz/bin/` and the definitions in `~/.lz/hooks/`.
|
|
468
|
+
2. `lanes hooks install --agent claude --container <name>` — prints the unified diff it would make to that
|
|
469
|
+
container's `~/.claude/settings.json` (your own hooks and keys stay; an earlier lane-signal install is replaced,
|
|
470
|
+
not duplicated). Read it. Then add `--write` (the old file is kept as `settings.json.lanes-bak-<time>`).
|
|
471
|
+
3. `lanes hooks install --agent codex --container <name>` — the same for `~/.codex/hooks.json`, plus a top-level
|
|
472
|
+
`notify = [".../lane-signal", "done", …]` in `~/.codex/config.toml` only when that file has no top-level
|
|
473
|
+
`notify` (an existing notifier is left alone). Codex asks you to review and trust new hooks before running
|
|
474
|
+
them (`[hooks.state]` in its config.toml); lanes never writes that trust.
|
|
475
|
+
|
|
476
|
+
Expect a hook to reach only agent sessions started after the install (both harnesses read their hook
|
|
477
|
+
configuration at start — INFERRED, not measured here). `--home <dir>` targets a home directory instead of a
|
|
478
|
+
container (for trying the merge on a copy). Undo: restore the `.lanes-bak-*` file, or delete the groups whose
|
|
479
|
+
command contains `lane-signal`.
|
|
480
|
+
|
|
481
|
+
What each event writes (`agent{}` vocabulary):
|
|
482
|
+
|
|
483
|
+
| Harness | Event | status |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| Claude Code | `UserPromptSubmit`, `PostToolUse` | `working` |
|
|
486
|
+
| Claude Code | `Stop`, `Notification` (`idle_prompt`) | `needs_input` |
|
|
487
|
+
| Claude Code | `PermissionRequest`, `Notification` (`permission_prompt`) | `needs_approval` |
|
|
488
|
+
| Claude Code | `StopFailure` | `errored` |
|
|
489
|
+
| Claude Code | `SessionEnd` | `shell` |
|
|
490
|
+
| Codex | `SessionStart` | `idle` |
|
|
491
|
+
| Codex | `UserPromptSubmit`, `PostToolUse` | `working` |
|
|
492
|
+
| Codex | `PermissionRequest` | `needs_approval` |
|
|
493
|
+
| Codex | `Stop`, `notify` (turn complete) | `done` |
|
|
494
|
+
| Codex | `SessionEnd` | `shell` |
|
|
495
|
+
|
|
496
|
+
Verified event names (2026-10-04, this host): Claude Code 2.1.288's hook-event list compiled into its binary
|
|
497
|
+
(`PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, Notification, UserPromptSubmit, UserPromptExpansion,
|
|
498
|
+
SessionStart, SessionEnd, Stop, StopFailure, SubagentStart, SubagentStop, PreCompact, PostCompact, PreModelSwitch,
|
|
499
|
+
PostModelSwitch, PermissionRequest, PermissionDenied, …`); Codex 0.160.0's hook event variants in its binary
|
|
500
|
+
(`PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, SessionStart, SessionEnd, SubagentStart,
|
|
501
|
+
SubagentStop, Interrupt`, plus `UserPromptSubmit` and `Stop`), and the shape of a trusted project
|
|
502
|
+
`.codex/hooks.json` (`{"hooks": {Event: [{"matcher": null, "hooks": [{"type": "command", "command", "timeout"}]}]}}`).
|
|
503
|
+
Codex 0.160 has no `codex --help hooks`; the names come from the binary. Not yet proven on a live lane: that Codex
|
|
504
|
+
loads a USER-level `~/.codex/hooks.json` the way it loads a project's (the only hooks.json files measured here are
|
|
505
|
+
project files), and what a hook's parent process is under each harness (lane-signal skips one shell either way).
|
|
506
|
+
|
|
507
|
+
Known limit (DEF-1434): after you approve a permission prompt, the lane shows `needs_approval` until the approved
|
|
508
|
+
tool call returns (`PostToolUse`), because neither harness signals "approved". A long approved command therefore
|
|
509
|
+
reads as waiting for you until it finishes. A lane whose agent crashed loses its sidecar at once (dead pid); one
|
|
510
|
+
whose agent is alive but silent keeps the last word for up to an hour.
|
|
511
|
+
|
|
512
|
+
## Economy projection (opt-in)
|
|
513
|
+
|
|
514
|
+
`~/.local/state/lanes/economy.json` is what each lane costs and how hard the fleet is pushing the Throttle pool:
|
|
515
|
+
a fleet strip (overflow spend against the enforced daily cap, alert level, pressure phase and spill fraction,
|
|
516
|
+
effectively free accounts, the soonest 5 h reset, new upstream 429s since the last poll, paid and subscription
|
|
517
|
+
requests in flight, the Fable governor's sustainable rate) and, per lane, live figures from the daemon's
|
|
518
|
+
recent-request ring (model, paid share, $/h, tokens/min, cache-read ratio, downgrades, 429s, age of the last
|
|
519
|
+
request) plus today's totals folded from `proxy.log` (requests, paid requests, the four token classes — dollars only
|
|
520
|
+
as `list_usd_estimate`, which stays `null`; lanes keeps no price table).
|
|
521
|
+
|
|
522
|
+
**What the sidebar shows from it.** Every sidebar re-reads `economy.json` when its mtime changes (one `stat()` per
|
|
523
|
+
tick). Column 34 of a lane row is one tail cell (`⚠` starving, `↓` downgraded, `$` mostly paid; `?` — not observed —
|
|
524
|
+
beats all three). The strip row above the footer rule (panes of 18 rows or more) becomes the fleet economy line, parts
|
|
525
|
+
in priority order and whole parts dropped from the end to fit 34 cells:
|
|
526
|
+
|
|
527
|
+
| Part | From | Shown |
|
|
528
|
+
|---|---|---|
|
|
529
|
+
| `$8.5k/16k 53%` | `overflow_spend_today_usd` / `overflow_budget_usd`, `spent_fraction` | amber at or over `alert_level_pct` |
|
|
530
|
+
| `429 +4` | `upstream_429_delta` | only when > 0, amber |
|
|
531
|
+
| `F2 S1` | this workspace's lanes by `model`: `F` fable, `O` opus, `S` sonnet, `H` haiku, `G` gpt | tier letters live here and in the preview, never in a row |
|
|
532
|
+
| `spill 76%` | `spill_fraction` | amber unless `pressure_phase` is `closed` |
|
|
533
|
+
| `free 3/19` | `effective_free` / `accounts` | |
|
|
534
|
+
| `reset 2h` | `soonest_reset_5h` | only while in the future |
|
|
535
|
+
|
|
536
|
+
A leading `?` and an all-dim line mean the figures are frozen: the poller kept its last fleet strip because `/status`
|
|
537
|
+
failed (`"stale": true`), or the poller stopped — the poller rewrites `economy.json` at least every 60 s, and a sidebar
|
|
538
|
+
treats a document more than 180 s old by its own clock as frozen (DEF-1447). A lane's tail cell follows the same rule:
|
|
539
|
+
a `⚠` or `↓` drawn dim (not amber) is the last known figure, not a live alarm — the poller kept that lane's record
|
|
540
|
+
because `/sessions` failed (`"stale": true` on the lane), or the whole document is frozen. With
|
|
541
|
+
`LANES_THROTTLE_CONTROL=none` every fleet figure is `null`: the strip row then shows fleet attention exactly as without
|
|
542
|
+
a poller, and no row carries an economy cell (the day totals carry no live figure).
|
|
543
|
+
|
|
544
|
+
**Operator gate.** `lanes-economy.service` sends a GET to the Throttle daemon's control port (`/status` and
|
|
545
|
+
`/sessions`, default `http://127.0.0.1:19181`) every 10 s. It is installed with the other units and enabled by
|
|
546
|
+
nothing. Turning it on is your call.
|
|
547
|
+
|
|
548
|
+
**Where it runs — read this before enabling.** The unit is a HOST user unit (the server installer renders it beside
|
|
549
|
+
`lanes-statusd`); the daemon, its control port and its log normally live inside a workspace container. Neither
|
|
550
|
+
default reaches them from the host:
|
|
551
|
+
|
|
552
|
+
- The control URL is resolved in the HOST's network namespace. A daemon bound to `127.0.0.1` inside its container
|
|
553
|
+
(`[server] host = "127.0.0.1"`, measured 2026-10-04 in the `work` workspace container) is not reachable from the host at all, and
|
|
554
|
+
`http://127.0.0.1:19181` on the host is a different loopback. Until the control port is reachable from the host —
|
|
555
|
+
an operator change to the daemon's bind or forwarding, outside lanes — set `LANES_THROTTLE_CONTROL=none`: the
|
|
556
|
+
fleet strip and live per-lane figures stay `null`, the log fold still works (DEF-1444, open).
|
|
557
|
+
- The log lives in the container's home. Set `LANES_THROTTLE_CONTAINER=<container name>`: the poller reads
|
|
558
|
+
`/home/coder/.local/share/throttle` through that container's home volume as `lanes sync` recorded it in
|
|
559
|
+
`~/.local/state/lanes/containers.json` (the container must be on a board). The host user's own
|
|
560
|
+
`~/.local/share/throttle` is the default only for a poller that runs where the daemon runs.
|
|
561
|
+
|
|
562
|
+
Check what the poller can see before you enable it — `--once` names every source it could not read on stderr
|
|
563
|
+
(`connection refused`, `timed out`, `HTTP 401`, `log dir missing: …`, `log dir unreadable: …`,
|
|
564
|
+
`container … is not in containers.json`). `log dir unreadable` on a host unit usually means the container's home
|
|
565
|
+
volume sits under the docker data root (`docker info -f '{{.DockerRootDir}}'`: `/var/lib/docker` by default,
|
|
566
|
+
`/data/docker` on ceobox, where the workspace homes are named volumes at `<root>/volumes/coder-<id>-home/_data`),
|
|
567
|
+
which only root normally traverses. The log half then needs the same operator decision as the control half
|
|
568
|
+
(DEF-1444): point `LANES_THROTTLE_LOG_DIR` at a directory this user can traverse, or run the poller where the daemon
|
|
569
|
+
runs:
|
|
570
|
+
|
|
571
|
+
```sh
|
|
572
|
+
LANES_THROTTLE_CONTROL=none LANES_THROTTLE_CONTAINER=<container> lanes economy --once --dry-run # writes nothing
|
|
573
|
+
systemctl --user edit lanes-economy.service # put the same two settings in the drop-in ([Service] Environment=)
|
|
574
|
+
systemctl --user enable --now lanes-economy.service
|
|
575
|
+
journalctl --user -u lanes-economy.service # one line each time a source goes down or comes back
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Before you enable it, settle DEF-1479 (the lane-stable trace's fairness consequence) if any lane was created with
|
|
579
|
+
the identity recipe; lanes without it are joined to their requests by `agent.session_id` alone.
|
|
580
|
+
|
|
581
|
+
Settings (environment of the installer — `install/services.py` renders them into this unit only — or a
|
|
582
|
+
`systemctl --user edit lanes-economy.service` drop-in):
|
|
583
|
+
|
|
584
|
+
| Variable | Default | Meaning |
|
|
585
|
+
|---|---|---|
|
|
586
|
+
| `LANES_THROTTLE_CONTROL` | `http://127.0.0.1:19181` | control base URL, resolved in the poller's network namespace (see above); `none` sends no request at all (the log fold still runs) |
|
|
587
|
+
| `LANES_THROTTLE_TOKEN_FILE` | unset | file holding a bearer; a loopback daemon without `remote_key` needs none. Never written to `economy.json` or the cursor |
|
|
588
|
+
| `LANES_THROTTLE_CONTAINER` | unset | the container whose home volume holds the daemon's log; the log dir becomes its `/home/coder/.local/share/throttle` (via `containers.json`) |
|
|
589
|
+
| `LANES_THROTTLE_LOG_DIR` | `~/.local/share/throttle` | where `proxy.log` and `proxy.log.<day>` are read (read only); wins over `LANES_THROTTLE_CONTAINER` |
|
|
590
|
+
|
|
591
|
+
Behaviour you can rely on: GET only, 3 s timeout, no proxy from the environment, no redirects followed (a redirect
|
|
592
|
+
would carry the bearer elsewhere). A failed `/status` keeps the last fleet strip with `"stale": true`; a failed
|
|
593
|
+
`/sessions` keeps each lane's last live figures with `"stale": true`; `source` says which of status, sessions and
|
|
594
|
+
log answered, and the journal (stderr) says why one did not — a line when a source's failure class changes, `ok`
|
|
595
|
+
when it recovers; the reason is never written to `economy.json`. `economy.json` is rewritten only when something other than `at` changed. The fold cursor
|
|
596
|
+
(`economy.cursor.json`) reads only bytes appended since the last poll and keeps ten days. Deleting both files is safe:
|
|
597
|
+
the next poll re-folds today. For a look without the daemon: `LANES_THROTTLE_CONTROL=none lanes economy --once
|
|
598
|
+
--dry-run --status-file status.json --sessions-file sessions.json` with saved documents.
|
|
599
|
+
|
|
600
|
+
## Push notifications (opt-in)
|
|
601
|
+
|
|
602
|
+
`lanes-notify.service` sends one push to an [ntfy](https://ntfy.sh) topic when a lane's agent ENTERS needs-you —
|
|
603
|
+
`agent.status` becomes `needs_input` (`Needs you`), `needs_approval` (`Needs approval`) or `errored` (`Error`), the
|
|
604
|
+
same set the sidebar's Needs you section and its OSC 9 use. It is the away-from-every-terminal half of the sidebar's
|
|
605
|
+
OSC 9 ("Title and notifications" above): it reads only `~/.local/state/lanes/status.json` (checked every 3 s,
|
|
606
|
+
re-read when its mtime changes) and `registry.json` for lane names; it never touches a lane, a session or the
|
|
607
|
+
Throttle daemon.
|
|
608
|
+
|
|
609
|
+
What a push says: `<lane name>: <agent task> — <status>` and, on a second line, `lanes-launch lane <id>` (the
|
|
610
|
+
command that opens that lane from a client). No client registers a URL handler for that command, so a push is
|
|
611
|
+
tappable only when you set `action_url` (below).
|
|
612
|
+
|
|
613
|
+
When nothing is sent:
|
|
614
|
+
|
|
615
|
+
- the status did not change, or the lane left needs-you (a later return is news again); a change from
|
|
616
|
+
`Needs you` to `Needs approval` is news;
|
|
617
|
+
- the service just started: lanes already waiting when it (re)starts are not announced (no burst on restart —
|
|
618
|
+
unlike a starting sidebar's OSC 9);
|
|
619
|
+
- a desk device was present within `quiet_when_device_active_s` (default 120 s): a `lanes attach` from any device
|
|
620
|
+
(`clients/<device>.<client>.pid`), or a lane opened from the sidebar of a desk device
|
|
621
|
+
(`clients/<device>.seen.json`, counted only for a device that has a `.pid` — one `lanes attach` has run for —
|
|
622
|
+
and whose `devices/<device>.json` is not `mobile: true`) that recently. Typing in a lane refreshes neither, so after two minutes at the keyboard without attaching or
|
|
623
|
+
opening a lane the pushes resume (DEF-1451). A suppressed transition is dropped, not sent later. Phone presence
|
|
624
|
+
is decided by the device's mobile layout, not by how it attached: a Safari phone writes no `.pid`, and an
|
|
625
|
+
SSH-only phone that ran `lanes attach phone --mobile` (below) is still a mobile device, so opening a lane from
|
|
626
|
+
the phone's picker never mutes pushes — you keep getting them while you are away from every desk (DEF-1452).
|
|
627
|
+
Only a phone's own `lanes attach` in the last two minutes counts, like any attach.
|
|
628
|
+
|
|
629
|
+
A failed send (server down, `HTTP 429`, no network) is one journal line naming the lane and the failure class — the
|
|
630
|
+
topic and the token are never logged — and is not retried.
|
|
631
|
+
|
|
632
|
+
Config `~/.config/lanes/notify.conf` (`LANES_NOTIFY_CONF` in the server installer's environment renders another
|
|
633
|
+
path into this unit only), `key=value` lines, `#` comments, re-read on change:
|
|
634
|
+
|
|
635
|
+
| Key | Default | Meaning |
|
|
636
|
+
|---|---|---|
|
|
637
|
+
| `url` | required | the ntfy server, `https://…` (self-hosted or `https://ntfy.sh`) |
|
|
638
|
+
| `topic` | required | `[A-Za-z0-9_-]{1,64}`; on a public server the topic name is the only secret — pick a long random one |
|
|
639
|
+
| `token_file` | unset | a file holding an ntfy access token (sent as a bearer). An unreadable file disables sending rather than sending unauthenticated. Keep the token in that file (`chmod 600`), never in `notify.conf` |
|
|
640
|
+
| `quiet_when_device_active_s` | `120` | presence window above; `0` never suppresses |
|
|
641
|
+
| `action_url` | unset | a URL template over `{lane}` (the lane id, URL-quoted), e.g. a page of yours that opens it; becomes the push's tap target and an **Open lane** button |
|
|
642
|
+
|
|
643
|
+
Without a valid `url` and `topic` the service only tracks transitions and sends nothing. Turn it on:
|
|
644
|
+
|
|
645
|
+
```sh
|
|
646
|
+
$EDITOR ~/.config/lanes/notify.conf # url=, topic=, token_file= …
|
|
647
|
+
python3 ~/.local/share/lanes/current/libexec/notify.py --test # one test push; exit 1 = not sent
|
|
648
|
+
systemctl --user enable --now lanes-notify.service
|
|
649
|
+
journalctl --user -u lanes-notify.service # send failures, one line each
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Operator gate.** The unit is installed with the others and enabled by nothing: it sends lane names and agent
|
|
653
|
+
task titles to the server you configure. Not yet run against any ntfy server or phone (unit-proven only).
|
|
654
|
+
|
|
655
|
+
**Not part of this:** answering a `PermissionRequest`-held approval from the phone. A push only says the lane needs
|
|
656
|
+
you; approving from the notification needs a request/response channel back into the hook that holds the prompt — a
|
|
657
|
+
follow-up design.
|
|
658
|
+
|
|
257
659
|
## Throttle Studio as a client
|
|
258
660
|
|
|
259
661
|
Throttle Studio (`throttle-rc studio host` in a workspace container, the `/workspaces` view of the Throttle console)
|
|
@@ -316,14 +718,14 @@ already ignore it. Afterwards the in-container path is on your clipboard and in
|
|
|
316
718
|
|
|
317
719
|
| Way | How |
|
|
318
720
|
|---|---|
|
|
319
|
-
| Upload
|
|
721
|
+
| Upload strip in the sidebar | The one row above the footer rule, ` ↑ uploads · click, then drop (U)`. Click it (or press `U` in the sidebar, `Ctrl-Alt-u` from anywhere in the tab) — it turns into ` ↓ DROP FILES NOW · anywhere · Esc` — then **drop the files anywhere on the window**. The file goes to the board of that tab and its path is typed into the lane, ready for the agent. `Esc` cancels. While it sends the strip says ` ↑ sending 2 files · 12.4 MiB` and repaints only when the helper reports progress (no spinner, no fast tick); the result stays 8 s as ` ✓ name, name` or ` ✗ reason`. |
|
|
320
722
|
| Watch folder | `~/Lanes Uploads/` → the board your view shows; `~/Lanes Uploads/<board>/` → that board. Sent files move to `.sent/<date>/`. Point CleanShot's save folder here for instant screenshot upload. |
|
|
321
723
|
| Command | `lanes-upload [-w <board>] [--paste] <files…>` · `lanes-upload --clipboard [--paste]` (clipboard image or copied files) |
|
|
322
724
|
| macOS droplet | `~/Applications/Lanes Upload.app`: drop files on it (Dock); double-click uploads the clipboard screenshot |
|
|
323
725
|
| anarchy | `Super+Shift+U` uploads the clipboard and types the path into the lane |
|
|
324
726
|
|
|
325
727
|
Why click first: a terminal delivers a drop as a *paste of the local path* into the FOCUSED pane (not the pane under
|
|
326
|
-
the pointer), so the
|
|
728
|
+
the pointer), so the strip has to hold the focus when you let go. And the server only ever receives that path string:
|
|
327
729
|
the file itself is sent by the **upload helper** that `lanes-launch` runs on your machine. The helper is the security
|
|
328
730
|
boundary — it reads only from folders listed in `~/.config/lanes/upload-allow` (defaults: Desktop, Downloads,
|
|
329
731
|
Pictures, Documents, Lanes Uploads, temp folders; macOS asks once per new folder; `lanes-upload --allow <dir>`), and
|
|
@@ -356,6 +758,34 @@ On the phone, connect Tailscale and open the printed URL. Paste the token into t
|
|
|
356
758
|
when tapping **Type here** or another text input; swipes, live updates and reconnects leave it closed.
|
|
357
759
|
**More → Text size** remembers your preferred terminal font. The phone has its own view selection of the same agents.
|
|
358
760
|
|
|
761
|
+
The lane picker shows three rows per lane: the agent glyph and the name (`›` before the lane this phone views), then
|
|
762
|
+
what the agent needs and since when with its harness (`Needs you · 25m · claude`; a shell says `Quiet · 2h`), then the
|
|
763
|
+
agent's task. Its first line counts the lanes that need you (`◆1 need approval · ◇2 need you · ▸6 working`). The same
|
|
764
|
+
counts come from the picker pane's title (see "Title and notifications" above): the header subtitle on the picker,
|
|
765
|
+
the page title, a number on the **Lanes** button for lanes needing approval or input, and `◆ 1 · ◇ 2` on each
|
|
766
|
+
workspace card. Nothing is fetched or polled for them. Not yet confirmed (DEF-1450): until plan Task 26 (2026-10-04)
|
|
767
|
+
the phone's picker pane had the fixed layout name `Lanes`, which Zellij reports instead of the sidebar's title, so the
|
|
768
|
+
phone showed no counts. Task 26 removed that name from the generated layout, which reaches a phone only when its
|
|
769
|
+
session is laid out again (`lanes rebuild <device>`; see the device check below); whether the title
|
|
770
|
+
now reaches the phone is still unchecked in a browser — until then only the picker's own first line is known to
|
|
771
|
+
show them (DEF-1450).
|
|
772
|
+
|
|
773
|
+
The phone's light/dark toggle repaints the whole terminal, not only the shell: the page applies its own sixteen-colour
|
|
774
|
+
palette on attach, on every toggle and after every Zellij config reload (DEF-1413; not yet looked at on a device).
|
|
775
|
+
|
|
776
|
+
On a lane's own tab a row of quick keys sits above the dock: `⏎ y n Esc ^C ↑ ↓`. A tap writes the key to the lane
|
|
777
|
+
without opening the keyboard. `n` and `^C` write only after you hold them for 400 ms; a short tap says
|
|
778
|
+
"Hold to send". With the keyboard open the row stays above the keyboard's modifier bar. The filter bar on the picker
|
|
779
|
+
sends its text as UTF-8, so a filter like `ø` matches.
|
|
780
|
+
|
|
781
|
+
Device check (plan Task 13 step 3, not yet run): on a real iPhone in Safari and from the Home Screen, tap each quick
|
|
782
|
+
key on a lane tab and confirm that the keyboard does NOT open, that `y`/`⏎`/arrows arrive in the lane, that a short tap
|
|
783
|
+
on `n`/`^C` sends nothing and a hold sends it, and (plan Task 26 has landed; DEF-1450 waits on this) that the Workspaces
|
|
784
|
+
sheet shows each picker's counts. A phone device created before Task 26 keeps the old picker pane (`upgrade-session`
|
|
785
|
+
does not re-lay-out a tab): run `lanes rebuild <device>` first (`lanes doctor` must not list it under DEF-1450), or no
|
|
786
|
+
counts appear and the check says nothing about the title path. Record the
|
|
787
|
+
device, iOS version and result here. The unit suite covers only the picker's text, not the browser.
|
|
788
|
+
|
|
359
789
|
Tailscale Serve forwards HTTPS 8443 to `lanes-mobile.service` on `127.0.0.1:8083`. The separate unprivileged
|
|
360
790
|
Caddy serves `web/` and forwards native authentication/assets/WebSockets to Zellij on `127.0.0.1:8082`.
|
|
361
791
|
The Mac is a client; serving happens on the workspace host. See [frontend maintenance](../web/README.md).
|
|
@@ -365,6 +795,23 @@ a real client before attaching; visible views accept 20 columns / 4 rows for the
|
|
|
365
795
|
Revoke a named token with `~/.local/share/lanes/bin/zellij-0.45.1 web --revoke-token NAME`.
|
|
366
796
|
An SSH-only phone client can run `lanes attach phone --mobile` instead.
|
|
367
797
|
|
|
798
|
+
Laptop and tablet browsers: see the [installation guide](INSTALL.md#laptop-and-tablet-browser). Troubleshooting:
|
|
799
|
+
|
|
800
|
+
- **A tablet landed on the lane-picker layout** (the device was made for a phone, for example by
|
|
801
|
+
`enable-phone.sh`'s default): give the tablet its own desktop-layout view with `lanes web <device> --desktop`
|
|
802
|
+
under a new device name. The old view stays as it is; a running view is never rebuilt into the other layout,
|
|
803
|
+
and an explicit `--desktop` against it is refused instead. Without a flag, `lanes web <device>` reuses
|
|
804
|
+
whatever layout that device already has. A tablet shows the whole tab (sidebar beside the lanes): the pinned
|
|
805
|
+
client opens its mobile UI in single pane on every load, and `web/browser.js` asks once per load for the full
|
|
806
|
+
view with the client's own preference. **More → Single pane** still switches to one pane for that load.
|
|
807
|
+
- **A laptop shows no Lanes chip**: the chip appears after sign-in, when the first workspace state arrives. A
|
|
808
|
+
reload after an upgrade picks up the new `mobile.js` and `browser.js` (both served with `no-cache`; `mobile.js`
|
|
809
|
+
appends `browser.js`/`browser.css` only on a tablet or desktop, so a phone's network panel never lists them).
|
|
810
|
+
The chip keeps the keyboard focus in the terminal; if typing stops after using a picker, Escape in the bar or a
|
|
811
|
+
click in the terminal returns it.
|
|
812
|
+
- **`lanes web` printed a `127.0.0.1` URL**: the host has no Tailscale name. Connect Tailscale on the host;
|
|
813
|
+
there is no public fallback.
|
|
814
|
+
|
|
368
815
|
## Safety rules baked into the code (do not remove)
|
|
369
816
|
|
|
370
817
|
- tmux is always called with `-N` (`attach-session`, `new-session` and `start-server` on a missing socket start
|
|
@@ -374,12 +821,48 @@ An SSH-only phone client can run `lanes attach phone --mobile` instead.
|
|
|
374
821
|
- pty-name collision guard: the lanes' `client-attached` hook detaches a client whose tty *name* equals the
|
|
375
822
|
lane's `pane_tty`; host and container pty numbers collide, so the holder re-wraps itself on a fresh pty.
|
|
376
823
|
- Views attach only from a pane zellij has really sized (unviewed tabs are 14x49) and only after a first client.
|
|
377
|
-
- The
|
|
378
|
-
with a single left,
|
|
824
|
+
- The two binds in `zellij/config.kdl` that still type into the sidebar (`Ctrl-Alt-/`, `Ctrl-Alt-u`) walk
|
|
825
|
+
`MoveFocus "left"` once per possible viewport before `WriteChars`: with a single left, a key pressed in the second
|
|
826
|
+
viewport typed into a lane. `Ctrl-Alt-↓/↑` type nothing since plan Task 19 (`lanes next|prev --focused`).
|
|
379
827
|
- Tests: `python3 -m unittest discover -s tests/unit` needs no server (runs on the Mac) and carries the harness's
|
|
380
828
|
pure-function pins — the viewer rule, the client-size bucketing, the status keying, the shared-socket
|
|
381
829
|
attribution and the zmx word (`tests/unit/test_alignment.py`). Live suites, on a QUIET host only:
|
|
382
830
|
`tests/guards.py`, `tests/view.py` (incl. the wide-view key walk), `tests/upload_box.py`,
|
|
383
|
-
`tests/smoke_real.py` (read-only, real lanes), `tests/latency.py` (from a client
|
|
831
|
+
`tests/smoke_real.py` (read-only, real lanes), `tests/latency.py` (from a client; `tests/latency.py --switch
|
|
832
|
+
[samples]` ON the server measures `switch` = SIGUSR1 to view.sh → first byte of the new lane, with a throwaway
|
|
833
|
+
view.sh on a temp `LANES_STATE` and two sessions on `-L lanes_test`, run ad hoc). All use disposable lanes except
|
|
384
834
|
the explicitly read-only smoke test; every one refuses to start on a busy host (exit 75) and removes its lanes and
|
|
385
835
|
sessions when it is killed with SIGTERM / SIGHUP.
|
|
836
|
+
|
|
837
|
+
## Workspace catalog propagation
|
|
838
|
+
|
|
839
|
+
Register and rename workspaces in the host site catalog. Device-local copies are
|
|
840
|
+
not a second authority. `lanes sync` publishes the registry and reconciles all
|
|
841
|
+
known live devices; `lanes upgrade-session mac anarchy phone` performs only the
|
|
842
|
+
view reconciliation. Device metadata chooses desktop or mobile layout. Missing
|
|
843
|
+
sessions are recreated by `lanes ensure` with that same catalog.
|
|
844
|
+
For an existing session with no attached client, additions wait until a client
|
|
845
|
+
connects and the next ensure tick. Zellij 0.45.1 otherwise acknowledges a new
|
|
846
|
+
background tab without spawning its panes. Names and ordering can still sync.
|
|
847
|
+
|
|
848
|
+
Reconciliation identifies managed tabs by the workspace IDs in their launched
|
|
849
|
+
sidebar/view commands, then adds or renames tabs and orders them using Zellij's
|
|
850
|
+
stable tab IDs. It never closes tabs, restarts agents or changes client focus.
|
|
851
|
+
An unknown session/pane response or an ambiguous identity refuses changes for that
|
|
852
|
+
device and reports an error. Another device can still be updated. An unmanaged
|
|
853
|
+
name collision is preserved for explicit inspection; it is never adopted by title.
|
|
854
|
+
Per-device file locks serialize a sync with the ensure timer.
|
|
855
|
+
SSH split tabs match the host, absolute helper and declared pane ID together.
|
|
856
|
+
Different pane IDs may share one helper; overlapping endpoint identities refuse
|
|
857
|
+
before tab mutations and require an unambiguous catalog assignment.
|
|
858
|
+
|
|
859
|
+
For a connection wait, the Syncing screen owns and drains terminal input until
|
|
860
|
+
the attach client produces output. Terminal modes and sizes are restored when a
|
|
861
|
+
viewer ends; TERM/HUP stop only that attach process group. The persistent lane
|
|
862
|
+
continues independently. Connection diagnostics remain in the view's `.err` file.
|
|
863
|
+
Existing views pick up the connection helper on their next attach; restart only
|
|
864
|
+
the relevant view panes when activating a changed supervisor, never the lane.
|
|
865
|
+
|
|
866
|
+
Catalog fields are additive to site schema 1. Back up the catalog and running
|
|
867
|
+
checkout before updating the server. Restore those files to roll back; do not
|
|
868
|
+
rebuild device sessions as a rollback shortcut. Previously added tabs remain open.
|