@vibelord/lanes 1.3.0 → 1.5.1
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 +797 -34
- 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 +182 -14
- package/payload/lanes/docs/DESIGN.md +7 -0
- package/payload/lanes/docs/INSTALL.md +23 -3
- package/payload/lanes/docs/RUNBOOK.md +402 -5
- package/payload/lanes/docs/USERGUIDE.md +222 -23
- package/payload/lanes/libexec/agent_state.py +604 -12
- package/payload/lanes/libexec/attach.py +181 -0
- 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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bound the desktop client's terminal backlog; reconnect a slow display, never its session.
|
|
3
|
+
|
|
4
|
+
Zellij 0.45.1 removes a slow client when its 5,000-message queue fills, but
|
|
5
|
+
sends Exit only after draining that queue. Input can wait for an unblock that
|
|
6
|
+
the removed client will never receive. SSH keepalives cannot detect this.
|
|
7
|
+
This relay bounds bytes and age before the terminal can block Zellij's writer.
|
|
8
|
+
"""
|
|
9
|
+
import collections
|
|
10
|
+
import errno
|
|
11
|
+
import fcntl
|
|
12
|
+
import os
|
|
13
|
+
import pty
|
|
14
|
+
import select
|
|
15
|
+
import signal
|
|
16
|
+
import sys
|
|
17
|
+
import termios
|
|
18
|
+
import time
|
|
19
|
+
import tty
|
|
20
|
+
|
|
21
|
+
OUTPUT_LIMIT = 1024 * 1024
|
|
22
|
+
OUTPUT_AGE = 5.0
|
|
23
|
+
CHUNK = 65536
|
|
24
|
+
STOP_GRACE = 1.0
|
|
25
|
+
# CAN cancels a partial escape sequence. Leave the alternate screen and restore
|
|
26
|
+
# terminal modes even when the child cannot run its own teardown.
|
|
27
|
+
RESET = (b'\x18\x1b[?2026l\x1b[?1049l\x1b[?1000l\x1b[?1002l\x1b[?1003l'
|
|
28
|
+
b'\x1b[?1006l\x1b[?2004l\x1b[<u\x1b[0m\x1b[?25h')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def relay(command, output_limit=OUTPUT_LIMIT, output_age=OUTPUT_AGE):
|
|
32
|
+
if not command:
|
|
33
|
+
raise ValueError('No display command supplied')
|
|
34
|
+
if not os.isatty(0) or not os.isatty(1):
|
|
35
|
+
raise ValueError('The Lanes display needs a terminal')
|
|
36
|
+
saved = termios.tcgetattr(0)
|
|
37
|
+
size = fcntl.ioctl(0, termios.TIOCGWINSZ, b'\0' * 8)
|
|
38
|
+
flags = fcntl.fcntl(1, fcntl.F_GETFL)
|
|
39
|
+
pending = collections.deque() # (time read, remaining bytes); includes partial writes
|
|
40
|
+
pending_size = 0
|
|
41
|
+
input_pending = b''
|
|
42
|
+
child = master = None
|
|
43
|
+
status = None
|
|
44
|
+
stopped = False
|
|
45
|
+
reason = None
|
|
46
|
+
|
|
47
|
+
def stop(_sig, _frame):
|
|
48
|
+
nonlocal stopped
|
|
49
|
+
stopped = True
|
|
50
|
+
|
|
51
|
+
def resize(_sig=None, _frame=None):
|
|
52
|
+
# forkpty returns -1 in the child. The parent's first resize can send
|
|
53
|
+
# SIGWINCH before that child has restored its inherited handlers.
|
|
54
|
+
if master is not None and master >= 0:
|
|
55
|
+
try:
|
|
56
|
+
fcntl.ioctl(master, termios.TIOCSWINSZ,
|
|
57
|
+
fcntl.ioctl(0, termios.TIOCGWINSZ, b'\0' * 8))
|
|
58
|
+
except OSError:
|
|
59
|
+
pass # the terminal may have closed while SIGWINCH was pending
|
|
60
|
+
|
|
61
|
+
signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT, signal.SIGWINCH)
|
|
62
|
+
handlers = {sig: signal.getsignal(sig) for sig in signals}
|
|
63
|
+
try:
|
|
64
|
+
# Install before fork: a close during startup must still reap our child.
|
|
65
|
+
for sig in signals:
|
|
66
|
+
signal.signal(sig, resize if sig == signal.SIGWINCH else stop)
|
|
67
|
+
tty.setraw(0)
|
|
68
|
+
child, master = pty.fork()
|
|
69
|
+
if child == 0:
|
|
70
|
+
for sig in signals:
|
|
71
|
+
signal.signal(sig, signal.SIG_DFL)
|
|
72
|
+
try:
|
|
73
|
+
# Give the first render the real dimensions, before exec can
|
|
74
|
+
# race the parent's initial SIGWINCH forwarding.
|
|
75
|
+
fcntl.ioctl(0, termios.TIOCSWINSZ, size)
|
|
76
|
+
os.execvpe(command[0], command, os.environ)
|
|
77
|
+
except OSError as error:
|
|
78
|
+
os.write(2, f'lanes: cannot start display: {error}\r\n'.encode())
|
|
79
|
+
os._exit(127)
|
|
80
|
+
os.set_blocking(master, False)
|
|
81
|
+
fcntl.fcntl(1, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
|
82
|
+
resize()
|
|
83
|
+
eof = False
|
|
84
|
+
while not stopped:
|
|
85
|
+
if status is None:
|
|
86
|
+
pid, value = os.waitpid(child, os.WNOHANG)
|
|
87
|
+
if pid:
|
|
88
|
+
status = value
|
|
89
|
+
if pending and time.monotonic() - pending[0][0] >= output_age:
|
|
90
|
+
reason = 'display output stalled'
|
|
91
|
+
break
|
|
92
|
+
if eof and not pending:
|
|
93
|
+
break
|
|
94
|
+
readers = ([] if eof else [master]) + ([] if input_pending or status is not None else [0])
|
|
95
|
+
writers = ([1] if pending else []) + ([master] if input_pending and not eof else [])
|
|
96
|
+
readable, writable, _ = select.select(readers, writers, [], 0.05)
|
|
97
|
+
# Drain first: a writable terminal should not trip the byte limit.
|
|
98
|
+
if 1 in writable:
|
|
99
|
+
at, data = pending[0]
|
|
100
|
+
try:
|
|
101
|
+
written = os.write(1, data)
|
|
102
|
+
except BlockingIOError:
|
|
103
|
+
written = 0
|
|
104
|
+
pending_size -= written
|
|
105
|
+
if written == len(data):
|
|
106
|
+
pending.popleft()
|
|
107
|
+
elif written:
|
|
108
|
+
pending[0] = (at, data[written:])
|
|
109
|
+
if master in writable:
|
|
110
|
+
try:
|
|
111
|
+
written = os.write(master, input_pending)
|
|
112
|
+
input_pending = input_pending[written:]
|
|
113
|
+
except BlockingIOError:
|
|
114
|
+
pass
|
|
115
|
+
if 0 in readable:
|
|
116
|
+
input_pending = os.read(0, CHUNK)
|
|
117
|
+
if not input_pending:
|
|
118
|
+
break
|
|
119
|
+
if master in readable:
|
|
120
|
+
try:
|
|
121
|
+
data = os.read(master, CHUNK)
|
|
122
|
+
except BlockingIOError:
|
|
123
|
+
continue
|
|
124
|
+
except OSError as error:
|
|
125
|
+
if error.errno != errno.EIO:
|
|
126
|
+
raise
|
|
127
|
+
data = b'' # Linux PTYs report EIO when the slave closes
|
|
128
|
+
if not data:
|
|
129
|
+
eof = True
|
|
130
|
+
elif pending_size + len(data) > output_limit:
|
|
131
|
+
reason = 'display output exceeded its backlog limit'
|
|
132
|
+
break
|
|
133
|
+
else:
|
|
134
|
+
pending.append((time.monotonic(), data))
|
|
135
|
+
pending_size += len(data)
|
|
136
|
+
finally:
|
|
137
|
+
if child and status is None:
|
|
138
|
+
# Signal only our unreaped child, never a session server or holder.
|
|
139
|
+
try:
|
|
140
|
+
os.kill(child, signal.SIGTERM)
|
|
141
|
+
except ProcessLookupError:
|
|
142
|
+
pass
|
|
143
|
+
deadline = time.monotonic() + STOP_GRACE
|
|
144
|
+
while time.monotonic() < deadline:
|
|
145
|
+
pid, value = os.waitpid(child, os.WNOHANG)
|
|
146
|
+
if pid:
|
|
147
|
+
status = value
|
|
148
|
+
break
|
|
149
|
+
time.sleep(0.02)
|
|
150
|
+
if status is None:
|
|
151
|
+
try:
|
|
152
|
+
os.kill(child, signal.SIGKILL)
|
|
153
|
+
except ProcessLookupError:
|
|
154
|
+
pass
|
|
155
|
+
_, status = os.waitpid(child, 0)
|
|
156
|
+
if master is not None:
|
|
157
|
+
os.close(master)
|
|
158
|
+
# Never block cleanup on the display that caused the timeout. The next
|
|
159
|
+
# attach repaints from current session state; queued stale output dies here.
|
|
160
|
+
ending = RESET
|
|
161
|
+
if reason:
|
|
162
|
+
ending += f'\r\nlanes: {reason}; reconnecting the display.\r\n'.encode()
|
|
163
|
+
try:
|
|
164
|
+
os.write(1, ending)
|
|
165
|
+
except OSError:
|
|
166
|
+
pass
|
|
167
|
+
try:
|
|
168
|
+
termios.tcsetattr(0, termios.TCSANOW, saved)
|
|
169
|
+
finally:
|
|
170
|
+
fcntl.fcntl(1, fcntl.F_SETFL, flags)
|
|
171
|
+
for sig, handler in handlers.items():
|
|
172
|
+
signal.signal(sig, handler)
|
|
173
|
+
# 1 is a reconnectable failure in lanes-launch; 64–78 impose a 60 s hold.
|
|
174
|
+
return 1 if reason or stopped else (os.waitstatus_to_exitcode(status) if status is not None else 0)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == '__main__':
|
|
178
|
+
try:
|
|
179
|
+
sys.exit(relay(sys.argv[1:]))
|
|
180
|
+
except (OSError, ValueError) as error:
|
|
181
|
+
sys.exit(f'lanes: display attach failed: {error}')
|
|
@@ -26,7 +26,9 @@ Trace keys: `cc-session/<id>` and `codex-session/<id>` (first two segments of th
|
|
|
26
26
|
|
|
27
27
|
The poller (plan Task 11, opt-in `lanes-economy.service`; enabling it is an operator gate because it reads the
|
|
28
28
|
production daemon's loopback control port): every `--interval` seconds it GETs `/status` and `/sessions` (3 s
|
|
29
|
-
timeout,
|
|
29
|
+
timeout, bearer from `LANES_THROTTLE_TOKEN_FILE`: both routes are guarded control reads, and the daemon's ControlAuth
|
|
30
|
+
answers 401 to a request without the bearer whatever the bind — there is no loopback exemption on the control port,
|
|
31
|
+
DEF-1930), folds today's log through `day_totals`, and writes
|
|
30
32
|
`STATE/economy.json` (schema `v: 1`, additive) only when the payload changed apart from `at` — or when the file's
|
|
31
33
|
`at` is HEARTBEAT seconds old, so `at` is a liveness heartbeat the sidebar ages with its own clock (DEF-1528); the
|
|
32
34
|
cursor lives in `STATE/economy.cursor.json`. `LANES_THROTTLE_CONTROL=none` disables every daemon request (the log fold still runs);
|
|
@@ -442,7 +444,9 @@ def control_url(env):
|
|
|
442
444
|
|
|
443
445
|
|
|
444
446
|
def read_token(env):
|
|
445
|
-
"""The
|
|
447
|
+
"""The bearer, or None when no file is named. `/status` and `/sessions` are guarded control routes: the daemon's
|
|
448
|
+
ControlAuth (its `remote_key` or the claude pool's managed local client token) answers 401 without one, loopback
|
|
449
|
+
or not (DEF-1930), so a poll without it degrades to `HTTP 401`. Never logged, never written anywhere."""
|
|
446
450
|
path = env.get('LANES_THROTTLE_TOKEN_FILE')
|
|
447
451
|
if not path:
|
|
448
452
|
return None
|