@vibelord/lanes 1.3.0 → 1.5.0
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 +4 -3
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +790 -31
- package/payload/lanes/container/hooks/grok-cli-hooks.json +8 -0
- package/payload/lanes/container/hooks/grok-hooks.json +12 -0
- package/payload/lanes/container/hooks/kimi-hooks.toml +49 -0
- package/payload/lanes/container/lane +13 -7
- package/payload/lanes/container/lane-mcp +56 -17
- package/payload/lanes/container/lane-signal +86 -23
- package/payload/lanes/container/lane_core.py +346 -36
- package/payload/lanes/container/throttle-pulse +456 -0
- package/payload/lanes/docs/ARCHITECTURE.md +165 -14
- package/payload/lanes/docs/DESIGN.md +7 -0
- package/payload/lanes/docs/INSTALL.md +23 -3
- package/payload/lanes/docs/RUNBOOK.md +392 -4
- package/payload/lanes/docs/USERGUIDE.md +222 -23
- package/payload/lanes/libexec/agent_state.py +604 -12
- package/payload/lanes/libexec/economy_poll.py +6 -2
- package/payload/lanes/libexec/harness_grok.py +742 -0
- package/payload/lanes/libexec/lanes_lib.py +143 -20
- package/payload/lanes/libexec/sidebar.py +522 -14
- package/payload/lanes/libexec/statusd.py +434 -28
- package/payload/lanes/libexec/tokens.py +12 -1
- package/payload/lanes/libexec/update.py +78 -5
- package/payload/lanes/libexec/users.py +31 -3
- package/payload/lanes/site/site.example.conf +4 -0
- package/payload/manifest.json +4 -3
|
@@ -55,20 +55,96 @@ for operations, the [runbook](RUNBOOK.md).
|
|
|
55
55
|
per container per cycle through `/proc/<container init pid>/root` without spawning anything. Precedence: a
|
|
56
56
|
hook sidecar > a Claude `~/.claude/sessions/<pid>.json` naming the pane
|
|
57
57
|
`<session>:@<win>.%<pane>` (live pids only, checked in the container's pid namespace, recycled pids rejected
|
|
58
|
-
by `procStart`) >
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
by `procStart`) > a Kimi Code session file > the Grok Build observer (`harness_grok`: its session file, else its
|
|
59
|
+
title read with the blink memory) > the Grok Build title on a pane observed as `grok` that no observer claimed
|
|
60
|
+
(`harness_grok.parse_grok_title`, `source: grok-title`) > the Codex pane title, only where Codex can be (`[ ! ]
|
|
61
|
+
Action Required` → `needs_approval`, braille spinner → `working`, a task title → `idle`) > the observed harness
|
|
62
|
+
itself (`source: process`). The window bell applies throughout: an idle title, a finished Kimi turn or a completed
|
|
63
|
+
Grok turn plus a bell is `done` (an approval title wins over the bell; a question Kimi still asks stays
|
|
64
|
+
`needs_input`; `harness_grok` applies its own rule, so a cancelled or interrupted Grok turn stays `needs_input`). A
|
|
65
|
+
Codex title over a bare-shell foreground whose shell has no child process (`pane_has_child`, read from
|
|
66
|
+
`/proc/<pid>/task/*/children`) is `shell`: the harness ended and tmux kept its title. Unknown liveness leaves that
|
|
67
|
+
key unset and never demotes. The tmux columns this needs (`window_bell_flag`, `window_id`, `pane_id`,
|
|
68
|
+
`pane_title`) are appended to the listing format, the free-text title last so a tab inside it cannot shift another
|
|
69
|
+
column.
|
|
70
|
+
|
|
71
|
+
**The harness is observed (W12, Lanes 1.5.0; spec
|
|
72
|
+
`docs/superpowers/specs/2026-10-15-lanes-harness-observers-design.md`; DM-LANES-STATUS, DM-LANES-CONTAINER-STATUS).**
|
|
73
|
+
`agent.harness` is one of `claude | codex | kimi | grok | grok-cli | shell | unknown` (`shell` is the walk's word for a
|
|
74
|
+
pane and never reaches a published `agent{}`), an OBSERVED fact read from
|
|
75
|
+
the pane's process tree, not a side effect of which title parser matched: `agent_state.harness_walk` reads
|
|
76
|
+
`/proc/<pane pid>/task/*/children` (the reads `pane_has_child` already makes) and each process's `comm` in the
|
|
77
|
+
container's /proc, breadth first from the pane's root down four levels and at most 64 processes, and the first
|
|
78
|
+
process whose comm is `claude`, `codex`, `kimi`/`kimi-code` or `grok` — or whose `/proc/<pid>/exe` holds
|
|
79
|
+
`/claude/versions/`, native Claude Code before 2.1.299 running with its version (`2.1.295`) as comm (DEF-1993) —
|
|
80
|
+
names it. Codex runs as `bash` → `node` → `codex`, so it is found one level down; two `grok` binaries are told
|
|
81
|
+
apart by `/proc/<pid>/exe`: `…/downloads/grok-linux-<arch>` is Grok Build (the rule of
|
|
82
|
+
`harness_grok.harness_of_exe`; the container walks of `lane-signal` and `lane_core` match
|
|
83
|
+
`/downloads/grok-linux-x86_64`, the one arch measured), anything else grok-cli, an unreadable link `unknown`. Only
|
|
84
|
+
shells is `shell`; anything else, a cut tree or an unreadable read is `unknown`. `statusd.HARNESS_CACHE`
|
|
85
|
+
(`agent_state.HarnessCache`) keeps the answer per pane pid with the harness process's comm, and walks again when
|
|
86
|
+
the pane's own children changed or that process exited or now runs under another comm (so a native Claude that
|
|
87
|
+
exits below an unchanged wrapper is walked again); `shell` and `unknown` are never kept. When it names a harness,
|
|
88
|
+
the observed harness is the published `harness` whatever signal decided the status (a Claude-installed hook firing
|
|
89
|
+
inside another harness says `claude`). The Codex title (`parse_codex_title`) is read only where Codex can be: on a
|
|
90
|
+
pane observed as Codex, on an `unknown` pane only its spinner and `[ ! ] Action Required` shapes, on a bare shell
|
|
91
|
+
or an unobserved pane also its `<task> | <dir>` idle shape — the former "any title of two or more words is Codex idle" rule
|
|
92
|
+
published every Kimi lane as `codex / idle` (DEF-1970). The last rung is the observed harness itself: nothing above
|
|
93
|
+
claimed the pane, so it is that harness, `idle` (`done` with the bell), `source: process`. The values are
|
|
94
|
+
additive: the `agent` object keeps its keys.
|
|
95
|
+
|
|
96
|
+
**Kimi Code (observed, no hooks needed).** For the panes whose walk found `kimi`, `agent_state.kimi_sessions`
|
|
97
|
+
lists `~/.kimi-code/sessions/wd_*/session_<uuid>/` through the container's root with every directory opened
|
|
98
|
+
`O_NOFOLLOW` (a link planted there is refused, never followed), stats `state.json` and `agents/main/wire.jsonl`
|
|
99
|
+
(a session neither file of which moved within an hour is not read), reads `state.json` (at most 256 KiB:
|
|
100
|
+
`cwd`, `updatedAt`, `lastTurnReason`, `title`, `id`) and the last 64 KiB of the wire log (whole lines only), and
|
|
101
|
+
joins a pane's kimi pid to the session whose `cwd` equals `/proc/<pid>/cwd` — the newest when several are
|
|
102
|
+
fresh. Two kimi panes in one cwd cannot be told apart: neither is claimed and both fall back to hooks, title and
|
|
103
|
+
bell (DEF-1971). `agent_state.kimi_status` maps the log: `agent.turn.started`/`turn.prompt` and, inside a turn,
|
|
104
|
+
`context.append_loop_event` (tool calls among them) and `llm.request` → `working`; an `interaction.request`
|
|
105
|
+
of kind `approval` with no `interaction.resolved` → `needs_approval` (a `question` → `needs_input`);
|
|
106
|
+
`turn.ended` completed/cancelled and `agent.turn.ended{outcome: done}` → `needs_input` (`done` with the bell),
|
|
107
|
+
`turn.ended` failed → `errored`; with no such event in the window `state.json`'s `lastTurnReason` decides. A
|
|
108
|
+
session whose newest content is more than an hour old makes no claim. The verdict has `source:
|
|
109
|
+
kimi-session-file`, the session uuid as `session_id`, the kimi pid as `pid`, the deciding wire event (or
|
|
110
|
+
`state.json`) as `event`, and the session title (cut at 120 characters) as `name`.
|
|
111
|
+
|
|
112
|
+
**Grok Build and grok-cli** (W12, spec §4–§5; `libexec/harness_grok.py`). One observer contract:
|
|
113
|
+
`agent_state.grok_sessions(root, panes, grok_home='.grok-build', *, home='/home/coder', titles=None)` forwards its
|
|
114
|
+
arguments to `harness_grok.grok_sessions`, a module looked up at the call (none shipped, no observer), and keeps
|
|
115
|
+
the verdicts whose status is a vocabulary word or the observer's `unknown`. `statusd.agent_signals` passes the tmux
|
|
116
|
+
rows ({pane key: row}) of every pane its walk observed as `grok`, as `shell` (the observer decides by the title
|
|
117
|
+
whether that is a Grok Build pane whose harness ended), and as `unknown` with a harness pid (a `grok` whose exe
|
|
118
|
+
could not be read: Grok Build only when `active_sessions.json` names its pid), and `statusd.GROK_TITLES` as `titles`
|
|
119
|
+
— one dict per container root, kept between cycles like `HARNESS_CACHE`, pruned to the panes passed each cycle and
|
|
120
|
+
dropped with the container. The observer returns the same per-pane verdict a Claude session file gives `merge`
|
|
121
|
+
(`harness: grok`, `source: grok-session-file | grok-title`), read through the container root like the other
|
|
122
|
+
signals, with no spawn and no write. Lanes runs Grok Build with `GROK_HOME=~/.grok-build` (its installer would
|
|
123
|
+
otherwise replace grok-cli's `~/.grok/bin/grok`). The pane's process tree (depth ≤ 4) names the grok pid;
|
|
124
|
+
`$GROK_HOME/active_sessions.json` (`{session_id, pid, cwd, opened_at}`, the TUI's own pid) joins it to a session —
|
|
125
|
+
dead pids, pids no longer `grok` and pids that started after `opened_at` are ignored, two live entries for one pid
|
|
126
|
+
fall back to the title — and the last 64 KiB of `sessions/<URL-encoded cwd>/<session id>/events.jsonl`, whole lines
|
|
127
|
+
only, give the state: `phase_changed{permission_prompt}` / `permission_requested` → `needs_approval`;
|
|
128
|
+
`turn_started`, the other phases, `permission_resolved` and in-turn events → `working`; `turn_ended` `completed` →
|
|
129
|
+
`needs_input` (`done` with the bell), `error` → `errored`, `cancelled | interrupted` → `needs_input`; no turn yet →
|
|
130
|
+
`idle`. `summary.json` `generated_title` is the name. Otherwise the title decides (`⚠ Action Required …` →
|
|
131
|
+
`needs_approval`, braille spinner → `working`, `grok` / `<name> - grok` → `idle`); the prefix blinks on an
|
|
132
|
+
unfocused pane, so a busy title right after a prefixed sample — the previous cycle's, from `titles` — stays
|
|
133
|
+
`needs_approval` (DEF-1982). A Grok title over a tree read in full with no grok in it is `shell`. Every path below
|
|
134
|
+
the container root is walked `O_NOFOLLOW` and every file opened non-blocking and bounded: a symlinked `GROK_HOME`,
|
|
135
|
+
session directory or log is refused. Leader mode (a `leader*.sock` socket, or a session for the pane's cwd that
|
|
136
|
+
only a grok outside every pane holds) is out of scope: the observer reports status `unknown` with a `note`, and
|
|
137
|
+
`merge` publishes no `agent{}` for that pane (nothing below it guesses) unless a hook sidecar or a Claude session
|
|
138
|
+
file outranks it (DEF-1983). grok-cli writes nothing a host can read (no title, no bell, no pid in its database):
|
|
139
|
+
it reaches `agent{}` only through the hook sidecar (DEF-1980). The Grok Build file shapes come from its source and
|
|
140
|
+
user guide; no logged-in session was measured yet (DEF-1981).
|
|
65
141
|
|
|
66
142
|
What is published is not `merge`'s word itself but its debounced form, `agent_state.settle`, with three more
|
|
67
143
|
additive keys (`raw_status`, `raw_since`, `stable_since`): see [Attention queue](#attention-queue).
|
|
68
144
|
|
|
69
145
|
The hook sidecar is `~/.lz/state/hooks/<lane id>.json` in the container, written by `~/.lz/bin/lane-signal`
|
|
70
|
-
(Python 3 stdlib, installed by `lanes install-container`) when an opt-in Claude Code
|
|
71
|
-
`{v: 1, lane, harness, status, since, at, event, pid, start, session}` — `since` when the status began (kept
|
|
146
|
+
(Python 3 stdlib, installed by `lanes install-container`) when an opt-in hook fires (Claude Code, Codex, Kimi
|
|
147
|
+
Code, Grok Build, grok-cli): `{v: 1, lane, harness, status, since, at, event, pid, start, session}` — `since` when the status began (kept
|
|
72
148
|
while the same status is re-signalled), `at` the last event, `pid`/`start` the agent process and its
|
|
73
149
|
starttime — plus `name`, the agent's own note when it reported one (`lanes_signal`, LNS-35: kept while the status
|
|
74
150
|
repeats, dropped when it changes). A report (`event: lanes_signal`) stands against the same agent's hooks — the
|
|
@@ -85,8 +161,65 @@ for operations, the [runbook](RUNBOOK.md).
|
|
|
85
161
|
container `lane ls` read (DEF-1572). A zmx lane's sidecar is therefore named by its zmx session, not by the
|
|
86
162
|
`<ws>--z-<name>` id statusd publishes, and does not reach that lane's `agent{}` yet — the zmx lane's `agent` key
|
|
87
163
|
stays lane-run's record (DEF-1573). A published sidecar adds `event` to `agent{}` and has `source: hook`. Hooks are
|
|
88
|
-
never installed implicitly: `lanes hooks install --agent claude|codex --container <name>`
|
|
89
|
-
writes only with `--write` (RUNBOOK "Installing agent hooks").
|
|
164
|
+
never installed implicitly: `lanes hooks install --agent claude|codex|kimi|grok|grok-cli --container <name>`
|
|
165
|
+
prints the merge and writes only with `--write` (RUNBOOK "Installing agent hooks" and "Harnesses").
|
|
166
|
+
|
|
167
|
+
**The container side** (W12 Track C). `lane-signal`'s default harness is `--harness`, else `LANE_HARNESS`, else
|
|
168
|
+
the parent processes' (`comm` in the same set, a `grok` split by its exe, a native Claude Code file by its exe),
|
|
169
|
+
else `unknown` — never `codex`; `lane-mcp` names the client's harness from its MCP client name (`claude`, `codex`,
|
|
170
|
+
`kimi-code`/`kimi`, `grok-shell-*` → `grok`, `grok-cli*` → `grok-cli`), else the parent rule. The per-harness
|
|
171
|
+
operator steps are RUNBOOK "Harnesses"; what a person sees, the user guide's "Harnesses".
|
|
172
|
+
|
|
173
|
+
**Fleet and Throttle facts** (W11, spec `docs/superpowers/specs/2026-10-11-lanes-fleet-dashboard-design.md` §2;
|
|
174
|
+
DM-LANES-THROTTLE-PULSE, DM-LANES-FLEET). Two additive `status.json` keys feed the sidebar's fleet block and
|
|
175
|
+
`lanes doctor`, which read nothing else for it: `throttle`, the Throttle daemon's facts, and `fleet`, the host's.
|
|
176
|
+
|
|
177
|
+
*The pulse.* The daemon's facts come the way the hooks' do, from a writer inside the container: the daemon binds
|
|
178
|
+
its container's `127.0.0.1` (DEF-1525), so the host cannot poll it. `~/.lz/bin/throttle-pulse` (a contract file,
|
|
179
|
+
Python 3 stdlib) runs as a loop only in the daemon's container — site.conf `LANES_THROTTLE_CONTAINER`, else the
|
|
180
|
+
container of the registry's `tools--throttle-monitor` tab — where `lanes install-container` starts it, once, as
|
|
181
|
+
`tmux -L lanes-pulse` session `pulse`. Every 10 s it GETs the open `/pools/{claude,codex,kimi}/ready` and
|
|
182
|
+
`/metrics`, and the guarded `/pools/{claude,codex,kimi}/status` only with the bearer the operator adopted into
|
|
183
|
+
`~/.lz/throttle-token` (`lanes throttle adopt-token`; the control port has no loopback exemption, DEF-1930), and
|
|
184
|
+
writes `~/.lz/state/throttle.json` by rename when a value changed or 60 s passed — never anything else, never a
|
|
185
|
+
POST (RUNBOOK "Throttle pulse"). The kimi pool (W12) rides the daemon's generic pool routes (`scoped_pool_state`):
|
|
186
|
+
a daemon with no Kimi account reports it with nothing configured, and since Kimi has no paid overflow lane its
|
|
187
|
+
`overflow` is always null (`MONEY_POOLS` is claude and codex). The sidebar draws one provider row per pool —
|
|
188
|
+
`◆ anthropic`, `◆ openai`, `◆ moonshot` — so the full block is a rule and six rows from 25 pane rows
|
|
189
|
+
(`FLEET_FULL`), and the three-row form shows the pool with the smallest usable share, a pool with nothing
|
|
190
|
+
configured never (DEF-2002). There is no Grok row: Throttle has no xAI pool.
|
|
191
|
+
|
|
192
|
+
*`throttle`.* On every cycle statusd copies that file into `status.json`. It looks the daemon's container up again
|
|
193
|
+
on a registry reload and every 300 s, never per cycle, and makes ONE stat per cycle through `/proc/<init>/root` with
|
|
194
|
+
every directory opened `O_NOFOLLOW`, a read only when (pid, mtime, inode, size) moved, through `read_regular_at` (a
|
|
195
|
+
regular file of at most 64 KiB, never a link or FIFO). Only the spec's keys of their own types are copied
|
|
196
|
+
(`statusd.pulse_projection`: counts, finite numbers, booleans, a one-word phase, a printable error — never an escape
|
|
197
|
+
sequence). The copy is `stale: true` when its `at` is more than 90 s old (`PULSE_STALE_S`: the pulse's 60 s
|
|
198
|
+
heartbeat on an int-truncated `at` and a 10 s loop is routinely read 60–84 s late, DEF-1941; `lanes doctor` and the
|
|
199
|
+
sidebar use the same 90 s) or the file is absent or refused, keeping the last values. The one spawn this adds is
|
|
200
|
+
`statusd.rearm_pulse`, between two cycles in `main()`: when the pulse file is older than 90 s (or not there) while
|
|
201
|
+
the daemon's container runs and the host is not `critical`, at most once per 600 s, one bounded (10 s)
|
|
202
|
+
`docker exec -d -u 1000 -e HOME=/home/coder <c> tmux -L lanes-pulse new -d -s pulse
|
|
203
|
+
/home/coder/.lz/bin/throttle-pulse --loop` (byte for byte the argv `lanes install-container` starts it with,
|
|
204
|
+
DEF-1940), journalled with its outcome — and nothing when `~/.lz/bin/throttle-pulse` is not installed there.
|
|
205
|
+
|
|
206
|
+
*`fleet`.* Every `FLEET_INTERVAL` (10 s) the cycle asks `lanes_lib.compute_health(sample=True)`, which reads —
|
|
207
|
+
from plain files, no subprocess, and nothing at all on a `critical` host — the aggregate `cpu` line of
|
|
208
|
+
`/proc/stat`, `MemTotal`/`MemAvailable` from the `/proc/meminfo` it reads anyway, the inotify watch CEILING
|
|
209
|
+
(`/proc/sys/fs/inotify/max_user_watches`; the watches in use are never counted: that walks every process's
|
|
210
|
+
fdinfo), and per recorded container (`containers.json`) its cgroup `memory.current` and `cpu.stat` `usage_usec`.
|
|
211
|
+
Those counters are cumulative: `lanes_lib.fleet_facts` turns this sample and the previous one — kept in statusd's
|
|
212
|
+
context, never in a global — into rates (`/proc/stat` busy ÷ total jiffies; per container `usage_usec` delta ÷
|
|
213
|
+
interval ÷ logical cpus × 100, null for a container whose init pid changed or whose counter went back) and
|
|
214
|
+
publishes `fleet` with OpenTelemetry names as the keys verbatim: `host.name` (`/etc/hostname`, read once per
|
|
215
|
+
daemon start, only a DNS host name), `system.cpu.utilization`, `system.cpu.logical.count`,
|
|
216
|
+
`system.cpu.load_average.1m`, `system.memory.utilization`, `system.memory.usage{used, total}`,
|
|
217
|
+
`system.linux.memory.available`, `system.pressure{cpu.some.avg10, memory.some.avg10, memory.full.avg10,
|
|
218
|
+
io.some.avg10}`, `system.inotify.max_user_watches`, and `containers` — the three biggest by memory
|
|
219
|
+
(`container.name`, `container.memory.usage.total`, `container.cpu.percent`). The first sample has no rate (null,
|
|
220
|
+
never a since-boot average). Every figure is quantized (fractions and percents to 1 %, bytes to 0.1 GiB, the load
|
|
221
|
+
to 0.1) and the sample's own `at` is left out of the rewrite comparison, so a sample that moved nothing visible
|
|
222
|
+
does not rewrite the file; a due sample that was not taken (critical) keeps the last facts with `stale: true`.
|
|
90
223
|
5. A lane's `source` is the container's spelling of its `terminals.json`. Every host-side reader of that file
|
|
91
224
|
maps it to the container's home volume first (`statusd.registry_source_paths`, deduplicated by that host path:
|
|
92
225
|
one repository cloned in two containers is two files): the status daemon's hot-sync
|
|
@@ -868,7 +1001,9 @@ release untouched; `services.py --install` is the commit, and its refusals (the
|
|
|
868
1001
|
symlink, a path no unit can hold — each raised before its first write as a `services.Refusal`) exit 3. After it,
|
|
869
1002
|
`libexec/update.py post-commit` runs from the new tree, with its own `bin/lanes`: daemon-reload, `lanes sync`, the
|
|
870
1003
|
statusd restart, a `try-restart` of each optional unit (`update.TRY_RESTART`), `install-container` for each container
|
|
871
|
-
`containers.json` records with a home volume (read after the sync),
|
|
1004
|
+
`containers.json` records with a home volume (read after the sync), then for the Throttle daemon's container when sync
|
|
1005
|
+
did not record it (`update.daemon_container`; skipped with a warning when sync or docker said it is gone or has no home
|
|
1006
|
+
volume, `update.daemon_gone`, so a stale name never leaves every update `partial`), `lanes reload-views`,
|
|
872
1007
|
`lanes ensure --if-layout-changed` — each step in its own session, bounded (`STEP_TIMEOUT`, then its process group is
|
|
873
1008
|
stopped), its output in `update/last.log`, its exit status in `update/last.json`, and run whatever the one before
|
|
874
1009
|
returned — `lanes sync`'s exit 1 (a board with an error) being a warning that leaves the result `ok`
|
|
@@ -1008,6 +1143,21 @@ serves:
|
|
|
1008
1143
|
| Publish requests and their receipts (DM-LANES-PUBLISH-REQUEST, -RECEIPT) | each container's `~/.lz/state/publish/<lane id>/` | the request by `lane publish` / `lanes_publish`, the receipt by docsd's pick-up |
|
|
1009
1144
|
| `LANES_DOCS_PORT`, `LANES_DOCS_DOMAINS`, `LANES_DOCS_AGE_RECIPIENT`, `LANES_DOCS_AGE_IDENTITY` | lanes-docsd's drop-in (`systemctl --user edit lanes-docsd.service`) | the operator; `host-install.sh` `check_docs_age` refuses the recipient where no `age` binary is on PATH |
|
|
1010
1145
|
|
|
1146
|
+
**What the fleet block installs (plan W11, 1.4.0; spec `docs/superpowers/specs/2026-10-11-lanes-fleet-dashboard-design.md`).**
|
|
1147
|
+
No collector and no new unit: one container-side file, two additive `status.json` keys, one opt-in token file and one
|
|
1148
|
+
site fact. Routes of truth: the pulse file is the daemon's facts as the pulse last read them (DM-LANES-THROTTLE-PULSE);
|
|
1149
|
+
`status.json`'s `throttle` is statusd's copy of it with `stale`, its `fleet` statusd's own host reading (DM-LANES-FLEET);
|
|
1150
|
+
the sidebar and `lanes doctor` read `status.json` only.
|
|
1151
|
+
|
|
1152
|
+
| Piece | Where | Installed or run by |
|
|
1153
|
+
| --- | --- | --- |
|
|
1154
|
+
| `throttle-pulse` (a contract file, `bin/lanes` `CONTRACT_FILES`; stdlib Python; GET only, 3 s timeout, 1 MiB cap, no redirect) | each container's `~/.lz/bin/`; its loop runs only in the daemon's container, on the tmux socket `lanes-pulse` | `lanes install-container <name>` installs it everywhere and starts the loop in the container `LANES_THROTTLE_CONTAINER` in the server's `~/.config/lanes/site.conf` names, else the Throttle tab's; every update runs install-container again for each container `containers.json` records and for that daemon container (the same site.conf resolution); statusd re-arms a stopped loop at most once per 10 min, never while the host is critical |
|
|
1155
|
+
| The pulse file (DM-LANES-THROTTLE-PULSE) | the daemon container's `~/.lz/state/throttle.json` | the pulse, by rename, when a value changed or 60 s passed; read by statusd through `/proc/<init>/root` (one stat per cycle, ≤ 64 KiB, no link) |
|
|
1156
|
+
| `throttle`, `fleet` in `status.json` (DM-LANES-STATUS, DM-LANES-FLEET) | `~/.local/state/lanes/status.json` | statusd: `throttle` copied with `stale` (pulse `at` > 90 s old, or the file absent or refused); `fleet` every 10 s from `/proc` and the containers' cgroups, skipped while the host is critical |
|
|
1157
|
+
| `~/.lz/throttle-token` (opt-in, 0600) | the daemon container's home | the operator only: `lanes throttle adopt-token <container>` (the container's local Throttle client token, never an Anthropic credential); read by the pulse for the guarded `/pools/<pool>/status` |
|
|
1158
|
+
| `LANES_FLEET_DASHBOARD_URL` | the server's `~/.config/lanes/site.conf` | the operator only, once an authenticated Grafana exists; https only (`bin/lanes` `fleet_dashboard_url`); no code path writes it; `lanes doctor` shows it, unset, or why it is ignored |
|
|
1159
|
+
| `lanes doctor`'s `throttle pulse:`, `throttle pools:` (1.5.0), `fleet:`, `fleet dashboard:` lines, and its harness mix `lanes: claude N · codex N · …` (1.5.0) | — | `bin/lanes` `fleet_doctor` and `harness_line`, from `status.json`, the registry's Throttle tab and site.conf (it never asks the daemon, starts the pulse or runs docker); a `⚠` at worst — a pulse that is absent, down or more than 90 s old by `at` (statusd's bound, DEF-1941), a configured pool with no usable account, a `fleet` that is stale or absent, a dashboard URL that is refused — never a `✗` |
|
|
1160
|
+
|
|
1011
1161
|
## Access
|
|
1012
1162
|
|
|
1013
1163
|
Clients log in to one Unix account over SSH (tailnet, or the `lanes-public` fallback). Who may do so is the
|
|
@@ -1052,8 +1202,9 @@ owner's look on that device instead of spreading it into the new owner's profile
|
|
|
1052
1202
|
decides whenever one exists, so a claim never overrides one), and it, `lanes keys adopt --login` and `lanes keys
|
|
1053
1203
|
owner` fold the devices they name; `lanes user list|show` read the store. The sidebar of a device with an owner
|
|
1054
1204
|
(cockpit v3 R3) resolves that owner exactly as `lanes attach` does (`users.login_for`, at start and whenever the device
|
|
1055
|
-
file changes — a claim, a hand-over) and adds the profile's
|
|
1056
|
-
|
|
1205
|
+
file changes — a claim, a hand-over) and adds the profile's `(st_ino, st_mtime_ns, st_size)` to its reload stamp, the
|
|
1206
|
+
one extra stat per tick an owned device pays (`users.update` replaces the file by rename, so every write has a new
|
|
1207
|
+
inode and two writes inside one timestamp tick are both seen, DEF-1972): on a change it re-reads the profile, merges its opened-lane marks into its own (the later per lane)
|
|
1057
1208
|
and paints its look over the device file's (`layered_look`: `--device-only` keys → profile → unmarked device keys →
|
|
1058
1209
|
environment → defaults).
|
|
1059
1210
|
Opening a lane there (`open_lane`, or `lanes next|prev`) writes the device's own `clients/<device>.seen.json` as before
|
|
@@ -190,6 +190,13 @@ guessed variant on what may be a light background (`test_sidebar.ViewStatusScree
|
|
|
190
190
|
| `✓` | U+2713 | `OK` | tone-done | mark `ok` |
|
|
191
191
|
| `✗` | U+2717 | `X` | tone-urgent | mark `fail` |
|
|
192
192
|
| `▤` | U+25A4 | `#` | ink-dim | mark `docs` |
|
|
193
|
+
| `⚙` | U+2699 | `@` | ink-dim | mark `fleet-throttle` |
|
|
194
|
+
| `◆` | U+25C6 | `*` | ink-dim | mark `fleet-provider` |
|
|
195
|
+
| `▣` | U+25A3 | `[` | ink-dim | mark `fleet-host` |
|
|
196
|
+
| `▰` | U+25B0 | `=` | tone-done | mark `bar-usable` |
|
|
197
|
+
| `▱` | U+25B1 | `~` | ink-dim | mark `bar-cooling` |
|
|
198
|
+
| `▪` | U+25AA | `:` | tone-attention | mark `bar-benched` |
|
|
199
|
+
| `·` | U+00B7 | `.` | ink-faint | mark `bar-dead` |
|
|
193
200
|
| `⚠` | U+26A0 | `!` | tone-attention | economy `starving` |
|
|
194
201
|
| `↓` | U+2193 | `v` | tone-attention | economy `downgraded` |
|
|
195
202
|
| `$` | U+0024 | `$` | ink-dim | economy `paid` |
|
|
@@ -118,11 +118,18 @@ account with UID 1000. With `--write` it then applies these steps in order:
|
|
|
118
118
|
or to the title the board already has. A board that already disagrees stops the run before the container is
|
|
119
119
|
touched.
|
|
120
120
|
2. **Contract.** Runs `lanes install-container`: the `lane` contract, and the Lanes MCP server (`lane-mcp`) in
|
|
121
|
-
Claude Code's `~/.claude.json` and Codex's `~/.codex/config.toml
|
|
121
|
+
Claude Code's `~/.claude.json` and Codex's `~/.codex/config.toml`, and in Kimi Code's, Grok Build's and grok-cli's
|
|
122
|
+
files where those harnesses are installed (only where it is missing; the old file is kept
|
|
122
123
|
as `<file>.bak-<UTC time>`). The `lane-mcp` row of the table then says whether that changed either file.
|
|
123
124
|
3. **Agent hooks.** Runs `lanes hooks install --write`, first for Claude and then for Codex. Your own hooks and
|
|
124
125
|
keys are kept, with a backup of each changed file. A Lanes hook group that is already installed stays where
|
|
125
|
-
it is, even when Throttle's setup has added its own group around it.
|
|
126
|
+
it is, even when Throttle's setup has added its own group around it. Then for Kimi Code, Grok Build and grok-cli
|
|
127
|
+
(`hooks-kimi`, `hooks-grok`, `hooks-grok-cli`), each only when its home (`~/.kimi-code`, `~/.grok-build`, `~/.grok`)
|
|
128
|
+
is in the container, else `skipped`. Two steps run only when you name them: `--step kimi-notify` (Kimi Code's
|
|
129
|
+
`tui.toml` `notification_condition = "always"`) and `--step grok-build` (Grok Build installed into
|
|
130
|
+
`~/.grok-build` from its stable channel, before step 2, so its MCP entry and hooks land in the same run; the
|
|
131
|
+
channel publishes no checksum, so the receipt's `warn` row records the download's size and sha256 as
|
|
132
|
+
unverified). The [runbook](RUNBOOK.md#harnesses) has the details.
|
|
126
133
|
4. **Throttle.** Asks the container, as UID 1000, whether it has a `throttle` and whether Throttle is configured
|
|
127
134
|
there (`~/.config/throttle/config.toml` exists). Only a configured Throttle gets `throttle setup
|
|
128
135
|
--install-clients`, which then runs without a person. A first setup needs one: it signs in to claude.ai with a
|
|
@@ -163,11 +170,23 @@ replaces only its own files, by rename, and never a `zmx-*` binary a running lan
|
|
|
163
170
|
| `bin/lane-mcp` | The Lanes MCP server for the agents (left out with `--no-mcp`) |
|
|
164
171
|
| `bin/zmx-*` | The pinned zmx holder (optional lanes only) |
|
|
165
172
|
| `VERSION`, `hooks/`, `env.sh` | The release that installed them, the hook definitions `lanes hooks install` merges, and the shell settings new shells source |
|
|
173
|
+
| `bin/throttle-pulse` | The Throttle pulse (1.4.0): reads the daemon's loopback control port every 10 s (GET only) and writes `state/throttle.json`, which the host's statusd reads for the sidebar's fleet block. Installed in every container, **started only in the daemon's container** |
|
|
166
174
|
|
|
167
175
|
It installs no agent hook (that is `lanes hooks install`, opt-in) and, unless `--no-mcp`, adds the agents' MCP entries
|
|
168
176
|
below. A re-run after a server update (the update does it itself for every container `lanes sync` knows) brings every
|
|
169
177
|
file to the installed release.
|
|
170
178
|
|
|
179
|
+
Since 1.4.0 `lanes install-container` also starts the Throttle pulse — but only in the container the Throttle daemon runs
|
|
180
|
+
in: the one `LANES_THROTTLE_CONTAINER` in the server's `~/.config/lanes/site.conf` names, else the container of the
|
|
181
|
+
board's Throttle tab (`tools--throttle-monitor`). There it runs `docker exec -d <container> tmux -L lanes-pulse new -d -s pulse
|
|
182
|
+
~/.lz/bin/throttle-pulse --loop`, a loop on a tmux socket of its own; every other container gets the file and nothing
|
|
183
|
+
runs. The daemon binds its own container's loopback (DEF-1525), which the host cannot reach, so the pulse runs beside it
|
|
184
|
+
and the host reads its file through the container's root. statusd starts it again the same way when its file is more
|
|
185
|
+
than 90 s old while that container runs (at most once every 10 minutes, never on a host under critical pressure).
|
|
186
|
+
Without a token it reads the daemon's open routes only; spend and burn figures need the operator's one-time `lanes
|
|
187
|
+
throttle adopt-token <container>` ([user guide](USERGUIDE.md#fleet)). Every server update re-runs `lanes
|
|
188
|
+
install-container` for each container `lanes sync` knows, so the pulse file follows the installed release.
|
|
189
|
+
|
|
171
190
|
`lanes install-container <name>` (and so `lanes onboard`, step 2 above) also gives the container's agents the Lanes
|
|
172
191
|
MCP server, `~/.lz/bin/lane-mcp`. With it, the agent in a lane can do these things in its own container:
|
|
173
192
|
|
|
@@ -593,7 +612,8 @@ just installed (`libexec/update.py`): `systemctl --user daemon-reload`; `lanes s
|
|
|
593
612
|
lanes-statusd.service`; `systemctl --user try-restart` of `lanes-economy`, `lanes-notify`, `lanes-enrold`,
|
|
594
613
|
`lanes-docsd`, `lanes-mobile`, `lanes-web` and `lanes-studio-host` (each restarted only when it runs; nothing is
|
|
595
614
|
enabled), with `sh install/enable-docs.sh` right after `lanes-docsd`'s (the docs page's Mermaid, step `enable-docs`);
|
|
596
|
-
`lanes install-container <name>` for each container `lanes sync` recorded with a home volume
|
|
615
|
+
`lanes install-container <name>` for each container `lanes sync` recorded with a home volume (the contract files
|
|
616
|
+
again, the Throttle pulse among them since 1.4.0, so the pulse follows the host's release); `lanes
|
|
597
617
|
reload-views`; `lanes ensure --if-layout-changed` (each view whose tabs this release lays out differently is rebuilt when no client is
|
|
598
618
|
attached, and otherwise marked `update ready · reload (r)` in its sidebar footer until it is rebuilt — by `r`, by
|
|
599
619
|
`lanes rebuild <device>`, or by `lanes-ensure.timer` once its last client has left). Every post-commit step runs
|