@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.
@@ -17,11 +17,19 @@ or positively does not exist.
17
17
  The docs cockpit (plan W10 W7): a lane row carries `docs: {n, at}` while docs/counts.json counts documents for it
18
18
  (one stat of that file per cycle), and the document `web_origin`, the https origin the docs page answers on, once
19
19
  main() has learnt it between two cycles (refresh_origin).
20
+
21
+ Fleet facts (spec W11 §2.2): every FLEET_INTERVAL, unless the host is critical, `fleet` — the host's cpu, memory,
22
+ pressure, inotify ceiling and the biggest workspace containers, from `lanes_lib.compute_health(sample=True)` and the
23
+ previous sample kept in the context — and on every cycle `throttle`, the Throttle pulse's `~/.lz/state/throttle.json`
24
+ in the daemon's container (one stat, read only when it moved). The one spawn either adds is `rearm_pulse`, between two
25
+ cycles: one bounded `docker exec -d` at most every PULSE_REARM_EVERY seconds while the pulse file has gone quiet.
20
26
  """
21
27
  import concurrent.futures
22
28
  import json
29
+ import math
23
30
  import os
24
31
  import re
32
+ import stat
25
33
  import subprocess
26
34
  import sys
27
35
  import time
@@ -72,12 +80,39 @@ WEB_PORT = 8443 # bin/lanes SERVE_PORT: `tailscale serve --https=8443` -
72
80
  ORIGIN_TTL = 300 # enrold.TAILNET_TTL
73
81
  HOST_NAME = re.compile(r'(?=.{1,253}\Z)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?'
74
82
  r'(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\Z', re.ASCII)
83
+ # Fleet facts and the Throttle pulse (spec W11 §2.1/§2.2, DM-LANES-FLEET, DM-LANES-THROTTLE-PULSE).
84
+ FLEET_INTERVAL = 10 # seconds between two fleet samples (cpu rates are over this); none taken while critical
85
+ HOSTNAME_FILE = '/etc/hostname' # `host.name`, read once per daemon start (new_context)
86
+ THROTTLE_TOOL = 'tools--throttle-monitor' # lanes_lib.build_registry's tool tab: its container hosts the daemon
87
+ PULSE_DIR = ('home', 'coder', '.lz', 'state') # under /proc/<daemon container init>/root, walked without a link
88
+ PULSE_FILE = 'throttle.json'
89
+ PULSE_MAX = 64 << 10 # a bigger file is not the pulse's: refused, never read
90
+ # A healthy pulse's `at` is seen up to ~84 s old here (its 60 s HEARTBEAT is judged on an int-truncated `at`, one
91
+ # 10 s loop cycle late at a second boundary, a slow cycle's five 3 s GET timeouts, then statusd's own 3 s cycle):
92
+ # older than PULSE_STALE_S is published `stale: true` — the same 90 s after which `rearm_pulse` calls the loop stopped.
93
+ PULSE_STALE_S = 90
94
+ PULSE_REARM_S = 90 # a pulse file quiet for longer is a stopped loop ...
95
+ PULSE_REARM_EVERY = 600 # ... restarted at most this often, never on a critical host
96
+ PULSE_REARM_TIMEOUT = 10
97
+ DAEMON_REFRESH = 300 # site.conf is re-read this often for LANES_THROTTLE_CONTAINER (CONTAINER_REFRESH)
98
+ PULSE_BIN = ('home', 'coder', '.lz', 'bin')
99
+ PULSE_PROGRAM = 'throttle-pulse'
100
+ PULSE_POOLS = ('claude', 'codex', 'kimi') # throttle-pulse POOLS (W12 adds kimi; its overflow is always null)
101
+ PULSE_COUNTS = ('configured', 'usable', 'cooling', 'exhausted', 'benched', 'auth_failed')
102
+ PULSE_OVERFLOW = ('spend_today_usd', 'budget_usd', 'fraction', 'alert_level_pct')
103
+ PULSE_PHASE = re.compile(r'[a-z][a-z0-9_-]{0,31}\Z', re.ASCII)
104
+ PULSE_ERROR_MAX = 160
75
105
 
76
106
 
77
107
  def log(where, error):
78
108
  print(time.strftime('%FT%T'), where, type(error).__name__, error, flush=True)
79
109
 
80
110
 
111
+ def note(where, message):
112
+ """A journal line that is no exception (the pulse re-arm and its outcome, a pulse file that went or came back)."""
113
+ print(time.strftime('%FT%T'), where, message, flush=True)
114
+
115
+
81
116
  # ------------------------------------------------- the viewer rule (A4a)
82
117
  # A Throttle Studio browser attaches with `new-session -t <lane> -s view-<studio>-<ch>`: an independent
83
118
  # current window on the lane's windows. Such a session is a CLIENT of the lane, not a lane of its own, and
@@ -295,34 +330,92 @@ def settled(previous, lane_id, now, raw):
295
330
  return AS.settle(old.get('agent') if isinstance(old, dict) else None, raw, now)
296
331
 
297
332
 
298
- def agent_signals(containers, sessions_by_server):
333
+ # W12 (spec §2): the harness each pane runs, read from the container's /proc and kept per pane pid between cycles —
334
+ # a pane whose own children did not change is not walked again (`agent_state.HarnessCache`; reads only, no spawn).
335
+ HARNESS_CACHE = AS.HarnessCache()
336
+ # The Grok Build observer's title memory between cycles (`agent_state.grok_sessions(..., titles=)`), one dict per
337
+ # container root ({pane key: the last title, held only while it carries `⚠ Action Required`}): unfocused, Grok blinks
338
+ # that prefix away half of every second, so a busy title right after a prefixed one is still an approval. Kept like
339
+ # HARNESS_CACHE; each cycle prunes it to the panes it passed and drops a container root it did not read.
340
+ GROK_TITLES = {}
341
+ # The harnesses whose panes go to the Grok observer: Grok Build itself; a bare shell (the observer reads its title: a
342
+ # Grok title over a tree with no grok left is that harness ended, `grok` / `shell`); and `unknown` WITH a harness pid —
343
+ # the walk's `grok` whose /proc/<pid>/exe could not be read, Grok Build only when active_sessions.json names its pid.
344
+ GROK_OBSERVED = ('grok', 'shell')
345
+
346
+
347
+ def agent_signals(containers, sessions_by_server, now=None):
299
348
  """The per-container inputs `agent_state.merge` needs, read ONCE per container per cycle through the
300
349
  container's root as the host sees it (`/proc/<init pid>/root`), so liveness is judged in the container's pid
301
- namespace and nothing is spawned. -> {container: claude_by_pane}.
350
+ namespace and nothing is spawned. -> {container: {'claude': claude_by_pane, 'kimi': kimi_by_pane,
351
+ 'grok': grok_by_pane}}.
352
+
353
+ Sets `harness` (and `harness_pid` when one was found) on every session row whose harness could be observed, in
354
+ place (W12, spec §2: `agent_state.HarnessCache` over `harness_walk` — the pane's process tree in the container's
355
+ /proc); the Kimi Code observer (`agent_state.kimi_sessions`) reads only for the panes found running `kimi`, and the
356
+ Grok Build observer (`agent_state.grok_sessions` -> `harness_grok.grok_sessions`) gets the ROWS of the panes found
357
+ running `grok`, ending in a bare shell, or running a grok whose exe could not be read (GROK_OBSERVED), with the
358
+ container's GROK_TITLES memory.
302
359
 
303
360
  Also sets `pane_has_child` on the session rows that need it, in place: only a pane whose title Codex wrote
304
- (`parse_codex_title`) over a bare-shell foreground can be a harness that ended into its shell, and only the
305
- child read (`child_liveness`) tells that apart from a live harness launched under `bash -c` or a wrapper. A pane
306
- a live Claude session file already claims is not probed (the file outranks the title). The key is set only for
307
- a known answer: unknown liveness leaves it absent, and an absent key never demotes a title (DEF-1557). A
308
- container whose files cannot be read costs only its own agents, never a lane its `state`."""
309
- claude = {}
361
+ (`parse_codex_title`, gated by the observed harness) over a bare-shell foreground can be a harness that ended into
362
+ its shell, and only the child read (`child_liveness`) tells that apart from a live harness launched under `bash -c`
363
+ or a wrapper. A pane a live Claude session file already claims is not probed (the file outranks the title), nor one
364
+ whose walk found a harness running (alive by that very read). The key is set only for a known answer: unknown
365
+ liveness leaves it absent, and an absent key never demotes a title (DEF-1557). A container whose files cannot be
366
+ read costs only its own agents, never a lane its `state`."""
367
+ signals = {}
368
+ walked = set()
369
+ titled = set()
370
+ home = tuple(part for part in L.CONTAINER_HOME.split('/') if part)
310
371
  for container in sorted({container for container, _socket in sessions_by_server}):
311
372
  info = containers.get(container) or {}
312
373
  if not info.get('pid'):
313
374
  continue
314
375
  root = Path(L.proc_root()) / str(info['pid']) / 'root'
315
376
  try:
316
- claude[container] = AS.claude_sessions(root / L.CONTAINER_HOME.lstrip('/') / '.claude' / 'sessions',
317
- root / 'proc')
377
+ claude = AS.claude_sessions(root / L.CONTAINER_HOME.lstrip('/') / '.claude' / 'sessions', root / 'proc')
318
378
  except Exception as error:
319
379
  log(f'agents {container}', error)
320
- claude[container] = {}
321
- probe = [pane for (owner, _socket), sessions in sessions_by_server.items() if owner == container
322
- for pane in sessions.values()
323
- if str(pane.get('pane_pid') or '').isdigit() and AS.parse_codex_title(pane.get('pane_title'))
324
- and AS._shell_foreground(pane)
325
- and f"{pane.get('session_name')}:{pane.get('window_id')}.{pane.get('pane_id')}" not in claude[container]]
380
+ claude = {}
381
+ signals[container] = mine = {'claude': claude, 'kimi': {}, 'grok': {}}
382
+ panes = [pane for (owner, _socket), sessions in sessions_by_server.items() if owner == container
383
+ for pane in sessions.values() if str(pane.get('pane_pid') or '').isdigit()]
384
+ observers = {'kimi': {}, 'grok': {}}
385
+ for pane in panes:
386
+ try:
387
+ harness, harness_pid = HARNESS_CACHE.harness(root / 'proc', int(pane['pane_pid']))
388
+ except Exception as error:
389
+ log(f'harness {container}', error)
390
+ continue
391
+ walked.add((str(root / 'proc'), int(pane['pane_pid'])))
392
+ if harness is None:
393
+ continue
394
+ pane['harness'] = harness
395
+ if harness_pid:
396
+ pane['harness_pid'] = harness_pid
397
+ if harness == 'kimi' and harness_pid:
398
+ observers['kimi'][AS.pane_key(pane)] = harness_pid
399
+ elif harness in GROK_OBSERVED or (harness == 'unknown' and harness_pid):
400
+ observers['grok'][AS.pane_key(pane)] = pane
401
+ if observers['kimi']:
402
+ try:
403
+ mine['kimi'] = AS.kimi_sessions(root, observers['kimi'], now, home)
404
+ except Exception as error:
405
+ log(f'kimi {container}', error)
406
+ if observers['grok']:
407
+ titles = GROK_TITLES.setdefault(str(root), {})
408
+ titled.add(str(root))
409
+ try:
410
+ mine['grok'] = AS.grok_sessions(root, observers['grok'], AS.GROK_HOME, home=L.CONTAINER_HOME,
411
+ titles=titles)
412
+ except Exception as error:
413
+ log(f'grok {container}', error)
414
+ for gone in set(titles) - set(observers['grok']):
415
+ del titles[gone]
416
+ probe = [pane for pane in panes
417
+ if AS.parse_codex_title(pane.get('pane_title'), pane.get('harness')) and AS._shell_foreground(pane)
418
+ and pane.get('harness') not in AS.AGENT_HARNESSES and AS.pane_key(pane) not in claude]
326
419
  if not probe:
327
420
  continue
328
421
  try:
@@ -334,7 +427,15 @@ def agent_signals(containers, sessions_by_server):
334
427
  known = live.get(int(pane['pane_pid']))
335
428
  if known is not None:
336
429
  pane['pane_has_child'] = known
337
- return claude
430
+ HARNESS_CACHE.prune(walked)
431
+ for gone in set(GROK_TITLES) - titled:
432
+ del GROK_TITLES[gone]
433
+ return signals
434
+
435
+
436
+ def merged(pane, signals, sidecar):
437
+ """`agent_state.merge` of one pane over its container's `agent_signals` and its lane's hook sidecar."""
438
+ return AS.merge(pane, signals.get('claude', {}), sidecar, signals.get('kimi', {}), signals.get('grok', {}))
338
439
 
339
440
 
340
441
  SIDECAR_MAX_AGE = 3600 # a hook sidecar whose last event is older than this loses to the live signals
@@ -532,10 +633,10 @@ def observe(registry, containers, live, previous=None, report=None):
532
633
  report['cut'] = len(cut)
533
634
  # Task 5: the agent signals of every container, read once per cycle over all of its servers' sessions.
534
635
  try:
535
- claude = agent_signals(containers, {key: sessions for key, (_o, sessions, _a) in probed.items()})
636
+ signals = agent_signals(containers, {key: sessions for key, (_o, sessions, _a) in probed.items()}, now)
536
637
  except Exception as error:
537
638
  log('agents', error)
538
- claude = {}
639
+ signals = {}
539
640
  # Task 8: the hook sidecars `lane-signal` writes, one directory listing per container per cycle. They outrank the
540
641
  # session file and the title in `agent_state.merge`; a container without hooks costs one failed listdir.
541
642
  try:
@@ -620,8 +721,8 @@ def observe(registry, containers, live, previous=None, report=None):
620
721
  entry['note'] = 'session name outside the lane declaration grammar'
621
722
  found = lane_entry(pane, now, activities.get(name), viewers.get(name, []),
622
723
  settled(previous, entry['id'], now,
623
- AS.merge(pane, claude.get(container, {}),
624
- sidecars.get(container, {}).get(entry['id']))))
724
+ merged(pane, signals.get(container, {}),
725
+ sidecars.get(container, {}).get(entry['id']))))
625
726
  except Exception as error:
626
727
  log(f'observe {container}/{socket}/{name}', error)
627
728
  continue
@@ -641,8 +742,8 @@ def observe(registry, containers, live, previous=None, report=None):
641
742
  try:
642
743
  lanes[lane['id']] = lane_entry(pane, now, activities.get(session), viewers.get(session, []),
643
744
  settled(previous, lane['id'], now,
644
- AS.merge(pane, claude.get(container, {}),
645
- sidecars.get(container, {}).get(lane['id']))))
745
+ merged(pane, signals.get(container, {}),
746
+ sidecars.get(container, {}).get(lane['id']))))
646
747
  except Exception as error: # a torn row: the session is there, its details are not readable
647
748
  log(f"observe {lane['id']}", error)
648
749
  lanes[lane['id']] = keep(previous, lane['id'], 'error')
@@ -1018,7 +1119,8 @@ def new_context():
1018
1119
  and HOST_NAME.match(origin[len('https://'):-len(f':{WEB_PORT}')]))
1019
1120
  return {'registry': None, 'stamp': 0, 'containers': {}, 'containers_at': 0.0,
1020
1121
  'payload': seed, 'published': None, 'status_at': written, 'cut': 0, 'unwalkable': [],
1021
- 'container_status': {}, 'container_status_errors': {}, 'web_origin': origin if valid else None}
1122
+ 'container_status': {}, 'container_status_errors': {}, 'web_origin': origin if valid else None,
1123
+ 'host_name': read_host_name()}
1022
1124
 
1023
1125
 
1024
1126
  # ------------------------------------------- the in-container projection (LNS-32)
@@ -1124,14 +1226,312 @@ def _container_status_error(errors, container, error):
1124
1226
  log(f'container status {container}', error)
1125
1227
 
1126
1228
 
1229
+ # ------------------------------------------ fleet facts and the Throttle pulse (W11)
1230
+ # `fleet` (DM-LANES-FLEET) is the host and its biggest workspace containers, `throttle` (DM-LANES-THROTTLE-PULSE) the
1231
+ # Throttle pulse's last document. Both are additive keys of status.json; the sidebar's pinned block and `lanes doctor`
1232
+ # read them there and nowhere else.
1233
+
1234
+
1235
+ def read_host_name(path=None):
1236
+ """`host.name`: the first line of /etc/hostname, else the kernel's node name, published only when it is a DNS host
1237
+ name (it is drawn into terminals) — else None. Read once per daemon start (new_context), never per cycle."""
1238
+ try:
1239
+ name = Path(path or HOSTNAME_FILE).read_text().split('\n', 1)[0].strip()
1240
+ except (OSError, ValueError):
1241
+ name = ''
1242
+ name = name or os.uname().nodename
1243
+ return name if HOST_NAME.match(name) else None
1244
+
1245
+
1246
+ def fleet_due(ctx, now):
1247
+ """True when FLEET_INTERVAL has passed since the cycle that took the last fleet sample (or none was taken yet)."""
1248
+ last = ctx.get('fleet_started')
1249
+ return last is None or now - last >= FLEET_INTERVAL
1250
+
1251
+
1252
+ def update_fleet(ctx, health, sample, due, now):
1253
+ """ctx['fleet'] after this cycle -> it (None until a first sample). A new sample (compute_health's, taken when
1254
+ `due`) replaces the facts (`stale: false`) and is kept as the previous sample of the next one — in the context,
1255
+ never in a global. A due sample that was not taken (a critical host: compute_health took none; a failed read)
1256
+ keeps the last facts, `stale: true`, until one is."""
1257
+ if sample is not None:
1258
+ ctx['fleet'] = L.fleet_facts(sample, ctx.get('fleet_sample'), health, ctx.get('host_name'))
1259
+ ctx['fleet_sample'], ctx['fleet_started'] = sample, now
1260
+ elif due and ctx.get('fleet') and ctx['fleet'].get('stale') is not True:
1261
+ ctx['fleet'] = dict(ctx['fleet'], stale=True)
1262
+ return ctx.get('fleet')
1263
+
1264
+
1265
+ def site_settings():
1266
+ """The server's site.conf LANES_* values, by the enrolment service's reader (read, never executed)."""
1267
+ import enrold
1268
+ return enrold.server_site()
1269
+
1270
+
1271
+ def daemon_container(registry, site):
1272
+ """The container the Throttle daemon runs in: site.conf's LANES_THROTTLE_CONTAINER, else the container of the
1273
+ registry's `tools--throttle-monitor` tab (lanes_lib.build_registry: the monitor host most boards name), else
1274
+ None. A name that is no container name is None — never a guess in its place."""
1275
+ name = (site or {}).get('LANES_THROTTLE_CONTAINER')
1276
+ if not name:
1277
+ tools = (registry or {}).get('tools') if isinstance(registry, dict) else None
1278
+ tool = next((t for t in tools or [] if isinstance(t, dict) and t.get('id') == THROTTLE_TOOL), None)
1279
+ name = (tool or {}).get('container')
1280
+ return name if isinstance(name, str) and L.IDENT.match(name) else None
1281
+
1282
+
1283
+ def resolve_daemon(ctx, now):
1284
+ """Which container hosts the daemon (site.conf, else the loaded registry) and its init pid: the one statusd
1285
+ tracks for that container each cycle, else — a container no lane runs in — the one `lanes sync` recorded in
1286
+ containers.json at this look. Never per cycle (the pulse adds ONE stat per cycle, nothing more): looked at again
1287
+ when the loaded registry changed, at most every FLEET_INTERVAL while the last snapshot could not open the
1288
+ container's ~/.lz/state (a container restarted under a new init pid), and every DAEMON_REFRESH s otherwise (an
1289
+ edited site.conf)."""
1290
+ last, registry = ctx.get('daemon_at'), ctx.get('registry')
1291
+ if last is not None and ctx.get('daemon_registry') is registry:
1292
+ wait = FLEET_INTERVAL if (ctx.get('pulse') or {}).get('no_dir') else DAEMON_REFRESH
1293
+ if now - last < wait:
1294
+ return
1295
+ ctx['daemon_at'], ctx['daemon_registry'] = now, registry
1296
+ try:
1297
+ site = site_settings()
1298
+ except Exception as error:
1299
+ log('site.conf', error)
1300
+ site = {}
1301
+ name = daemon_container(ctx.get('registry'), site)
1302
+ recorded = None
1303
+ if name and name not in (ctx.get('containers') or {}):
1304
+ known = L.read_json(L.STATE / 'containers.json', {})
1305
+ entry = known.get(name) if isinstance(known, dict) else None
1306
+ recorded = entry.get('pid') if isinstance(entry, dict) else None
1307
+ ctx['daemon'] = {'name': name, 'pid': recorded} if name else None
1308
+
1309
+
1310
+ def daemon_info(ctx):
1311
+ """(container name, init pid or None) of the daemon's container, or None when no container is named."""
1312
+ daemon = ctx.get('daemon')
1313
+ if not daemon:
1314
+ return None
1315
+ tracked = (ctx.get('containers') or {}).get(daemon['name'])
1316
+ pid = tracked.get('pid') if isinstance(tracked, dict) else daemon.get('pid')
1317
+ return daemon['name'], pid if isinstance(pid, int) and pid > 0 else None
1318
+
1319
+
1320
+ def _number(value):
1321
+ return value if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) else None
1322
+
1323
+
1324
+ def _count(value):
1325
+ return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
1326
+
1327
+
1328
+ def _flag(value):
1329
+ return value if isinstance(value, bool) else None
1330
+
1331
+
1332
+ def pulse_projection(data):
1333
+ """The pulse's bytes as status.json carries them, or None when they are no v1 pulse document (spec W11 §2.1).
1334
+
1335
+ The daemon's container writes that file, and the sidebar draws what is in it into terminals: nothing is copied as
1336
+ such. Only the spec's keys are, each of its own type — counts non-negative integers; fractions, dollars and
1337
+ timestamps finite numbers; flags booleans; `pressure_phase` one short lowercase word; `error` printable characters
1338
+ only, at most PULSE_ERROR_MAX — anything else null, unknown keys and pools dropped. No escape sequence rides
1339
+ through, and a pool the pulse did not write is not invented."""
1340
+ if data is None:
1341
+ return None
1342
+ try:
1343
+ document = json.loads(data.decode('utf-8'))
1344
+ except (UnicodeDecodeError, ValueError, RecursionError):
1345
+ return None
1346
+ if not isinstance(document, dict) or _count(document.get('v')) != 1 or _number(document.get('at')) is None:
1347
+ return None
1348
+ pools, overflow = {}, {}
1349
+ for pool in PULSE_POOLS:
1350
+ row = (document.get('pools') or {}).get(pool) if isinstance(document.get('pools'), dict) else None
1351
+ if isinstance(row, dict):
1352
+ phase = row.get('pressure_phase')
1353
+ pools[pool] = dict({key: _count(row.get(key)) for key in PULSE_COUNTS},
1354
+ overflow_ready=_flag(row.get('overflow_ready')),
1355
+ pressure_phase=phase if isinstance(phase, str) and PULSE_PHASE.match(phase) else None,
1356
+ spill_fraction=_number(row.get('spill_fraction')),
1357
+ overflow_in_flight=_count(row.get('overflow_in_flight')),
1358
+ upstream_429_total=_count(row.get('upstream_429_total')),
1359
+ worst_7d=_number(row.get('worst_7d')), worst_5h=_number(row.get('worst_5h')),
1360
+ soonest_reset=_number(row.get('soonest_reset')))
1361
+ money = (document.get('overflow') or {}).get(pool) if isinstance(document.get('overflow'), dict) else None
1362
+ if isinstance(money, dict):
1363
+ overflow[pool] = {key: _number(money.get(key)) for key in PULSE_OVERFLOW}
1364
+ error = document.get('error')
1365
+ error = ''.join(c for c in error if c.isprintable())[:PULSE_ERROR_MAX] if isinstance(error, str) else ''
1366
+ return {'v': 1, 'at': int(document['at']), 'up': _flag(document.get('up')), 'tokened': _flag(document.get('tokened')),
1367
+ 'pools': pools, 'overflow': overflow, 'error': error or None}
1368
+
1369
+
1370
+ def throttle_snapshot(ctx, now):
1371
+ """The `throttle` block of status.json, or None when no daemon container is named (no Throttle here: no key).
1372
+
1373
+ ONE stat of the pulse file per cycle — through /proc/<daemon container init>/root, every directory down to
1374
+ ~/.lz/state walked with O_NOFOLLOW (lanes_lib.open_real_dir: the container owns it, and a link planted there would
1375
+ resolve against the HOST's root) and the file itself lstat'ed — and a read only when its (init pid, mtime, inode,
1376
+ size) moved: `read_regular_at`, a regular file of at most PULSE_MAX bytes, never a link, never a FIFO. What it
1377
+ holds reaches status.json only through `pulse_projection`. The last values are kept when the file goes away, is
1378
+ refused or does not parse, `stale: true` then — and whenever their `at` is more than PULSE_STALE_S old by this
1379
+ clock; with none ever read the block is `{"stale": true}`. ctx['pulse']['mtime'] (the file's own age, None while
1380
+ it cannot be seen) is what `rearm_pulse` judges."""
1381
+ found = daemon_info(ctx)
1382
+ if found is None:
1383
+ ctx.pop('pulse', None)
1384
+ return None
1385
+ name, pid = found
1386
+ pulse = ctx.get('pulse')
1387
+ if not isinstance(pulse, dict) or pulse.get('name') != name:
1388
+ pulse = ctx['pulse'] = {'name': name}
1389
+ stamp = mtime = fd = None
1390
+ why = None if pid else 'container not running'
1391
+ pulse['no_dir'] = not pid
1392
+ if pid:
1393
+ try:
1394
+ fd = L.open_real_dir(Path(L.proc_root()) / str(pid) / 'root', PULSE_DIR)
1395
+ except FileNotFoundError: # the init is gone (a restart: resolve_daemon looks again soon), or no ~/.lz/state
1396
+ pulse['no_dir'] = not pid_alive(pid)
1397
+ why = 'container not running' if pulse['no_dir'] else 'no ~/.lz/state'
1398
+ except (OSError, L.LanesError) as error:
1399
+ why = f'~/.lz/state refused ({type(error).__name__})'
1400
+ if fd is not None:
1401
+ try:
1402
+ info = os.stat(PULSE_FILE, dir_fd=fd, follow_symlinks=False)
1403
+ stamp, mtime = (pid, info.st_mtime_ns, info.st_ino, info.st_size), info.st_mtime
1404
+ if stamp != pulse.get('stamp'):
1405
+ document = pulse_projection(L.read_regular_at(fd, PULSE_FILE, PULSE_MAX))
1406
+ pulse['valid'] = document is not None
1407
+ if document is not None:
1408
+ pulse['doc'] = document
1409
+ if not pulse.get('valid'):
1410
+ why = 'refused (no regular file of at most 64 KiB holding a v1 pulse document)'
1411
+ except FileNotFoundError:
1412
+ why = 'absent'
1413
+ except OSError as error:
1414
+ why = f'unreadable ({type(error).__name__})'
1415
+ finally:
1416
+ os.close(fd)
1417
+ pulse['stamp'], pulse['mtime'] = stamp, mtime
1418
+ if why != pulse.get('why', 'unseen'):
1419
+ note('throttle pulse', f'{name}: {why or "ok"}')
1420
+ pulse['why'] = why
1421
+ document = pulse.get('doc')
1422
+ stale = why is not None or document is None or now - document['at'] > PULSE_STALE_S
1423
+ return dict(document or {}, stale=stale)
1424
+
1425
+
1426
+ def pulse_rearm_argv(container):
1427
+ """The one spawn of the fleet feature: the pulse loop started detached in its own tmux server in the container,
1428
+ as the container user (CONTAINER_UID, like every other exec Lanes makes into a container: the loop must write
1429
+ /home/coder/.lz/state, and a later re-arm must find its tmux server) with HOME=CONTAINER_HOME whatever the image
1430
+ sets (the pulse's `~/.lz/state` and `~/.lz/throttle-token` defaults expand against it) — byte for byte the argv
1431
+ `lanes install-container` starts it with (bin/lanes `pulse_argv`). argv only, no shell."""
1432
+ return ['docker', 'exec', '-d', '-u', str(L.CONTAINER_UID), '-e', f'HOME={L.CONTAINER_HOME}', container, 'tmux',
1433
+ '-L', 'lanes-pulse', 'new', '-d', '-s', 'pulse', f'{L.CONTAINER_HOME}/.lz/bin/{PULSE_PROGRAM}', '--loop']
1434
+
1435
+
1436
+ def pulse_installed(pid):
1437
+ """True when ~/.lz/bin/throttle-pulse is a regular file in the container (`lanes install-container` shipped it),
1438
+ looked at through /proc/<pid>/root without following a link."""
1439
+ try:
1440
+ fd = L.open_real_dir(Path(L.proc_root()) / str(pid) / 'root', PULSE_BIN)
1441
+ except (OSError, L.LanesError):
1442
+ return False
1443
+ try:
1444
+ return stat.S_ISREG(os.stat(PULSE_PROGRAM, dir_fd=fd, follow_symlinks=False).st_mode)
1445
+ except OSError:
1446
+ return False
1447
+ finally:
1448
+ os.close(fd)
1449
+
1450
+
1451
+ def rearm_pulse(ctx, level, now=None):
1452
+ """main()'s restart of a stopped Throttle pulse, between two cycles and never inside one -> True when it spawned.
1453
+
1454
+ Only while a daemon container is named and its init is alive, the host is not `critical`, the pulse file is
1455
+ older than PULSE_REARM_S by its mtime (or cannot be seen at all), and the last attempt is at least
1456
+ PULSE_REARM_EVERY seconds ago. An attempt is counted whatever comes of it, so a failing one is not retried every
1457
+ cycle; it starts nothing when ~/.lz/bin/throttle-pulse is not there. Then ONE bounded `docker exec -d`
1458
+ (pulse_rearm_argv, PULSE_REARM_TIMEOUT s), its outcome journalled."""
1459
+ now = time.time() if now is None else now
1460
+ found = daemon_info(ctx)
1461
+ if level == 'critical' or found is None:
1462
+ return False
1463
+ name, pid = found
1464
+ mtime = (ctx.get('pulse') or {}).get('mtime')
1465
+ if mtime is not None and now - mtime <= PULSE_REARM_S:
1466
+ return False
1467
+ last = ctx.get('pulse_rearm_at')
1468
+ if last is not None and now - last < PULSE_REARM_EVERY:
1469
+ return False
1470
+ if not pid_alive(pid):
1471
+ return False
1472
+ ctx['pulse_rearm_at'] = now
1473
+ if not pulse_installed(pid):
1474
+ note('throttle pulse', f'{name}: quiet, and ~/.lz/bin/{PULSE_PROGRAM} is not installed there '
1475
+ f'(lanes install-container {name}): not started')
1476
+ return False
1477
+ argv = pulse_rearm_argv(name)
1478
+ rc, _out, err = L.run_bounded(argv, PULSE_REARM_TIMEOUT)
1479
+ age = 'not seen' if mtime is None else f'{int(now - mtime)} s old'
1480
+ if rc is None:
1481
+ outcome = f'no answer within {PULSE_REARM_TIMEOUT} s'
1482
+ else:
1483
+ last_line = err.strip().splitlines()[-1][:160] if rc and err.strip() else ''
1484
+ outcome = f'exit {rc}' + (f': {last_line}' if last_line else '')
1485
+ note('throttle pulse', f'{name}: pulse file {age}, re-armed ({outcome}): {" ".join(argv)}')
1486
+ return True
1487
+
1488
+
1489
+ def compared(payload):
1490
+ """`payload` as the rewrite decision sees it: without the sample time of `fleet` (new every FLEET_INTERVAL) and
1491
+ of `throttle` (the pulse's 60-s heartbeat) — the quantized facts and the stale marks are what may move a rewrite
1492
+ (like `health`, which is not in the payload at all); a block whose only news is its `at` waits for HEARTBEAT."""
1493
+ if not isinstance(payload, dict):
1494
+ return payload
1495
+ out = dict(payload)
1496
+ for key in ('fleet', 'throttle'):
1497
+ if isinstance(out.get(key), dict):
1498
+ out[key] = {name: value for name, value in out[key].items() if name != 'at'}
1499
+ return out
1500
+
1501
+
1502
+ def with_fleet(ctx, payload, health, sample, due, now):
1503
+ """`payload` plus `fleet` and `throttle`, each failing alone: a fleet that could not be computed is the last one,
1504
+ stale; a pulse that could not be looked at is the last block published, stale."""
1505
+ out = dict(payload)
1506
+ try:
1507
+ fleet = update_fleet(ctx, health, sample, due, now)
1508
+ except Exception as error:
1509
+ log('fleet', error)
1510
+ fleet = dict(ctx['fleet'], stale=True) if ctx.get('fleet') else None
1511
+ if fleet:
1512
+ out['fleet'] = fleet
1513
+ try:
1514
+ resolve_daemon(ctx, now)
1515
+ throttle = throttle_snapshot(ctx, now)
1516
+ except Exception as error:
1517
+ log('throttle pulse', error)
1518
+ last = (ctx.get('payload') or {}).get('throttle')
1519
+ throttle = dict(last, stale=True) if isinstance(last, dict) else None
1520
+ if throttle is not None:
1521
+ out['throttle'] = throttle
1522
+ return out
1523
+
1524
+
1127
1525
  def cycle(ctx, reconcile=True):
1128
1526
  """Observe once and ALWAYS publish -> (health level, payload or None)."""
1129
1527
  started = time.time()
1130
- try:
1131
- health = L.compute_health()
1528
+ due = fleet_due(ctx, started)
1529
+ try: # every FLEET_INTERVAL the fleet counters too (W11; compute_health takes none on a critical host)
1530
+ health = L.compute_health(sample=due)
1132
1531
  except Exception as error:
1133
1532
  log('health', error)
1134
1533
  health = {}
1534
+ sample = health.pop('sample', None)
1135
1535
  level = health.get('level', 'ok')
1136
1536
  try:
1137
1537
  payload = build_payload(ctx, level, reconcile)
@@ -1139,10 +1539,12 @@ def cycle(ctx, reconcile=True):
1139
1539
  log('cycle', error)
1140
1540
  payload = None
1141
1541
  if payload is not None:
1542
+ payload = with_fleet(ctx, payload, health, sample, due, started)
1142
1543
  ctx['payload'] = payload
1143
1544
  # Rewrite only on change (plus a heartbeat) so sidebars do not repaint for nothing. `health` changes every
1144
- # cycle and is therefore not part of the comparison.
1145
- if payload != ctx['published'] or started - ctx['status_at'] > HEARTBEAT:
1545
+ # cycle and is therefore not part of the comparison; nor is the fleet's sample time (`compared`).
1546
+ if (ctx['published'] is None or compared(payload) != compared(ctx['published'])
1547
+ or started - ctx['status_at'] > HEARTBEAT):
1146
1548
  try:
1147
1549
  written = time.time()
1148
1550
  L.atomic_write(L.STATE / 'status.json', json.dumps(dict(payload, health=health, updated_at=written,
@@ -1239,6 +1641,10 @@ def main():
1239
1641
  level, _payload = cycle(ctx)
1240
1642
  # The docs links' origin (plan W10 W7): a bounded look between two cycles, never inside one.
1241
1643
  refresh_origin(ctx, level)
1644
+ try: # W11: a pulse file gone quiet -> one bounded re-arm, at most every PULSE_REARM_EVERY s, never critical
1645
+ rearm_pulse(ctx, level)
1646
+ except Exception as error:
1647
+ log('throttle pulse', error)
1242
1648
  # Hot-reload: check if any terminals.json changed since we last synced.
1243
1649
  # Rate-limited to at most one triggered sync per HOT_SYNC_EVERY_S to avoid thrashing.
1244
1650
  if time.time() - last_triggered_sync > HOT_SYNC_EVERY_S and ctx['registry']:
@@ -93,7 +93,18 @@ MARKS = {'cursor': ('▌', '|', 'accent'), 'viewing': ('›', '=', 'accent'), 'v
93
93
  'ok': ('✓', 'OK', 'tone-done'), 'fail': ('✗', 'X', 'tone-urgent'),
94
94
  # The docs cockpit on the upload strip (` ↑ uploads · ▤ docs 3 (U)`, plan W10 W7): the strip's own tone.
95
95
  # Seven-bit `#` is free on that row (`^` uploads, `v` armed, `-` sep are the others).
96
- 'docs': ('▤', '#', 'ink-dim')}
96
+ 'docs': ('▤', '#', 'ink-dim'),
97
+ # The fleet block (W11, spec 2026-10-11 §3), pinned above the upload strip: column 2 of each row says what
98
+ # the row is about — Throttle ⚙, a provider's subscription pool ◆, the host and its containers ▣ — in the
99
+ # row's worst tone; then a provider's availability bar, ten cells of usable ▰ · cooling ▱ · benched ▪ ·
100
+ # dead ·. The seven-bit twins are unique among the block's own marks and against every other mark its rows
101
+ # draw (`-` sep, `?` stale, `OK` / `!` / `X` the folded line's verdict). `#` stays the docs twin alone; the
102
+ # unicode `·` of a dead cell is the spec's (it shares the separator's glyph, never its place: a bar has no
103
+ # blank inside it).
104
+ 'fleet-throttle': ('⚙', '@', 'ink-dim'), 'fleet-provider': ('◆', '*', 'ink-dim'),
105
+ 'fleet-host': ('▣', '[', 'ink-dim'),
106
+ 'bar-usable': ('▰', '=', 'tone-done'), 'bar-cooling': ('▱', '~', 'ink-dim'),
107
+ 'bar-benched': ('▪', ':', 'tone-attention'), 'bar-dead': ('·', '.', 'ink-faint')}
97
108
  ECONOMY_TAIL = {'starving': ('⚠', '!', 'tone-attention'), 'downgraded': ('↓', 'v', 'tone-attention'), 'paid': ('$', '$', 'ink-dim')}
98
109
  MOBILE_AGENT_LABELS = {'needs_approval': 'Needs approval', 'needs_input': 'Needs you', 'working': 'Working',
99
110
  'done': 'Done', 'errored': 'Error', 'shell': 'Shell', 'idle': 'Idle'}