@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
|
@@ -93,7 +93,7 @@ def osc_text(text):
|
|
|
93
93
|
# ----------------------------------------------------------------- economy (plan Task 12, spec §4.2/§4.6(c))
|
|
94
94
|
ECONOMY_SCHEMA = 1 # economy.json `v` this reader understands (Task 11, DM-LANES-ECONOMY; additive)
|
|
95
95
|
STARVING_AGE = 600 # seconds without a request while the pane is producing output
|
|
96
|
-
# economy.json older than this by its own `at`, measured with OUR clock, is a frozen picture (DEF-
|
|
96
|
+
# economy.json older than this by its own `at`, measured with OUR clock, is a frozen picture (DEF-1528): the poller
|
|
97
97
|
# rewrites the file at least every economy_poll.HEARTBEAT (60 s) even when nothing changed, so three missed heartbeats
|
|
98
98
|
# mean the poller stopped (crashed, disabled, or a host too loaded to run it) — the same own-clock rule as HEALTH_STALE.
|
|
99
99
|
ECONOMY_STALE = 180
|
|
@@ -564,7 +564,7 @@ class Sidebar:
|
|
|
564
564
|
|
|
565
565
|
def economy_aged(self, now):
|
|
566
566
|
"""True when the loaded economy.json is older than ECONOMY_STALE by its own `at` against `now` (our clock):
|
|
567
|
-
the poller stopped, so every figure in it is frozen (DEF-
|
|
567
|
+
the poller stopped, so every figure in it is frozen (DEF-1528). False without a document or without a numeric
|
|
568
568
|
`at` — nothing is known, so nothing is claimed (health_banner's rule for a missing health file)."""
|
|
569
569
|
at = number(self.economy.get('at')) if self.economy else None
|
|
570
570
|
return at is not None and now - at > ECONOMY_STALE
|
|
@@ -682,7 +682,7 @@ class Sidebar:
|
|
|
682
682
|
marks the fleet `stale` (a frozen picture is not live), ink-dim with the stale mark in front."""
|
|
683
683
|
fleet = self.economy.get('fleet') or {}
|
|
684
684
|
# Frozen two ways: the poller kept its last fleet strip (`/status` down), or the poller itself stopped and the
|
|
685
|
-
# whole document is older than ECONOMY_STALE by our clock (DEF-
|
|
685
|
+
# whole document is older than ECONOMY_STALE by our clock (DEF-1528). Both are painted alike.
|
|
686
686
|
stale = fleet.get('stale') is True or self.economy_aged(now)
|
|
687
687
|
spend, budget = number(fleet.get('overflow_spend_today_usd')), number(fleet.get('overflow_budget_usd'))
|
|
688
688
|
fraction = number(fleet.get('spent_fraction'))
|
|
@@ -766,7 +766,7 @@ class Sidebar:
|
|
|
766
766
|
"""(glyph, role) for column 34, by priority (spec §4.2): not-observed beats everything, then what is
|
|
767
767
|
actionable — stale `?` > starving ⚠ > downgraded ↓ > paid `$` > a blank. `econ` is the lane's economy.json
|
|
768
768
|
object from `self.economy` or None (no poller, or a lane the poller has not joined: the tail is blank).
|
|
769
|
-
A FROZEN economy figure keeps its glyph but loses the attention tone (ink-dim, DEF-
|
|
769
|
+
A FROZEN economy figure keeps its glyph but loses the attention tone (ink-dim, DEF-1528): the lane record
|
|
770
770
|
carries `stale: true` (the poller kept it while `/sessions` was down), or — with `now` — the whole document
|
|
771
771
|
is older than ECONOMY_STALE. A frozen ⚠ is last-known, not a live alarm; the `?` stays statusd's mark."""
|
|
772
772
|
if observed.get('stale'):
|
|
@@ -906,7 +906,7 @@ class Sidebar:
|
|
|
906
906
|
"""The cursor is a LANE (spec §4.5). `entries` is a freshly computed list, `cursor_id` the lane the cursor was
|
|
907
907
|
on before it: the index is re-found from the id, and `top` shifts by the rows that appeared above it, so a lane
|
|
908
908
|
that moves between sections never moves the operator's eye. Then the index is clamped. render() and
|
|
909
|
-
render_mobile() bound the shifted `top` by the list (DEF-
|
|
909
|
+
render_mobile() bound the shifted `top` by the list (DEF-1514): a list that fits the body is never scrolled."""
|
|
910
910
|
if cursor_id:
|
|
911
911
|
index = next((i for i, e in enumerate(entries) if e.get('id') == cursor_id), None)
|
|
912
912
|
if index is not None and index != self.cursor:
|
|
@@ -1107,7 +1107,7 @@ class Sidebar:
|
|
|
1107
1107
|
return
|
|
1108
1108
|
try:
|
|
1109
1109
|
# Routed by the registry SHAPE, never by the id's spelling: an adopted session keeps its `--x-` id
|
|
1110
|
-
# but its name authority is registered-sessions.json, not user-sessions/ (DEF-
|
|
1110
|
+
# but its name authority is registered-sessions.json, not user-sessions/ (DEF-1556).
|
|
1111
1111
|
if entry.get('registered_session'):
|
|
1112
1112
|
R.rename(entry['id'], new_name)
|
|
1113
1113
|
self.message = f'Renamed → {new_name}'
|
|
@@ -1142,7 +1142,7 @@ class Sidebar:
|
|
|
1142
1142
|
return
|
|
1143
1143
|
# Gated the way `start_registration` is: only an UNREGISTERED, reconnectable tmux session. A repository
|
|
1144
1144
|
# lane or an adopted (`registered_session`) lane is already registered; promoting it would append a
|
|
1145
|
-
# second declaration of the same binding to the repository's terminals.json (DEF-
|
|
1145
|
+
# second declaration of the same binding to the repository's terminals.json (DEF-1555).
|
|
1146
1146
|
if entry.get('registered_session') or entry.get('repo') or entry.get('source'):
|
|
1147
1147
|
self.message = 'Already a registered lane; nothing to promote'
|
|
1148
1148
|
self.dirty = True
|
|
@@ -276,7 +276,7 @@ def agent_signals(containers, sessions_by_server):
|
|
|
276
276
|
(`parse_codex_title`) over a bare-shell foreground can be a harness that ended into its shell, and only the
|
|
277
277
|
child read (`child_liveness`) tells that apart from a live harness launched under `bash -c` or a wrapper. A pane
|
|
278
278
|
a live Claude session file already claims is not probed (the file outranks the title). The key is set only for
|
|
279
|
-
a known answer: unknown liveness leaves it absent, and an absent key never demotes a title (DEF-
|
|
279
|
+
a known answer: unknown liveness leaves it absent, and an absent key never demotes a title (DEF-1557). A
|
|
280
280
|
container whose files cannot be read costs only its own agents, never a lane its `state`."""
|
|
281
281
|
claude = {}
|
|
282
282
|
for container in sorted({container for container, _socket in sessions_by_server}):
|
|
@@ -312,7 +312,7 @@ def agent_signals(containers, sessions_by_server):
|
|
|
312
312
|
SIDECAR_MAX_AGE = 3600 # a hook sidecar whose last event is older than this loses to the live signals
|
|
313
313
|
# The hook sidecars' own directory under the container home. NOT `.lz/state` itself: lane-run writes each zmx lane's
|
|
314
314
|
# `{phase, ...}` record there as `<zmx name>.json` and exports that name as LANE_ID, so a shared name would let a hook
|
|
315
|
-
# overwrite the record `observe_zmx_container` and the container `lane ls` read (DEF-
|
|
315
|
+
# overwrite the record `observe_zmx_container` and the container `lane ls` read (DEF-1572).
|
|
316
316
|
SIDECAR_DIR = ('.lz', 'state', 'hooks')
|
|
317
317
|
|
|
318
318
|
|
|
@@ -337,7 +337,7 @@ def agent_sidecars(containers, wanted, now=None):
|
|
|
337
337
|
~/.lz/state/hooks per cycle, and a read only for the `.json` names in it — lane-run's phase records in the parent
|
|
338
338
|
directory are never listed or read here. A container whose directory cannot be listed simply has no sidecars —
|
|
339
339
|
absent is "no hook installed", never an error a lane pays for. A zmx lane's hook sidecar is named by its zmx
|
|
340
|
-
session (lane-run's LANE_ID), not by its published `<ws>--z-<name>` id, so it joins no lane yet (DEF-
|
|
340
|
+
session (lane-run's LANE_ID), not by its published `<ws>--z-<name>` id, so it joins no lane yet (DEF-1573)."""
|
|
341
341
|
out = {}
|
|
342
342
|
for container in sorted(wanted):
|
|
343
343
|
info = containers.get(container) or {}
|
|
@@ -757,7 +757,7 @@ def observe_zmx_container(container):
|
|
|
757
757
|
name = fields.get('name', '')
|
|
758
758
|
if not L.IDENT.match(name):
|
|
759
759
|
continue
|
|
760
|
-
# The meta and state sidecars are written INSIDE the container (DEF-
|
|
760
|
+
# The meta and state sidecars are written INSIDE the container (DEF-1542): each one is read as an object or
|
|
761
761
|
# nothing, each field as one printable line or nothing, and the workspace must be a lanes identifier — it
|
|
762
762
|
# names a host file below. A malformed sidecar costs its own row's labels, never the container's other rows.
|
|
763
763
|
# `lane new` writes exactly these shapes (`ws` checked against IDENT or empty, `zmx_version` a string).
|
|
@@ -782,12 +782,22 @@ def observe_zmx_container(container):
|
|
|
782
782
|
return lanes, groups
|
|
783
783
|
|
|
784
784
|
|
|
785
|
-
def observe_zmx(containers, previous=None):
|
|
786
|
-
"""zmx-held lanes describe themselves (labels + meta sidecar): no registry edit is needed to see one.
|
|
785
|
+
def observe_zmx(containers, previous=None, listed=None):
|
|
786
|
+
"""zmx-held lanes describe themselves (labels + meta sidecar): no registry edit is needed to see one.
|
|
787
|
+
|
|
788
|
+
`listed`, when given, receives `{container: unix time the zmx list was ISSUED}` for every container whose listing
|
|
789
|
+
ANSWERED this cycle (status.json `zmx_listed_at`). A container that timed out or failed is absent from it: its
|
|
790
|
+
groups are the previous ones republished, which say nothing about a lane created since. The co-located Studio
|
|
791
|
+
Host prunes a lane it started only on such a listing (design §8.10 C1, review round 6 — it read absence from any
|
|
792
|
+
fresh status.json and lost stop authority over lanes a timed-out listing had never seen)."""
|
|
787
793
|
lanes, groups = {}, {}
|
|
788
794
|
for container in containers:
|
|
795
|
+
issued = time.time()
|
|
789
796
|
try:
|
|
790
|
-
|
|
797
|
+
answered = observe_zmx_container(container)
|
|
798
|
+
found = answered or kept_zmx(previous or {}, 'timeout', container)
|
|
799
|
+
if answered is not None and listed is not None:
|
|
800
|
+
listed[container] = issued
|
|
791
801
|
except Exception as error:
|
|
792
802
|
log(f'zmx {container}', error)
|
|
793
803
|
found = kept_zmx(previous or {}, 'error', container)
|
|
@@ -860,7 +870,7 @@ def build_payload(ctx, level, reconcile=True):
|
|
|
860
870
|
except Exception as error:
|
|
861
871
|
log('observe', error)
|
|
862
872
|
lanes = {lane_id: keep(previous, lane_id, 'error') for lane_id in previous.get('lanes') or {}}
|
|
863
|
-
# DEF-
|
|
873
|
+
# DEF-1538: COPIES of what was published, never the previous payload itself (`previous` is the very object
|
|
864
874
|
# `ctx['published']` holds, so a kept list extended in place would grow the published payload), and only
|
|
865
875
|
# observe's own rows (holder 'tmux', the rows `observe` carries over too).
|
|
866
876
|
servers = dict(previous.get('servers') or {})
|
|
@@ -876,11 +886,15 @@ def build_payload(ctx, level, reconcile=True):
|
|
|
876
886
|
reconcile_client_sizes(registry, containers, live)
|
|
877
887
|
except Exception as error:
|
|
878
888
|
log('reconcile', error)
|
|
889
|
+
listed = {}
|
|
879
890
|
try:
|
|
880
|
-
zmx_lanes, zmx_groups = observe_zmx(containers, previous)
|
|
891
|
+
zmx_lanes, zmx_groups = observe_zmx(containers, previous, listed)
|
|
881
892
|
except Exception as error:
|
|
882
893
|
log('zmx', error)
|
|
883
894
|
zmx_lanes, zmx_groups = kept_zmx(previous, 'error')
|
|
895
|
+
listed = {}
|
|
896
|
+
# Outside the payload: it changes every cycle, and status.json is rewritten only on change (plus the heartbeat).
|
|
897
|
+
ctx['zmx_listed_at'] = listed
|
|
884
898
|
lanes.update(zmx_lanes)
|
|
885
899
|
return {'lanes': lanes, 'servers': servers, 'unregistered': unregistered, 'zmx': zmx_groups,
|
|
886
900
|
'containers': {k: {'running': v.get('running', False)} for k, v in containers.items()}}
|
|
@@ -919,7 +933,8 @@ def cycle(ctx, reconcile=True):
|
|
|
919
933
|
if payload != ctx['published'] or started - ctx['status_at'] > HEARTBEAT:
|
|
920
934
|
try:
|
|
921
935
|
written = time.time()
|
|
922
|
-
L.atomic_write(L.STATE / 'status.json', json.dumps(dict(payload, health=health, updated_at=written
|
|
936
|
+
L.atomic_write(L.STATE / 'status.json', json.dumps(dict(payload, health=health, updated_at=written,
|
|
937
|
+
zmx_listed_at=ctx.get('zmx_listed_at') or {})) + '\n')
|
|
923
938
|
ctx['published'], ctx['status_at'] = payload, written
|
|
924
939
|
except Exception as error:
|
|
925
940
|
log('status.json', error)
|