@vibelord/lanes 1.0.2 → 1.0.3
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/VERSION +1 -1
- package/payload/lanes/bin/lanes +21 -8
- package/payload/lanes/container/env.sh +1 -1
- package/payload/lanes/container/lane +5 -5
- package/payload/lanes/container/lane-signal +4 -4
- package/payload/lanes/docs/ARCHITECTURE.md +14 -7
- package/payload/lanes/docs/DESIGN.md +3 -3
- package/payload/lanes/docs/INSTALL.md +37 -2
- package/payload/lanes/docs/RUNBOOK.md +52 -23
- package/payload/lanes/docs/SHARING.md +239 -4
- package/payload/lanes/holders.lock +10 -0
- package/payload/lanes/install/enable-studio-host.sh +14 -0
- package/payload/lanes/install/host-install.sh +5 -2
- package/payload/lanes/install/services.py +101 -4
- package/payload/lanes/install.sh +1 -1
- package/payload/lanes/libexec/agent_state.py +1 -1
- package/payload/lanes/libexec/cursor_sync.py +2 -2
- package/payload/lanes/libexec/economy_poll.py +3 -3
- package/payload/lanes/libexec/lanes_lib.py +1 -1
- package/payload/lanes/libexec/notify.py +3 -3
- package/payload/lanes/libexec/registrations.py +1 -1
- package/payload/lanes/libexec/session_actions.py +1 -1
- package/payload/lanes/libexec/sidebar.py +7 -7
- package/payload/lanes/libexec/statusd.py +25 -10
- package/payload/lanes/libexec/studio_host.py +1608 -0
- package/payload/lanes/systemd/lanes-economy.service +1 -1
- package/payload/lanes/systemd/lanes-studio-host-enrol.service +11 -0
- package/payload/lanes/systemd/lanes-studio-host-enrol.timer +10 -0
- package/payload/lanes/systemd/lanes-studio-host.service +37 -0
- package/payload/lanes/web/README.md +2 -2
- package/payload/lanes/zellij/config.kdl +1 -1
- package/payload/manifest.json +4 -3
package/package.json
CHANGED
package/payload/lanes/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.3
|
package/payload/lanes/bin/lanes
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
attach-only, missing script) with the canonical one whose `tmux new -e` flags give the
|
|
37
37
|
lane LANE_ID/LANE_WS and the lanes/<ws>/<id> trace; --identity adds those flags to a
|
|
38
38
|
working plain `tmux -L S new -ADs N [-c D] CMD` recipe, keeping CMD. The old recipe is
|
|
39
|
-
kept as createPrevious. Only these -e flags give a lane its identity (DEF-
|
|
39
|
+
kept as createPrevious. Only these -e flags give a lane its identity (DEF-1559)
|
|
40
40
|
lanes cursor-commit [ws…] [--push]
|
|
41
41
|
commit the normalized terminals.json on top of each repo's REMOTE branch tip and push it,
|
|
42
42
|
with git plumbing only (no working tree or index is touched). Dry-run without --push.
|
|
@@ -79,6 +79,15 @@
|
|
|
79
79
|
its last 300 lines (guarded tmux capture, resolved against its cwd, existing ones only),
|
|
80
80
|
newest first; one is shown at once, several give a numbered menu. glow (pinned in
|
|
81
81
|
holders.lock) in the device's dark/light theme, else less -R
|
|
82
|
+
lanes studio-host status [--json] [--check]
|
|
83
|
+
the co-located Throttle Studio Host (opt-in: install/enable-studio-host.sh): binary
|
|
84
|
+
digest, config mode, enrolment, units. Exit 0 only when it runs enrolled
|
|
85
|
+
lanes studio-host install [--from PATH [--sha256 HEX]]
|
|
86
|
+
select the Host binary: the holders.lock opt-in pin, or a locally built file (both
|
|
87
|
+
sha256-verified); restarts nothing
|
|
88
|
+
lanes studio-host re-key [--yes] [--dry-run]
|
|
89
|
+
after the owner revoked this machine: stop the Host, remove the machine key and its
|
|
90
|
+
dead enrolment, publish a NEW public key for the next deploy. Nothing without --yes
|
|
82
91
|
lanes doctor full health check (every external call is bounded; includes the pressure summary), with
|
|
83
92
|
the lanes per create-recipe class (idempotent / attach-only / null / external /
|
|
84
93
|
retired-path), lane identity, numeric session names, crowded `default` sockets and
|
|
@@ -257,7 +266,7 @@ def mobile_workspace_tab(device, workspace, picker):
|
|
|
257
266
|
command = str(L.REPO / 'libexec' / ('sidebar' if picker else 'view.sh'))
|
|
258
267
|
args = f'{q(device)} {q(workspace["id"])}' + ('' if picker else ' "main" "--mobile"')
|
|
259
268
|
# The picker's sidebar pane has NO layout name: Zellij reports a non-empty pane name instead of the pane's own
|
|
260
|
-
# OSC 2 title, and that title (the attention counts) is what the phone reads (DEF-
|
|
269
|
+
# OSC 2 title, and that title (the attention counts) is what the phone reads (DEF-1531).
|
|
261
270
|
name = '' if picker else f' name={q(workspace["title"])}'
|
|
262
271
|
return [f' tab name={q(title)} {{',
|
|
263
272
|
f' pane borderless=true{name} command={q(command)} {{',
|
|
@@ -268,7 +277,7 @@ def stale_picker_layouts():
|
|
|
268
277
|
"""Phone devices whose stored layout still names the picker's sidebar pane (the pre-Task 26 `name="Lanes"`).
|
|
269
278
|
|
|
270
279
|
`upgrade-session` / `ensure` only add missing tabs and rename titles: a picker tab created from an older layout
|
|
271
|
-
keeps its pane name, which Zellij reports instead of the OSC 2 counts, until `lanes rebuild <device>` (DEF-
|
|
280
|
+
keeps its pane name, which Zellij reports instead of the OSC 2 counts, until `lanes rebuild <device>` (DEF-1531).
|
|
272
281
|
`create_session` is the only writer of layouts/<device>.kdl, so the file is the layout the session was built from.
|
|
273
282
|
"""
|
|
274
283
|
sidebar = str(L.REPO / 'libexec' / 'sidebar')
|
|
@@ -1214,7 +1223,7 @@ def cmd_gc(_args):
|
|
|
1214
1223
|
# shells) and `zmx` into `lanes` (statusd.observe/build_payload; pinned by test_statusd
|
|
1215
1224
|
# Cycle.test_every_published_row_id_is_also_a_lanes_key), so `lanes` alone covers them today. The union is
|
|
1216
1225
|
# defensive: the sweep does not rest on that invariant if the payload is ever decoupled. The mtime is no
|
|
1217
|
-
# liveness signal — make_entry rewrites an unregistered session's file only when it changes (DEF-
|
|
1226
|
+
# liveness signal — make_entry rewrites an unregistered session's file only when it changes (DEF-1537).
|
|
1218
1227
|
known = set(status.get('lanes') or {})
|
|
1219
1228
|
for key in ('unregistered', 'zmx'):
|
|
1220
1229
|
for entries in (status.get(key) or {}).values():
|
|
@@ -1234,6 +1243,10 @@ def cmd_guard(args):
|
|
|
1234
1243
|
return libexec_module('guard').run(args)
|
|
1235
1244
|
|
|
1236
1245
|
|
|
1246
|
+
def cmd_studio_host(args):
|
|
1247
|
+
return libexec_module('studio_host').main(args) # verbs, refusals and exit codes are studio_host's
|
|
1248
|
+
|
|
1249
|
+
|
|
1237
1250
|
def libexec_module(name):
|
|
1238
1251
|
try:
|
|
1239
1252
|
return __import__(name)
|
|
@@ -1295,7 +1308,7 @@ DEFAULT_SOCKET_LANES = 4
|
|
|
1295
1308
|
|
|
1296
1309
|
def recipe_report(registry):
|
|
1297
1310
|
"""(info lines, warnings) of the registry's tmux lanes for `lanes doctor` (plan Task 18): lanes per create-recipe
|
|
1298
|
-
class (lanes_lib.classify_recipe), how many create with their identity (-e LANE_ID, DEF-
|
|
1311
|
+
class (lanes_lib.classify_recipe), how many create with their identity (-e LANE_ID, DEF-1559), lanes bound to a
|
|
1299
1312
|
numeric session name, and crowded `default` sockets. Reads registry.json only: no tmux, no container."""
|
|
1300
1313
|
counts, identity, total = dict.fromkeys(L.RECIPE_CLASSES, 0), 0, 0
|
|
1301
1314
|
broken, numeric, on_default = [], [], {}
|
|
@@ -1499,7 +1512,7 @@ def cmd_doctor(_args):
|
|
|
1499
1512
|
except (OSError, L.LanesError) as error:
|
|
1500
1513
|
check(False, 'zellij reachable', str(error))
|
|
1501
1514
|
stale_pickers = stale_picker_layouts()
|
|
1502
|
-
check(not stale_pickers, 'phone picker panes report their own title (DEF-
|
|
1515
|
+
check(not stale_pickers, 'phone picker panes report their own title (DEF-1531)',
|
|
1503
1516
|
'built from a layout that names the picker pane, so the phone shows no counts; upgrade-session does not '
|
|
1504
1517
|
're-lay-out a tab — run ' + '; '.join(f'lanes rebuild {d}' for d in stale_pickers))
|
|
1505
1518
|
print('PRESSURE')
|
|
@@ -1545,7 +1558,7 @@ def cmd_rename(args):
|
|
|
1545
1558
|
if not lane:
|
|
1546
1559
|
raise L.LanesError(f'Unknown lane: {lane_id}')
|
|
1547
1560
|
# An adopted session (Add to workspace / `lanes register`) takes its name from registered-sessions.json,
|
|
1548
|
-
# which is the one file renamed here; its id and binding never change (DEF-
|
|
1561
|
+
# which is the one file renamed here; its id and binding never change (DEF-1556).
|
|
1549
1562
|
if lane.get('registered_session'):
|
|
1550
1563
|
import registrations
|
|
1551
1564
|
registrations.rename(lane_id, new_name)
|
|
@@ -1800,7 +1813,7 @@ COMMANDS = {'rebuilt': cmd_rebuilt, 'hello': cmd_hello, 'client-config': cmd_cli
|
|
|
1800
1813
|
'prepare': cmd_prepare, 'web': cmd_web, 'attach': cmd_attach, 'rebuild': cmd_rebuild, 'ensure': cmd_ensure, 'invariant': cmd_invariant,
|
|
1801
1814
|
'doctor': cmd_doctor, 'pressure': cmd_pressure, 'sockets': cmd_sockets, 'tmp': cmd_tmp,
|
|
1802
1815
|
'guard': cmd_guard, 'rename': cmd_rename, 'register': cmd_register, 'hooks': cmd_hooks, 'economy': cmd_economy,
|
|
1803
|
-
'preview': cmd_preview, 'device': cmd_device, 'next': cmd_next, 'prev': cmd_prev}
|
|
1816
|
+
'preview': cmd_preview, 'device': cmd_device, 'next': cmd_next, 'prev': cmd_prev, 'studio-host': cmd_studio_host}
|
|
1804
1817
|
|
|
1805
1818
|
if __name__ == '__main__':
|
|
1806
1819
|
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# (<ws>--<slug>), LANE_WS (<ws>), THROTTLE_TRACE and ANTHROPIC_CUSTOM_HEADERS ('x-throttle-trace: lanes/<ws>/<id>';
|
|
15
15
|
# trace charset [A-Za-z0-9._-/], at most 256). Never export them from a profile or a client: the client that starts
|
|
16
16
|
# a shared board socket's tmux server copies its env into the server's GLOBAL env, which every later session there
|
|
17
|
-
# without its own -e inherits (DEF-
|
|
17
|
+
# without its own -e inherits (DEF-1559) — so `lane open` strips them. A running lane gains its identity only when it
|
|
18
18
|
# is next created by a recipe carrying -e; a legacy recipe without -e gives none (`lanes cursor-sync --identity`).
|
|
19
19
|
if [ -z "${LANES_TMP_OPTOUT+set}" ] && [ -d "${LANES_TMP_DIR:-/home/coder/.tmp}" ] \
|
|
20
20
|
&& [ -w "${LANES_TMP_DIR:-/home/coder/.tmp}" ] && [ -x "${LANES_TMP_DIR:-/home/coder/.tmp}" ]; then
|
|
@@ -38,7 +38,7 @@ ROOT = Path(os.environ.get('LANE_ROOT', '/home/coder/.lz'))
|
|
|
38
38
|
VERSION = os.environ.get('LANE_ZMX_VERSION', '0.8.1')
|
|
39
39
|
ZMX = ROOT / 'bin' / f'zmx-{VERSION}'
|
|
40
40
|
SOCKETS, META, STATE = ROOT / 's', ROOT / 'meta', ROOT / 'state'
|
|
41
|
-
HOOKS = STATE / 'hooks' # lane-signal's sidecars (never lane-run's phase records one directory up, DEF-
|
|
41
|
+
HOOKS = STATE / 'hooks' # lane-signal's sidecars (never lane-run's phase records one directory up, DEF-1572)
|
|
42
42
|
PROC = Path('/proc') # this container's pid namespace: the hook's agent pid is checked here
|
|
43
43
|
IDENT = re.compile(r'^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$')
|
|
44
44
|
STATUSES = ('working', 'needs_input', 'needs_approval', 'done', 'errored', 'shell', 'idle') # agent_state.STATUSES
|
|
@@ -167,7 +167,7 @@ IDENTITY = ('LANE_ID', 'LANE_WS', 'THROTTLE_TRACE') # + a lanes/ ANTHROPIC_CUS
|
|
|
167
167
|
def identity_free(environ, drop=('TMUX',)):
|
|
168
168
|
"""`environ` without any lane identity (lanes/docs/ARCHITECTURE.md "Lane identity"). Whatever tmux client STARTS
|
|
169
169
|
a socket's server hands its whole environment to the server's GLOBAL environment, and every later session on
|
|
170
|
-
that socket created without its own `-e` inherits it (MEASURED, tmux 3.4: DEF-
|
|
170
|
+
that socket created without its own `-e` inherits it (MEASURED, tmux 3.4: DEF-1559). Board sockets are shared by
|
|
171
171
|
many lanes, so a lane identity in the env of anything that may start one would be stamped on unrelated lanes.
|
|
172
172
|
Identity therefore reaches a lane's shell ONLY through `-e` in its create recipe (lanes_lib.canonical_recipe).
|
|
173
173
|
An operator's own ANTHROPIC_CUSTOM_HEADERS that is not a lane trace is theirs and stays."""
|
|
@@ -292,7 +292,7 @@ def cmd_open(args):
|
|
|
292
292
|
sys.exit(f"lane open: already inside tmux — refusing to nest. The lane is: {json.dumps(entry.get('tmux') or entry.get('zmx'))}")
|
|
293
293
|
# Nothing `lane open` execs carries a lane identity — neither this lane's nor one inherited from ANOTHER lane (an
|
|
294
294
|
# editor terminal started inside a lane): the recipe may be the first client on a shared board socket and would
|
|
295
|
-
# hand it to every later session there (DEF-
|
|
295
|
+
# hand it to every later session there (DEF-1559). The canonical recipe's own `-e` flags give the identity.
|
|
296
296
|
env = identity_free(os.environ, drop=('TMUX', 'TMUX_PANE'))
|
|
297
297
|
if entry.get('holder') == 'zmx':
|
|
298
298
|
name = entry['zmx']['name']
|
|
@@ -453,7 +453,7 @@ def cmd_status(args):
|
|
|
453
453
|
zname = zmx_name('status', name)
|
|
454
454
|
row = next((r for r in describe() if r['id'] == zname), None)
|
|
455
455
|
if row:
|
|
456
|
-
row['hook'] = hook_agent(zname) # lane-run's phase record stays `agent` (DEF-
|
|
456
|
+
row['hook'] = hook_agent(zname) # lane-run's phase record stays `agent` (DEF-1572)
|
|
457
457
|
print(json.dumps(row or {'id': name, 'running': False}, ensure_ascii=False, indent=1))
|
|
458
458
|
sys.exit(0 if row else 1)
|
|
459
459
|
|
|
@@ -519,7 +519,7 @@ def cmd_gc(args):
|
|
|
519
519
|
Refusal is read from the errno, never from tmux's prose: tmux 3.4 words ECONNREFUSED as `no server running on
|
|
520
520
|
<path>` and uses `error connecting to <path> (<strerror>)` only for OTHER errnos (measured 2026-10-04: EACCES →
|
|
521
521
|
`(Permission denied)`, ENOENT → `(No such file or directory)`), so a 'connection refused' text match never fired
|
|
522
|
-
(DEF-
|
|
522
|
+
(DEF-1540). When tmux fails, `socket_refused` connects to the same path once and only ConnectionRefusedError
|
|
523
523
|
counts."""
|
|
524
524
|
if args != ['--sockets']:
|
|
525
525
|
sys.exit(GC_USAGE)
|
|
@@ -5,7 +5,7 @@ lanes-statusd reads through /proc/<container init pid>/root and publishes as the
|
|
|
5
5
|
|
|
6
6
|
The sidecar has its own directory because `lane-run`, the root process of every zmx-held lane, writes its
|
|
7
7
|
`{phase, exit, at, pid}` record to ~/.lz/state/<zmx name>.json and exports LANE_ID=<zmx name> to the agent: a
|
|
8
|
-
sidecar at state/<lane id>.json would overwrite that record for the agent's whole life (DEF-
|
|
8
|
+
sidecar at state/<lane id>.json would overwrite that record for the agent's whole life (DEF-1572). Nothing but
|
|
9
9
|
this script writes state/hooks/, and lane-run never writes there.
|
|
10
10
|
|
|
11
11
|
{"v":1, "lane":"<id>", "harness":"claude|codex", "status":"<agent{} vocabulary>", "since":<epoch the status
|
|
@@ -16,10 +16,10 @@ It never fails the agent and never speaks: every path exits 0 and nothing reache
|
|
|
16
16
|
UserPromptSubmit/SessionStart hook's stdout becomes model context). An unresolvable lane, a status outside the
|
|
17
17
|
vocabulary or an unwritable state directory are silent no-ops.
|
|
18
18
|
|
|
19
|
-
Lane id: $LANE_ID — exported in a tmux lane only by a canonical/`--identity` recipe's `-e` flags (Task 7, DEF-
|
|
19
|
+
Lane id: $LANE_ID — exported in a tmux lane only by a canonical/`--identity` recipe's `-e` flags (Task 7, DEF-1559;
|
|
20
20
|
`lane open` never exports it), and in a zmx-held lane by `lane-run` as the zmx session name (which is NOT the
|
|
21
21
|
`<ws>--z-<name>` id lanes-statusd publishes for it, so statusd does not join that sidecar to the zmx lane yet:
|
|
22
|
-
DEF-
|
|
22
|
+
DEF-1573) — else the pane's tmux session (`tmux -S ${TMUX%%,*} display -p -t $TMUX_PANE '#S'`) looked up by (socket
|
|
23
23
|
name, session) in ~/.lz/registry.json, the projection `lanes sync` writes.
|
|
24
24
|
|
|
25
25
|
Agent pid: the parent process, or that parent's parent when the parent is a shell (Claude Code runs a hook command
|
|
@@ -141,7 +141,7 @@ def main(argv):
|
|
|
141
141
|
else 'codex')
|
|
142
142
|
pid, start = agent()
|
|
143
143
|
now = int(time.time())
|
|
144
|
-
state = os.path.join(root, 'state', 'hooks') # never state/<id>.json: lane-run's phase record (DEF-
|
|
144
|
+
state = os.path.join(root, 'state', 'hooks') # never state/<id>.json: lane-run's phase record (DEF-1572)
|
|
145
145
|
path = os.path.join(state, f'{lane}.json')
|
|
146
146
|
try:
|
|
147
147
|
old = json.load(open(path))
|
|
@@ -44,9 +44,9 @@ terminal. Installing or closing a view does not end a lane.
|
|
|
44
44
|
whose agent pid is dead or recycled in the container's pid namespace. The sidecar has its own directory because
|
|
45
45
|
`lane-run` writes each zmx lane's `{phase, exit, at, pid}` record to `~/.lz/state/<zmx name>.json` and exports
|
|
46
46
|
that same name as `LANE_ID`: one shared file name would let a hook overwrite the record the zmx observer and the
|
|
47
|
-
container `lane ls` read (DEF-
|
|
47
|
+
container `lane ls` read (DEF-1572). A zmx lane's sidecar is therefore named by its zmx session, not by the
|
|
48
48
|
`<ws>--z-<name>` id statusd publishes, and does not reach that lane's `agent{}` yet — the zmx lane's `agent` key
|
|
49
|
-
stays lane-run's record (DEF-
|
|
49
|
+
stays lane-run's record (DEF-1573). A published sidecar adds `event` to `agent{}` and has `source: hook`. Hooks are
|
|
50
50
|
never installed implicitly: `lanes hooks install --agent claude|codex --container <name>` prints the merge and
|
|
51
51
|
writes only with `--write` (RUNBOOK "Installing agent hooks").
|
|
52
52
|
5. A lane's `source` is the container's spelling of its `terminals.json`. Every host-side reader of that file
|
|
@@ -71,7 +71,7 @@ to the server's GLOBAL environment (`show-environment -g`), and every later sess
|
|
|
71
71
|
unless its own `-e` overrides each key — a session created by a client carrying `LANE_ID=b` but no `-e` still got
|
|
72
72
|
the server starter's `LANE_ID=a`, and one created with `-e LANE_ID=c` alone kept `a`'s `THROTTLE_TRACE`. Board
|
|
73
73
|
sockets are shared (9 of the 10 bw-tools fixture lanes are on one socket), so `lane open` execs nothing — attach or
|
|
74
|
-
recipe — that carries a lane identity, this lane's or one inherited from another lane's editor terminal (DEF-
|
|
74
|
+
recipe — that carries a lane identity, this lane's or one inherited from another lane's editor terminal (DEF-1559);
|
|
75
75
|
a recipe run by it may start the server with an identity-free global environment only. An attach cannot change a
|
|
76
76
|
running session's env, so a running lane gains its identity only when it is next CREATED by a recipe carrying `-e`.
|
|
77
77
|
A legacy recipe without `-e` gives its lane no identity. `lanes cursor-sync --canonicalize` (dry-run unless
|
|
@@ -92,7 +92,7 @@ stamped `lanes/<ws>/<id>` therefore puts its main session, every subagent and an
|
|
|
92
92
|
into ONE fairness bucket where today each Claude session/agent is its own. `max_in_flight_per_trace` is configured
|
|
93
93
|
and reported but no enforcement site exists in `src/` (MEASURED by search; `pressure.rs` documents fairness as "not a
|
|
94
94
|
hard cap"). Whether a lane should be one bucket is an open decision for the plan owner before any live enablement
|
|
95
|
-
(DEF-
|
|
95
|
+
(DEF-1560); this tree does not change the header shape. Unit-proven only (`test_lib_probe` `LaneOpenIdentity`,
|
|
96
96
|
`CursorSyncCanonicalize`, `CursorSyncIdentity`, stopping at the exec boundary); the disposable-lane
|
|
97
97
|
`tests/cursor_lane.py` exercises the installed `/home/coder/.lz/bin/lane`, so it proves this only after `lanes
|
|
98
98
|
install-container`.
|
|
@@ -153,6 +153,10 @@ Explicit **New +** starts a detached tmux shell inside the workspace container o
|
|
|
153
153
|
rechecking its session ID and creation time and refusing registered lanes; affected viewers become empty.
|
|
154
154
|
The tmux close condition is checked inside the server, protecting a replacement created between observation
|
|
155
155
|
and the action. `user-sessions/` stores display metadata; `status.json` and `resolve.dynamic/` are projections.
|
|
156
|
+
`status.json` also carries `zmx_listed_at` — per container, the unix time statusd ISSUED the `zmx list` that last
|
|
157
|
+
answered (a container whose listing timed out or failed is absent; its `zmx` groups are then the previous ones
|
|
158
|
+
republished). It sits outside the change comparison, so the file is still rewritten only on change or the heartbeat;
|
|
159
|
+
the co-located Studio Host prunes a lane it started only on such a listing (docs/SHARING.md, design §8.10 C1).
|
|
156
160
|
The pre-persistence Docker-shell tracker (`legacy-sessions/`, `legacy_sessions.py`, `lanes verify-legacy`) was
|
|
157
161
|
retired by plan Task 19: statusd no longer reads that directory (a record still there is inert — kept on disk,
|
|
158
162
|
never observed, never closed), and a `legacy` row from an older status.json is refused by close. See
|
|
@@ -175,15 +179,15 @@ writes that last `;` as `\;`. Any other id is a zmx lane (a registry `holder: zm
|
|
|
175
179
|
keeps the zmx path; zmx `send` still writes the bytes as given, never an Enter of its own. `kill` now needs `--yes`
|
|
176
180
|
on both holders and refuses while the lane's hook sidecar says `working` unless `--force`; the container sees only
|
|
177
181
|
that sidecar (Claude session files and pane titles are judged on the host by `lanes-statusd`), so on a lane without
|
|
178
|
-
installed hooks the refusal cannot fire (DEF-
|
|
182
|
+
installed hooks the refusal cannot fire (DEF-1575). `ls --json` stays the zmx-only listing Throttle Studio reads —
|
|
179
183
|
its zmx holder takes EVERY row as a zmx lane, so tmux rows there would publish each tmux lane twice; `ls --all` adds
|
|
180
184
|
the registry's tmux lanes with `alive` from one `list-sessions` per socket (and still lists them in a root without the zmx binary, where the default listing keeps failing as before). zmx rows gain `alive`, and `status` of a
|
|
181
|
-
zmx lane gains `hook` (its sidecar); `agent` stays lane-run's phase record (DEF-
|
|
185
|
+
zmx lane gains `hook` (its sidecar); `agent` stays lane-run's phase record (DEF-1572).
|
|
182
186
|
|
|
183
187
|
The bw-tools agent launchers (`.bw-tools/bin/claude`, `codex`) exec the agent binary (shell `exec` → `agent.py`
|
|
184
188
|
`os.execvpe`), so a lane started through them shows the agent as the pane's foreground command. The operator's
|
|
185
189
|
`~/.local/bin/eval-claude` runs Claude as a child instead (it appends `session_end` to its trace file after the
|
|
186
|
-
exit), so a pane started through it keeps `bash` as its foreground command (DEF-
|
|
190
|
+
exit), so a pane started through it keeps `bash` as its foreground command (DEF-1576); on the launcher path the end
|
|
187
191
|
of a session is the `SessionEnd` hook's `lane-signal shell`, when hooks are installed. `.bw-tools/bin/claude` runs
|
|
188
192
|
`$EVAL_CLAUDE_REAL_BIN` when it is set (the binary eval-claude would run) and skips it when it names a `.bw-tools`
|
|
189
193
|
shim, so `EVAL_CLAUDE_REAL_BIN=<checkout>/.bw-tools/bin/claude eval-claude` composes the two without a loop.
|
|
@@ -236,6 +240,9 @@ It replaces old unit symlinks atomically and creates `~/.local/share/lanes/curre
|
|
|
236
240
|
No service requires the checkout to be named `~/lanes`. Re-run the server installer after moving it.
|
|
237
241
|
|
|
238
242
|
Core units are the status observer and sync, ensure and garbage-collection timers. The web units are opt-in.
|
|
243
|
+
The co-located Throttle Studio Host (`lanes-studio-host.service` and its enrolment retry timer) is opt-in too:
|
|
244
|
+
`install/enable-studio-host.sh` enables it, and its binary is selected by `lanes studio-host install` from an
|
|
245
|
+
`opt-in` line of `holders.lock` or a local build, sha256-verified either way (`libexec/studio_host.py`).
|
|
239
246
|
Temporary-file cleanup and process protection have explicit plan/apply or enable steps; installation does not
|
|
240
247
|
arm them. The root host guard is also separate and records rollback values. See [the runbook](RUNBOOK.md)
|
|
241
248
|
and [host protection](HOST-GUARD.md) for those operations.
|
|
@@ -128,7 +128,7 @@ frame is seven-bit. The `ANSI-16` column is what `LANES_COLOR=16` emits; truecol
|
|
|
128
128
|
| 2 | `AGENT_GLYPHS[agent.status]`; blank without `agent{}`; ink-dim when stale | its tone |
|
|
129
129
|
| 3 | › viewing (not on the cursor row) · ² / ³ another viewport shows it · else space | accent |
|
|
130
130
|
| 4–27 | name (24 cells) in `38;5;N` or ink, **bold when viewing**; viewer dots reserved first, name clipped to `24 − len(dots)` and ending in `…` when clipped; truncation detected by `len(clipped) < len(name)` (no second width walk) | identity colour |
|
|
131
|
-
| 28 | `+` register control on an unregistered row that can be added to a workspace (closable and `R.eligible`; CHA-positioned at `cols − 8`; hit box `cols − 8 ≤ x < cols − 4`), else space — amended by Task 23 (§10, DEF-
|
|
131
|
+
| 28 | `+` register control on an unregistered row that can be added to a workspace (closable and `R.eligible`; CHA-positioned at `cols − 8`; hit box `cols − 8 ≤ x < cols − 4`), else space — amended by Task 23 (§10, DEF-1509) | tone-done |
|
|
132
132
|
| 29–31 | age, right-aligned: `NNs` < 60 s, `NNm` < 60 min, `NNh` < 24 h, `NNd` ≤ 99 d; blank when nothing is known; source `agent.since` else `activity` | ink-dim; attention inside Needs you |
|
|
133
133
|
| 32 | space | |
|
|
134
134
|
| 33 | `GLYPHS[state]` — full tone without an agent, ink-dim with one (▸ + ○ is the long-tool-call case and stays readable) | its tone |
|
|
@@ -231,14 +231,14 @@ Each budget is pinned by a test; a budget that is not met is an open defect row,
|
|
|
231
231
|
|
|
232
232
|
| Budget | Pin |
|
|
233
233
|
|---|---|
|
|
234
|
-
| `render(35 lanes + 5 unregistered, 36×60)` median of 50 after 5 warm-ups ≤ 2.0 ms hard (CI, loose); design target ≤ 0.9× the same-run baseline cold (today 0.459 ms here, binary-safe read) and ≤ 0.10 ms warm (row cache) — MEASURED 2026-10-04 after Task 23: cold 0.64× met; warm 0.27–0.31 ms at load ~23–30, NOT met, DEF-
|
|
234
|
+
| `render(35 lanes + 5 unregistered, 36×60)` median of 50 after 5 warm-ups ≤ 2.0 ms hard (CI, loose); design target ≤ 0.9× the same-run baseline cold (today 0.459 ms here, binary-safe read) and ≤ 0.10 ms warm (row cache) — MEASURED 2026-10-04 after Task 23: cold 0.64× met; warm 0.27–0.31 ms at load ~23–30, NOT met, DEF-1510 open; the target is not raised | `test_sidebar_budget.test_desktop_render_time_and_frame` (time) + bench numbers in the commit message of every task touching `render()` |
|
|
235
235
|
| Frame ≤ 9216 B truecolor, ≤ 5632 B in `LANES_COLOR=16` at 36×60 (today 4330 B in both on the DEF-1417 fixture, 4297 B before it — `LANES_COLOR` is not read before Task 22; truecolor prefixes are the whole delta) | same test, `len(frame)` |
|
|
236
236
|
| Exactly `viewports` `open()` calls of target files per render (12 over 6 renders at 2 viewports today); `subprocess.Popen`/`run` never called | `test_render_opens_only_target_files_and_never_forks` (open counter on the view dir, Popen/run patched to raise) |
|
|
237
237
|
| Every emitted row ≤ 36 cells and column 36 blank on every row that is not made of the `rule` glyph alone. At Task 21 (before the one-row strip) the four upload-box rows are exempt as well — rows starting with `─ ┌ │ └`; Task 24 narrows the exemption to the rule when it removes the box | `test_no_row_exceeds_36_cells_and_column_36_is_blank` (ANSI- and OSC-stripped, binary-safe sink read, `cell_width`; and, because the strip hides positioned writes, every CHA/CUP column in the raw frame < 36 — today's maximum is 34; amendment 2026-10-04; and, because text positioned left of 36 can still run into it, every raw row walked cell by cell — `highest_cell` — writes no cell past 35, 36 on the exempt rows, over a fixture whose registrable ghosts carry both the `+` and the `×`; DEF-1417) with its detectors proven by `test_sidebar_budget_sabotage` |
|
|
238
238
|
| Per tick ≤ 7 `stat()` at one viewport — registry, status, target, health, `flags/sockets-lost` (today's five), economy (Task 12), `devices/<device>.json` (Task 22) — plus one per extra viewport; zero timers added; the OSC 11 query is one write at start | `test_tick_stats_at_most_seven_files` (one viewport); `test_theme_query_is_written_once` |
|
|
239
239
|
| Upload: select timeout stays 1.0 while uploading; `dirty` only on `.progress` mtime change | `test_upload_does_not_shorten_the_tick` (Task 24) |
|
|
240
240
|
| `render_mobile(35, 48×60)` ≤ 2.0 ms hard, frame ≤ 6144 B; target ≤ 0.45 ms cold / ≤ 0.10 ms warm (today 0.352 ms / 1663 B) | `test_mobile_render_time_and_frame` |
|
|
241
|
-
| `reload(force)` unchanged (0.14 ms at 35 / 0.39 ms at 183); fleet attention computed inside it from the loaded dict — NOT shown met: Task 24 measured 0.26–0.44 ms at 35 / 0.69–0.80 ms at 183 and Task 12 0.296 → 0.386 ms at 35 (load 30–62; the 0.14/0.39 figures are quiet-host figures), DEF-
|
|
241
|
+
| `reload(force)` unchanged (0.14 ms at 35 / 0.39 ms at 183); fleet attention computed inside it from the loaded dict — NOT shown met: Task 24 measured 0.26–0.44 ms at 35 / 0.69–0.80 ms at 183 and Task 12 0.296 → 0.386 ms at 35 (load 30–62; the 0.14/0.39 figures are quiet-host figures), DEF-1564 open; the budget is not raised | bench in Task 24's and Task 12's commit messages; DEF-1564 |
|
|
242
242
|
| Phone: exactly one `setInterval(`, zero `setTimeout(`, no `backdrop-filter`, a painted `box-shadow` only in `.lanes-sheet-panel` and `.zj-mobile-menu` (a `box-shadow: none` override, as on the security modal, is not paint work and is not counted); `fetch(` absent; mobile.css grows ≤ 2048 B over today's 16 934 B, mobile.js ≤ 3072 B over today's 19 102 B (measured 2026-10-03; the caps are baseline + allowance, quoted in the test); `index.html` keeps its 9 SRI-pinned upstream assets | `test_web.test_js_timers_unchanged`, `test_web.test_no_scroll_time_effects`, `test_web.test_bundle_growth`, `test_web.test_integrity_pins_untouched` |
|
|
243
243
|
| Zellij: config.kdl grows by the theme + ui block only (~4 KB), `load_plugins {}` empty, no `rename-tab` from the sidebar | `test_keybinds.test_theme_block_and_plugin_free`, `test_tokens.test_generated_blocks_match_committed_files` |
|
|
244
244
|
| Unit suite stays under 75 s wall (MEASURED 2026-10-04 after Task 21: 833 tests, 124–176 s at load ~30–60 — exceeded, DEF-1415 open; the budget is not raised); new pins are exact-string asserts, no sleeps; `tokens.py --check` ≤ 50 ms, contrast test ≤ 10 ms | observed in each task's run step |
|
|
@@ -194,7 +194,37 @@ There is no public fallback by design: views are reachable only through the tail
|
|
|
194
194
|
Tailscale name, `lanes web` prints the loopback URL `http://127.0.0.1:8083/<device>` and says that it opens only
|
|
195
195
|
on the host itself.
|
|
196
196
|
|
|
197
|
-
## 5.
|
|
197
|
+
## 5. Optional Throttle Studio Host on the server
|
|
198
|
+
|
|
199
|
+
One Throttle Studio Host can serve every board of this server from the workspace account, supervised as the
|
|
200
|
+
opt-in `lanes-studio-host.service` (Slice `lanes.slice`, after `lanes-statusd`). `./install.sh --server` renders its
|
|
201
|
+
unit but never enables it or downloads its binary. Install a sha256-verified `throttle-rc`, then enable it:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
lanes studio-host install --from ./throttle-rc --sha256 <digest> # or, once holders.lock pins a release: lanes studio-host install
|
|
205
|
+
sh install/enable-studio-host.sh --server https://throttle.example.com --dry-run
|
|
206
|
+
sh install/enable-studio-host.sh --server https://throttle.example.com
|
|
207
|
+
lanes studio-host status
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Enrolment needs no browser on the server: the platform deploy adopts the machine's published public key and writes a
|
|
211
|
+
receipt that `lanes-studio-host-enrol.timer` installs, or the owner places a registration key file. Until then the
|
|
212
|
+
unit waits (exit 3, never a restart loop). The deploy-provisioned path needs the deploy to run on this machine as
|
|
213
|
+
this account (one filesystem, its own env file via `THROTTLE_RC_ENV_FILE`); otherwise use the registration key.
|
|
214
|
+
[Sharing](SHARING.md#co-located-studio-host-one-host-per-lanes-server) has the details, the deploy variables and the
|
|
215
|
+
migration from a hand-built `lanes-sharing-host.service`.
|
|
216
|
+
|
|
217
|
+
The installers write the lanes units where the user manager reads them: `~/.config/systemd/user`, or
|
|
218
|
+
`$XDG_CONFIG_HOME/systemd/user` when the MANAGER's environment sets `XDG_CONFIG_HOME` (`systemctl --user
|
|
219
|
+
show-environment`). Setting `XDG_CONFIG_HOME` in the manager's environment after installing (pam_env,
|
|
220
|
+
`/etc/environment`, `systemctl --user set-environment`) moves where it looks: re-run `./install.sh --server`. The
|
|
221
|
+
two studio units pin `XDG_CONFIG_HOME`/`XDG_DATA_HOME` themselves (the roots `install/enable-studio-host.sh` ran
|
|
222
|
+
with, recorded in `$LANES_STATE/studio-host/xdg-roots.json`, else `%h/.config` and `%h/.local/share`), so neither the
|
|
223
|
+
manager's own values nor a later `./install.sh --server` from a shell without those roots moves the Host's config or
|
|
224
|
+
credential; the server installer warns when its shell's roots differ from the record. To move them, re-run
|
|
225
|
+
`install/enable-studio-host.sh` from the shell whose roots should hold.
|
|
226
|
+
|
|
227
|
+
## 6. Updates, optional operations and removal
|
|
198
228
|
|
|
199
229
|
A server installed with `npx @vibelord/lanes --server` runs from the staged tree `~/.local/share/lanes/pkg/<version>/`; updating is `npx @vibelord/lanes@latest --server`, and the previous versions a service may still run from are kept.
|
|
200
230
|
|
|
@@ -205,6 +235,8 @@ use the same overrides when reinstalling. `LANES_WORKSPACES` is also carried int
|
|
|
205
235
|
when supplied; the standard configuration directory is the simplest default.
|
|
206
236
|
|
|
207
237
|
After updating server code, restart the status observer with `systemctl --user restart lanes-statusd.service`.
|
|
238
|
+
If you enabled the Studio Host and selected a new `throttle-rc` or edited its config, restart
|
|
239
|
+
`lanes-studio-host` (re-running `install/enable-studio-host.sh` does that, after `studio host --check`).
|
|
208
240
|
If web service definitions or the pinned Zellij binary changed, restart `lanes-web` and `lanes-mobile` too;
|
|
209
241
|
browser clients reconnect. Existing Zellij view sessions use their original layout until explicitly rebuilt.
|
|
210
242
|
`lanes rebuild DEVICE` replaces only that device's view, never the underlying agent sessions.
|
|
@@ -216,6 +248,8 @@ Normal paths:
|
|
|
216
248
|
| `~/.config/lanes/` | Site profile, client settings and server workspace configuration |
|
|
217
249
|
| `~/.local/share/lanes/` | Client copy, pinned binaries, download cache, installed checkout link and npm-staged trees (`pkg/<version>/`) |
|
|
218
250
|
| `~/.local/state/lanes/` | Host registry, device selections, status, upload handoff and housekeeping projections |
|
|
251
|
+
| `~/.local/state/lanes/studio-host/` | Optional Studio Host: published public key, deploy receipt, enrolment record, migrated unit text (`lanes studio-host re-key --yes` clears the key half after a revoke) |
|
|
252
|
+
| `~/.config/throttle-rc/`, `~/.local/share/throttle-rc/` | Optional Studio Host config, machine key and enrolment receipt (throttle-rc's own files) |
|
|
219
253
|
| `~/.local/state/lanes-client/` | Client helper heartbeat and logs |
|
|
220
254
|
| `/home/coder/.lz/` in each container | Lane helper, bindings and optional zmx metadata |
|
|
221
255
|
|
|
@@ -226,6 +260,7 @@ its root guard. These operations are included in the package but are not an inst
|
|
|
226
260
|
Clients uninstall with `./install.sh --uninstall`; SSH keys, `~/Lanes Uploads`, and any server workspace
|
|
227
261
|
configuration and state on the same account are kept.
|
|
228
262
|
To stop the host services, disable `lanes-statusd.service`, `lanes-sync.timer`, `lanes-ensure.timer`,
|
|
229
|
-
`lanes-gc.timer`, and any optional units you enabled.
|
|
263
|
+
`lanes-gc.timer`, and any optional units you enabled (`lanes-studio-host.service` and
|
|
264
|
+
`lanes-studio-host-enrol.timer` included; revoke the machine in the console if it should not come back). For the web endpoint, also run
|
|
230
265
|
`tailscale serve --https=8443 off`. Keep the state and container lane data until you have decided which
|
|
231
266
|
sessions to retain; no broad deletion command is part of uninstall.
|
|
@@ -50,7 +50,7 @@ Since plan Task 26 the phone's picker pane has no fixed layout name, so Zellij c
|
|
|
50
50
|
session laid out after Task 26: an existing phone device keeps the old picker pane until `lanes rebuild <device>`
|
|
51
51
|
(`upgrade-session` and `ensure` add missing tabs and rename titles, they never re-lay-out a tab; `lanes doctor` names
|
|
52
52
|
such a device). Whether it
|
|
53
|
-
reaches the phone is still unchecked in a browser (DEF-
|
|
53
|
+
reaches the phone is still unchecked in a browser (DEF-1531). A lane that
|
|
54
54
|
STARTS needing you (`◆` `✗` `◇`) raises one desktop notification, `name: task`, through OSC 9 — once per transition,
|
|
55
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
56
|
(or restarts) while lanes already need you announces each of them once. Both are bytes in the frame the sidebar
|
|
@@ -120,13 +120,16 @@ lanes rebuild <device> recreate a view session lanes ensure recrea
|
|
|
120
120
|
lanes invariant save|check prove no live pane changed (pid + window size)
|
|
121
121
|
lanes device list the devices ensure keeps lanes device forget <name> stop recreating one
|
|
122
122
|
systemctl --user status lanes-statusd lanes-sync.timer lanes-ensure.timer lanes-web
|
|
123
|
+
lanes studio-host status [--check] the optional co-located Studio Host
|
|
124
|
+
lanes studio-host re-key --yes after the owner revoked the Host: a new machine key for the next deploy
|
|
125
|
+
(refused while lanes-sharing-host or throttle-rc-studio-host still runs)
|
|
123
126
|
```
|
|
124
127
|
|
|
125
128
|
- **What `lanes doctor` says about the boards' recipes (plan Task 18):** one line counts the tmux lanes per create
|
|
126
129
|
recipe class — `idempotent` (a `tmux new -A…`: creates, or attaches when the session exists; the canonical recipe),
|
|
127
130
|
`attach-only` (`attach-session` and no `new`), `null` (no recipe), `external` (a script or anything else, run as
|
|
128
131
|
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-
|
|
132
|
+
them create with their identity (`-e LANE_ID=…`, DEF-1559). Warnings (`⚠`, never a `✗`, the exit code is
|
|
130
133
|
unchanged): lanes that `lane open` cannot recreate once they end (attach-only, null, retired-path — fix with
|
|
131
134
|
`lanes cursor-sync --canonicalize`), lanes bound to a numeric session name (tmux numbers unnamed sessions itself,
|
|
132
135
|
so a recreated one gets another number), more than 4 lanes on one container's tmux socket `default` (the server every
|
|
@@ -137,7 +140,7 @@ systemctl --user status lanes-statusd lanes-sync.timer lanes-ensure.timer lanes-
|
|
|
137
140
|
file is there, nobody listens) unlinks it, printing each path; a server that answers (even with no session), does
|
|
138
141
|
not answer within 5 s, or refuses for another reason (EACCES, …) keeps its file. The errno decides, never tmux's
|
|
139
142
|
wording: tmux 3.4 prints `no server running on <path>` for ECONNREFUSED and `error connecting to <path>
|
|
140
|
-
(<strerror>)` only for other errnos (DEF-
|
|
143
|
+
(<strerror>)` only for other errnos (DEF-1540).
|
|
141
144
|
- **A device you no longer use (an old laptop, a phone you replaced):** `lanes device forget <name>` removes
|
|
142
145
|
`devices/<name>.json`, so `lanes ensure` (`lanes-ensure.timer`) stops recreating its view session and `sync` /
|
|
143
146
|
`upgrade-session` stop updating it. A view session that is running is left alone and says so; once it ends, nothing
|
|
@@ -179,6 +182,30 @@ systemctl --user status lanes-statusd lanes-sync.timer lanes-ensure.timer lanes-
|
|
|
179
182
|
- **A new workspace:** add it to `~/.config/lanes/workspaces/catalog.json` + a manifest, `lanes sync`, then
|
|
180
183
|
`lanes rebuild <device>` (the only time a layout changes).
|
|
181
184
|
- **Registry invalid:** the workspace keeps its previous lanes; the sidebar footer and `lanes doctor` show why.
|
|
185
|
+
- **The Studio Host (optional, `lanes-studio-host.service`):** `lanes studio-host status` names the state —
|
|
186
|
+
`running`, `waiting-for-enrolment` (the deploy has not written a receipt yet; the unit is enabled and the enrol
|
|
187
|
+
timer active, so it installs the receipt within 5 minutes), `receipt-refused` (a receipt is on disk and the
|
|
188
|
+
installer refused it — the code, when, and the next retry are named; the timer's one line per tick is in
|
|
189
|
+
`journalctl --user -u lanes-studio-host-enrol.service`), `key-refused` (the registration key was tried and
|
|
190
|
+
`studio enroll` refused it — code, exit, when, next retry), `credential-for-gone-key` (a receipt on disk for a
|
|
191
|
+
machine key this machine no longer holds, for a row it has NOT measured revoked: revoke the row in the console,
|
|
192
|
+
then `lanes studio-host re-key --yes`; the installer never enrols a second machine beside it by itself),
|
|
193
|
+
`not-enabled` (not enrolled and the unit or the enrol
|
|
194
|
+
timer is not enabled, so nothing retries: re-run `sh install/enable-studio-host.sh`),
|
|
195
|
+
`stopped` (exit 3 is the Host's typed refusal: `journalctl --user -u lanes-studio-host.service -n 30`, then
|
|
196
|
+
`systemctl --user start lanes-studio-host.service` or re-run `sh install/enable-studio-host.sh` — a check red only on
|
|
197
|
+
that run's own recorded refusal no longer blocks the start; the retry timer does NOT restart it on the same
|
|
198
|
+
enrolment),
|
|
199
|
+
`receipt-unreadable` / `key-unreadable` (the file exists but this account cannot read it — e.g. a receipt a deploy
|
|
200
|
+
run as another account wrote 0600: give it to this account), `conflict` (another Host unit runs here: one Host per
|
|
201
|
+
machine), `not-installed`, or `unknown` (no answer from the user manager; a shell without `XDG_RUNTIME_DIR` falls
|
|
202
|
+
back to `/run/user/<uid>`). `status --check` exits 1 (`running-check-failed`) when the unit runs but its own
|
|
203
|
+
`studio host --check` does not pass. A receipt refused `receipt_server_mismatch`: fix the Host's `server` in
|
|
204
|
+
`~/.config/throttle-rc/studio-host.toml` — the timer retries the same receipt when the config changes, a manual
|
|
205
|
+
`sh install/enable-studio-host.sh` retries it at once. A new binary: `lanes studio-host install --from FILE --sha256 HEX`, then re-run
|
|
206
|
+
`sh install/enable-studio-host.sh` (checks, then restarts). Revoked by the owner (`journalctl` says revoked; the
|
|
207
|
+
deploy warns `host_revoked`): `lanes studio-host re-key --yes`, then deploy again — see
|
|
208
|
+
[Sharing](SHARING.md#after-the-owner-revokes-this-machine). See [Sharing](SHARING.md#co-located-studio-host-one-host-per-lanes-server).
|
|
182
209
|
|
|
183
210
|
## When the host is overloaded
|
|
184
211
|
|
|
@@ -402,7 +429,7 @@ The sidebar's own colours and glyphs come from one token table (`libexec/tokens.
|
|
|
402
429
|
and the `say` line names it. A plain `./install.sh` re-run without `--palette` removes the include and all palette
|
|
403
430
|
files: inherit-first is the default. NOT yet looked at on a real Ghostty (2026-10-04: no Mac or Omarchy client was
|
|
404
431
|
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-
|
|
432
|
+
model of Ghostty's load order read from its source). DEF-1547 stays open until that is seen: run `--palette` on a
|
|
406
433
|
Mac (Ghostty ≥ 1.1) and on Omarchy, and the window background must be `#101318` (the dark `surface` token).
|
|
407
434
|
- 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
435
|
attached, and no live session was touched). To check: with the Mac in light mode, `lanes attach mac` from the Mac,
|
|
@@ -436,17 +463,17 @@ only normalizes HOW a terminal reaches its lane:
|
|
|
436
463
|
are additionally projected into the file Cursor reads (`"cursor"` in `workspaces/*.json`).
|
|
437
464
|
- `lanes cursor-sync <ws> --canonicalize` (dry run; add `--write` to apply) replaces every tmux lane's recorded
|
|
438
465
|
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-
|
|
466
|
+
classes `lanes doctor` warns about, DEF-1541), or a script missing in the container — with the
|
|
440
467
|
canonical `tmux new -ADs … -e LANE_ID=… -e LANE_WS=… bash -l` recipe (old value kept as `createPrevious`).
|
|
441
468
|
`--identity` adds the same `-e` flags to a working plain `tmux -L S new -ADs N [-c D] CMD` recipe, keeping CMD's
|
|
442
469
|
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-
|
|
470
|
+
tmux lane gets `LANE_ID`/the trace: `lane open` deliberately exports none (DEF-1559 — the first client on a shared
|
|
444
471
|
board socket would otherwise hand its identity to every later lane there). Both change the file only: a RUNNING
|
|
445
472
|
lane keeps its environment and gains the identity when it is next created. Before enabling it on a live board,
|
|
446
|
-
read DEF-
|
|
473
|
+
read DEF-1560: a `lanes/<ws>/<id>` trace makes the whole lane — main session, subagents, fan-out — one proxy
|
|
447
474
|
fairness bucket. Applying `--canonicalize --write` or `--identity --write` to a live workspace is an operator gate
|
|
448
475
|
(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-
|
|
476
|
+
as `createPrevious`, file backup under `cursor-sync-backups/<ts>/` as above) and produces the first live `lanes/<ws>/<id>` trace — decide DEF-1560 lane-level
|
|
450
477
|
vs per-session fairness first.
|
|
451
478
|
- Size policy "the lanes view wins": lanes-view tmux clients run with `TERM=xterm-lanesview` (a user-level terminfo
|
|
452
479
|
alias of xterm-256color). While such a client shows a lane, every other client of that lane (Cursor terminals,
|
|
@@ -462,7 +489,7 @@ event-driven: Claude Code or Codex runs `~/.lz/bin/lane-signal <status>` on a tu
|
|
|
462
489
|
`~/.lz/state/hooks/<lane id>.json` in the container, and `lanes-statusd` prefers that over the other signals while
|
|
463
490
|
its agent runs and its last event is under an hour old. Nothing installs a hook for you. (`~/.lz/state/<name>.json`
|
|
464
491
|
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-
|
|
492
|
+
its sidecar under the zmx session name, which the sidebar does not show yet — DEF-1573.)
|
|
466
493
|
|
|
467
494
|
1. `lanes install-container <name>` — puts `lane-signal` in `~/.lz/bin/` and the definitions in `~/.lz/hooks/`.
|
|
468
495
|
2. `lanes hooks install --agent claude --container <name>` — prints the unified diff it would make to that
|
|
@@ -504,7 +531,7 @@ Codex 0.160 has no `codex --help hooks`; the names come from the binary. Not yet
|
|
|
504
531
|
loads a USER-level `~/.codex/hooks.json` the way it loads a project's (the only hooks.json files measured here are
|
|
505
532
|
project files), and what a hook's parent process is under each harness (lane-signal skips one shell either way).
|
|
506
533
|
|
|
507
|
-
Known limit (DEF-
|
|
534
|
+
Known limit (DEF-1515): after you approve a permission prompt, the lane shows `needs_approval` until the approved
|
|
508
535
|
tool call returns (`PostToolUse`), because neither harness signals "approved". A long approved command therefore
|
|
509
536
|
reads as waiting for you until it finishes. A lane whose agent crashed loses its sidecar at once (dead pid); one
|
|
510
537
|
whose agent is alive but silent keeps the last word for up to an hour.
|
|
@@ -535,7 +562,7 @@ in priority order and whole parts dropped from the end to fit 34 cells:
|
|
|
535
562
|
|
|
536
563
|
A leading `?` and an all-dim line mean the figures are frozen: the poller kept its last fleet strip because `/status`
|
|
537
564
|
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-
|
|
565
|
+
treats a document more than 180 s old by its own clock as frozen (DEF-1528). A lane's tail cell follows the same rule:
|
|
539
566
|
a `⚠` or `↓` drawn dim (not amber) is the last known figure, not a live alarm — the poller kept that lane's record
|
|
540
567
|
because `/sessions` failed (`"stale": true` on the lane), or the whole document is frozen. With
|
|
541
568
|
`LANES_THROTTLE_CONTROL=none` every fleet figure is `null`: the strip row then shows fleet attention exactly as without
|
|
@@ -553,7 +580,7 @@ default reaches them from the host:
|
|
|
553
580
|
(`[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
581
|
`http://127.0.0.1:19181` on the host is a different loopback. Until the control port is reachable from the host —
|
|
555
582
|
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-
|
|
583
|
+
fleet strip and live per-lane figures stay `null`, the log fold still works (DEF-1525, open).
|
|
557
584
|
- The log lives in the container's home. Set `LANES_THROTTLE_CONTAINER=<container name>`: the poller reads
|
|
558
585
|
`/home/coder/.local/share/throttle` through that container's home volume as `lanes sync` recorded it in
|
|
559
586
|
`~/.local/state/lanes/containers.json` (the container must be on a board). The host user's own
|
|
@@ -565,7 +592,7 @@ Check what the poller can see before you enable it — `--once` names every sour
|
|
|
565
592
|
volume sits under the docker data root (`docker info -f '{{.DockerRootDir}}'`: `/var/lib/docker` by default,
|
|
566
593
|
`/data/docker` on ceobox, where the workspace homes are named volumes at `<root>/volumes/coder-<id>-home/_data`),
|
|
567
594
|
which only root normally traverses. The log half then needs the same operator decision as the control half
|
|
568
|
-
(DEF-
|
|
595
|
+
(DEF-1525): point `LANES_THROTTLE_LOG_DIR` at a directory this user can traverse, or run the poller where the daemon
|
|
569
596
|
runs:
|
|
570
597
|
|
|
571
598
|
```sh
|
|
@@ -575,10 +602,11 @@ systemctl --user enable --now lanes-economy.service
|
|
|
575
602
|
journalctl --user -u lanes-economy.service # one line each time a source goes down or comes back
|
|
576
603
|
```
|
|
577
604
|
|
|
578
|
-
Before you enable it, settle DEF-
|
|
605
|
+
Before you enable it, settle DEF-1560 (the lane-stable trace's fairness consequence) if any lane was created with
|
|
579
606
|
the identity recipe; lanes without it are joined to their requests by `agent.session_id` alone.
|
|
580
607
|
|
|
581
|
-
Settings (environment of the installer — `install/services.py` renders them into this unit only
|
|
608
|
+
Settings (environment of the installer — `install/services.py` renders them into this unit only, from
|
|
609
|
+
`./install.sh --server` and from `install/enable-studio-host.sh` alike, which re-renders every lanes unit — or a
|
|
582
610
|
`systemctl --user edit lanes-economy.service` drop-in):
|
|
583
611
|
|
|
584
612
|
| Variable | Default | Meaning |
|
|
@@ -620,17 +648,18 @@ When nothing is sent:
|
|
|
620
648
|
(`clients/<device>.<client>.pid`), or a lane opened from the sidebar of a desk device
|
|
621
649
|
(`clients/<device>.seen.json`, counted only for a device that has a `.pid` — one `lanes attach` has run for —
|
|
622
650
|
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-
|
|
651
|
+
opening a lane the pushes resume (DEF-1532). A suppressed transition is dropped, not sent later. Phone presence
|
|
624
652
|
is decided by the device's mobile layout, not by how it attached: a Safari phone writes no `.pid`, and an
|
|
625
653
|
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-
|
|
654
|
+
the phone's picker never mutes pushes — you keep getting them while you are away from every desk (DEF-1533).
|
|
627
655
|
Only a phone's own `lanes attach` in the last two minutes counts, like any attach.
|
|
628
656
|
|
|
629
657
|
A failed send (server down, `HTTP 429`, no network) is one journal line naming the lane and the failure class — the
|
|
630
658
|
topic and the token are never logged — and is not retried.
|
|
631
659
|
|
|
632
|
-
Config `~/.config/lanes/notify.conf` (`LANES_NOTIFY_CONF` in the server installer's environment
|
|
633
|
-
|
|
660
|
+
Config `~/.config/lanes/notify.conf` (`LANES_NOTIFY_CONF` in the server installer's environment — or
|
|
661
|
+
`install/enable-studio-host.sh`'s, which re-renders every lanes unit — renders another path into this unit only),
|
|
662
|
+
`key=value` lines, `#` comments, re-read on change:
|
|
634
663
|
|
|
635
664
|
| Key | Default | Meaning |
|
|
636
665
|
|---|---|---|
|
|
@@ -763,12 +792,12 @@ what the agent needs and since when with its harness (`Needs you · 25m · claud
|
|
|
763
792
|
agent's task. Its first line counts the lanes that need you (`◆1 need approval · ◇2 need you · ▸6 working`). The same
|
|
764
793
|
counts come from the picker pane's title (see "Title and notifications" above): the header subtitle on the picker,
|
|
765
794
|
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-
|
|
795
|
+
workspace card. Nothing is fetched or polled for them. Not yet confirmed (DEF-1531): until plan Task 26 (2026-10-04)
|
|
767
796
|
the phone's picker pane had the fixed layout name `Lanes`, which Zellij reports instead of the sidebar's title, so the
|
|
768
797
|
phone showed no counts. Task 26 removed that name from the generated layout, which reaches a phone only when its
|
|
769
798
|
session is laid out again (`lanes rebuild <device>`; see the device check below); whether the title
|
|
770
799
|
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-
|
|
800
|
+
show them (DEF-1531).
|
|
772
801
|
|
|
773
802
|
The phone's light/dark toggle repaints the whole terminal, not only the shell: the page applies its own sixteen-colour
|
|
774
803
|
palette on attach, on every toggle and after every Zellij config reload (DEF-1413; not yet looked at on a device).
|
|
@@ -780,9 +809,9 @@ sends its text as UTF-8, so a filter like `ø` matches.
|
|
|
780
809
|
|
|
781
810
|
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
811
|
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-
|
|
812
|
+
on `n`/`^C` sends nothing and a hold sends it, and (plan Task 26 has landed; DEF-1531 waits on this) that the Workspaces
|
|
784
813
|
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-
|
|
814
|
+
does not re-lay-out a tab): run `lanes rebuild <device>` first (`lanes doctor` must not list it under DEF-1531), or no
|
|
786
815
|
counts appear and the check says nothing about the title path. Record the
|
|
787
816
|
device, iOS version and result here. The unit suite covers only the picker's text, not the browser.
|
|
788
817
|
|