@vibelord/lanes 1.5.0 → 1.6.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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
- """lane-signal <status> [--event NAME] [--harness claude|codex|kimi|grok|grok-cli|unknown] [--note TEXT]
3
- [--agent-pid PID] [PAYLOAD] — the hook target of Claude Code, Codex, Kimi Code, Grok Build and grok-cli inside a
4
- workspace container, and the writer behind lane-mcp's `lanes_signal`. Writes ~/.lz/state/hooks/<lane id>.json
2
+ """lane-signal <status> [--event NAME] [--harness claude|codex|kimi|grok|grok-cli|muse|zcode|unknown] [--note TEXT]
3
+ [--agent-pid PID] [PAYLOAD] — the hook target of Claude Code, Codex, Kimi Code, Grok Build, grok-cli, Muse Code and
4
+ ZCode inside a workspace container, and the writer behind lane-mcp's `lanes_signal`. Writes ~/.lz/state/hooks/<lane id>.json
5
5
  (atomic replace), which the host's lanes-statusd reads through /proc/<container init pid>/root and publishes as the
6
6
  lane's `agent{}` (source `hook`).
7
7
 
@@ -10,7 +10,7 @@ The sidecar has its own directory because `lane-run`, the root process of every
10
10
  sidecar at state/<lane id>.json would overwrite that record for the agent's whole life (DEF-1572). Nothing but
11
11
  this script writes state/hooks/, and lane-run never writes there.
12
12
 
13
- {"v":1, "lane":"<id>", "harness":"claude|codex|kimi|grok|grok-cli|unknown", "status":"<agent{} vocabulary>",
13
+ {"v":1, "lane":"<id>", "harness":"claude|codex|kimi|grok|grok-cli|muse|zcode|unknown", "status":"<agent{} vocabulary>",
14
14
  "since":<epoch the status began>, "at":<epoch of this event>, "event":"<hook event>", "pid":<agent pid>,
15
15
  "start":"<its starttime>", "session":"<session/thread id or ''>", "name":"<the agent's own note>" (only when
16
16
  there is one)}
@@ -24,7 +24,11 @@ Lane id: $LANE_ID — exported in a tmux lane only by a canonical/`--identity` r
24
24
  `<ws>--z-<name>` id lanes-statusd publishes for it, so statusd does not join that sidecar to the zmx lane yet:
25
25
  DEF-1573) — else the pane's tmux session (`tmux -S ${TMUX%%,*} display -p -t $TMUX_PANE '#S'`) looked up by (socket
26
26
  name, session) in ~/.lz/registry.json, the projection `lanes sync` writes. lane-mcp passes the lane it resolved as
27
- $LANE_ID.
27
+ $LANE_ID. W13 (DEF-2031): when this process has neither LANE_ID nor TMUX — Muse Code runs a hook with an environment
28
+ cleared to an allow-list (HOME, PATH, TERM, LANG, LC_ALL, USER, LOGNAME, SHELL, TMPDIR, MUSE_SESSION_ID,
29
+ MUSE_THREAD_ID; MEASURED 2026-10-16, muse 1.4.10) — the agent's own /proc/<pid>/environ stands in for LANE_ID, LANE_WS,
30
+ TMUX and TMUX_PANE, read only when that pid is a known harness, bounded (ENVIRON_MAX) and without following a link
31
+ (`agent_environ`, lane_core.parent_environ's rule, pinned equal by test_lane_signal).
28
32
 
29
33
  Agent pid: --agent-pid when that process exists (lane-mcp passes its own parent, the agent that started it: the
30
34
  default rule below would name lane-mcp itself); else the parent process, or that parent's parent when the parent is
@@ -32,12 +36,15 @@ a shell (Claude Code runs a hook command through a shell, which may fork it rath
32
36
  with the shell would read as a dead agent).
33
37
  Session id: `session_id` of the hook's JSON on stdin (Grok Build's camel-case `sessionId` too), `thread-id` of a JSON
34
38
  PAYLOAD argument (Codex `notify` passes its JSON as the last argument), else $CLAUDE_CODE_SESSION_ID / $CODEX_THREAD_ID
35
- / $GROK_SESSION_ID (lane-mcp passes the first and the last: Claude Code and Grok Build give them to their MCP servers;
36
- Grok Build also sets GROK_SESSION_ID for every hook it runs).
39
+ / $GROK_SESSION_ID / $MUSE_SESSION_ID / $ZCODE_SESSION_ID (lane-mcp passes them: Claude Code, Grok Build and Muse Code
40
+ give them to their MCP servers; Grok Build, Muse Code and ZCode also set theirs for every hook they run).
41
+ A PostToolUseFailure whose hook JSON says `is_interrupt: true` is the person interrupting the turn: needs_input,
42
+ whatever status the hook names (ZCode sends no Stop after an interrupt, DEF-2091).
37
43
  Harness (W12, DEF-1990): --harness when it is one of HARNESSES, else $LANE_HARNESS (the same rule), else the first
38
44
  process up the parent chain (PARENT_DEPTH steps from this process's parent) whose comm is a harness's
39
- (HARNESS_COMMS: claude, codex, kimi, kimi-code, grok; a `grok` is Grok Build when /proc/<pid>/exe ends in
40
- GROK_BUILD_EXE, else grok-cli; any comm whose exe holds CLAUDE_EXE is Claude Code), else `unknown` — never `codex`:
45
+ (HARNESS_COMMS: claude, codex, kimi, kimi-code, grok, muse, zcode-cli; a `grok` is Grok Build when /proc/<pid>/exe ends
46
+ in GROK_BUILD_EXE, else grok-cli; a comm starting `muse-bin-` or an exe whose file name does is Muse Code; any comm
47
+ whose exe holds CLAUDE_EXE is Claude Code), else `unknown` — never `codex`:
41
48
  before W12 every hook without --harness and outside Claude Code was recorded as Codex. $LANE_PROC names another /proc for the chain and the agent's starttime (tests).
42
49
  Stdin is read only when it is not a terminal and for at most STDIN_WAIT seconds: a stdin that never closes cannot hang
43
50
  the hook.
@@ -62,6 +69,7 @@ import json
62
69
  import os
63
70
  import re
64
71
  import select
72
+ import stat
65
73
  import subprocess
66
74
  import sys
67
75
  import time
@@ -76,12 +84,16 @@ NOTE_MAX = 120
76
84
  EXPLICIT = 'lanes_signal' # the --event of lane-mcp's lanes_signal: the agent's own word
77
85
  REPORT_TOOL = 'lanes_signal' # its tool name; Claude Code names an MCP tool mcp__<server>__<tool>
78
86
  RESTING = {('Stop', 'needs_input'), ('Stop', 'done'), ('Notification', 'needs_input')} # the hooks' "turn ended"
79
- SESSION_VARS = ('CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID', 'GROK_SESSION_ID')
80
- HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'unknown') # agent{}.harness, but `shell`
81
- HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok'}
87
+ SESSION_VARS = ('CLAUDE_CODE_SESSION_ID', 'CODEX_THREAD_ID', 'GROK_SESSION_ID', 'MUSE_SESSION_ID', 'ZCODE_SESSION_ID')
88
+ HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'muse', 'zcode', 'unknown') # agent{}.harness, but `shell`
89
+ HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok',
90
+ 'muse': 'muse', 'zcode-cli': 'zcode'}
91
+ MUSE_PREFIX = 'muse-bin-' # Muse Code's binary `muse-bin-<version>` and its comm (MEASURED 2026-10-16: muse-bin-1.4.10)
82
92
  GROK_BUILD_EXE = '/downloads/grok-linux-x86_64' # Grok Build's file ($GROK_HOME/downloads/…); grok-cli's: bin/grok
83
93
  CLAUDE_EXE = '/claude/versions/' # native Claude Code before 2.1.299 kept its file's name, the version, as comm (MEASURED)
84
94
  PARENT_DEPTH = 8
95
+ ENVIRON_MAX = 1 << 20 # lane_core.ENVIRON_MAX: /proc/<pid>/environ read at most this far
96
+ IDENTITY_VARS = ('LANE_ID', 'LANE_WS', 'TMUX', 'TMUX_PANE') # lane_core.WHOAMI_VARS
85
97
 
86
98
 
87
99
  def options(argv):
@@ -109,12 +121,13 @@ def clean_note(text):
109
121
  return ' '.join(kept.split())[:NOTE_MAX]
110
122
 
111
123
 
112
- def lane_id(root):
113
- found = os.environ.get('LANE_ID', '')
114
- if not found and os.environ.get('TMUX') and os.environ.get('TMUX_PANE'):
115
- sock = os.environ['TMUX'].split(',', 1)[0]
124
+ def lane_id(root, environ=None):
125
+ environ = os.environ if environ is None else environ
126
+ found = environ.get('LANE_ID', '')
127
+ if not found and environ.get('TMUX') and environ.get('TMUX_PANE'):
128
+ sock = environ['TMUX'].split(',', 1)[0]
116
129
  try:
117
- done = subprocess.run(['tmux', '-S', sock, 'display', '-p', '-t', os.environ['TMUX_PANE'], '#S'],
130
+ done = subprocess.run(['tmux', '-S', sock, 'display', '-p', '-t', environ['TMUX_PANE'], '#S'],
118
131
  stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=1)
119
132
  session = done.stdout.strip() if done.returncode == 0 else ''
120
133
  lanes = json.load(open(os.path.join(root, 'registry.json'))).get('lanes') or {}
@@ -178,7 +191,54 @@ def process_harness(comm, pid, proc='/proc'):
178
191
  return 'grok' if exe_of(pid, proc).endswith(GROK_BUILD_EXE) else 'grok-cli'
179
192
  if found:
180
193
  return found
181
- return 'claude' if CLAUDE_EXE in exe_of(pid, proc) else None
194
+ if comm.startswith(MUSE_PREFIX):
195
+ return 'muse'
196
+ exe = exe_of(pid, proc)
197
+ if CLAUDE_EXE in exe:
198
+ return 'claude'
199
+ return 'muse' if exe.rpartition('/')[2].startswith(MUSE_PREFIX) else None
200
+
201
+
202
+ def agent_environ(pid, proc='/proc', names=IDENTITY_VARS):
203
+ """{name: value} of `names` in /proc/<pid>/environ when `pid` is a known harness (process_harness) — {} otherwise
204
+ (lane_core.parent_environ's rule). Bounded (ENVIRON_MAX), never through a link (O_NOFOLLOW on the file; /proc's own
205
+ entries are not links), never blocking; entries are NUL-split and one that is not UTF-8 or holds no `=` is skipped."""
206
+ try:
207
+ pid = int(pid)
208
+ if pid <= 1:
209
+ return {}
210
+ comm = stat_fields(pid, proc)[0]
211
+ except (OSError, ValueError, IndexError, TypeError):
212
+ return {}
213
+ if not process_harness(comm, pid, proc):
214
+ return {}
215
+ try:
216
+ fd = os.open(f'{proc}/{pid}/environ', os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | getattr(os, 'O_CLOEXEC', 0))
217
+ except OSError:
218
+ return {}
219
+ try:
220
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
221
+ return {}
222
+ data, chunk = b'', b'x'
223
+ while chunk and len(data) <= ENVIRON_MAX:
224
+ chunk = os.read(fd, ENVIRON_MAX + 1 - len(data))
225
+ data += chunk
226
+ except OSError:
227
+ return {}
228
+ finally:
229
+ os.close(fd)
230
+ if len(data) > ENVIRON_MAX:
231
+ return {}
232
+ found = {}
233
+ for item in data.split(b'\0'):
234
+ key, sep, value = item.partition(b'=')
235
+ try:
236
+ key, value = key.decode('utf-8'), value.decode('utf-8')
237
+ except UnicodeDecodeError:
238
+ continue
239
+ if sep and key in names and value and key not in found:
240
+ found[key] = value
241
+ return found
182
242
 
183
243
 
184
244
  def parent_harness(proc='/proc', start=None):
@@ -253,10 +313,19 @@ def main(argv):
253
313
  if status not in STATUSES:
254
314
  return
255
315
  root = os.environ.get('LANE_ROOT') or os.path.join(os.path.expanduser('~'), '.lz')
256
- lane = lane_id(root)
316
+ proc = os.environ.get('LANE_PROC') or '/proc'
317
+ pid, start = agent(agent_pid, proc)
318
+ environ = None
319
+ if not os.environ.get('LANE_ID') and not os.environ.get('TMUX'):
320
+ inherited = agent_environ(pid, proc) # a harness that cleared its hooks' environment (Muse Code, DEF-2031)
321
+ if inherited.get('LANE_ID') or (inherited.get('TMUX') and inherited.get('TMUX_PANE')):
322
+ environ = dict(os.environ, **inherited)
323
+ lane = lane_id(root, environ)
257
324
  if not lane:
258
325
  return
259
326
  hook = stdin_doc()
327
+ if event == 'PostToolUseFailure' and hook.get('is_interrupt') is True:
328
+ status = 'needs_input' # the person interrupted the turn; ZCode sends no Stop after it (DEF-2091)
260
329
  session = str(hook.get('session_id') or hook.get('sessionId') or '')
261
330
  if not session and payload:
262
331
  try:
@@ -265,10 +334,8 @@ def main(argv):
265
334
  except ValueError:
266
335
  session = ''
267
336
  session = (session or next((os.environ[key] for key in SESSION_VARS if os.environ.get(key)), ''))[:128]
268
- proc = os.environ.get('LANE_PROC') or '/proc'
269
337
  given_harness = harness if harness in HARNESSES else ''
270
338
  harness = harness_of(harness, proc)
271
- pid, start = agent(agent_pid, proc)
272
339
  now = int(time.time())
273
340
  state = os.path.join(root, 'state', 'hooks') # never state/<id>.json: lane-run's phase record (DEF-1572)
274
341
  path = os.path.join(state, f'{lane}.json')
@@ -59,7 +59,10 @@ STATUS_FRESH = 60 # status.json `at` is renewed at least every 15 s (sta
59
59
  READ_MAX = 16 << 20 # a projection bigger than this is not ours
60
60
  TMUX_TIMEOUT = 5
61
61
  IDENTITY = ('LANE_ID', 'LANE_WS', 'THROTTLE_TRACE', 'LANES_TRACE') # + our trace headers and GROK_HOME (identity_free)
62
- TRACE_HEADERS = ('ANTHROPIC_CUSTOM_HEADERS', 'KIMI_CODE_CUSTOM_HEADERS') # ours: 'x-throttle-trace: lanes/<ws>/<id>'
62
+ # Ours: 'x-throttle-trace: lanes/<ws>/<id>'. W13: MUSE_CUSTOM_HEADERS reaches Muse Code's model requests (MEASURED
63
+ # 2026-10-16 on muse 1.4.10: the header arrived on its `GET …/models`; undocumented by Meta, so it may stop working in a
64
+ # later release — DEF-2059). ZCode has no per-process header (its api.headers are static config): no trace.
65
+ TRACE_HEADERS = ('ANTHROPIC_CUSTOM_HEADERS', 'KIMI_CODE_CUSTOM_HEADERS', 'MUSE_CUSTOM_HEADERS')
63
66
  GROK_HOME = CONTAINER_HOME + '/.grok-build' # Grok Build's home in a lane, never grok-cli's ~/.grok (DM-LANES-GROK-HOME)
64
67
 
65
68
 
@@ -213,7 +216,8 @@ def identity_free(environ, drop=('TMUX',)):
213
216
  a socket's server hands its whole environment to the server's GLOBAL environment, and every later session on
214
217
  that socket created without its own `-e` inherits it (MEASURED, tmux 3.4: DEF-1559). Board sockets are shared by
215
218
  many lanes, so a lane identity in the env of anything that may start one would be stamped on unrelated lanes.
216
- An operator's own ANTHROPIC_CUSTOM_HEADERS / KIMI_CODE_CUSTOM_HEADERS that is not a lane trace is theirs and stays,
219
+ An operator's own ANTHROPIC_CUSTOM_HEADERS / KIMI_CODE_CUSTOM_HEADERS / MUSE_CUSTOM_HEADERS that is not a lane trace
220
+ is theirs and stays,
217
221
  and so is a GROK_HOME that is not the one the recipe stamps (W12)."""
218
222
  return {k: v for k, v in environ.items() if k not in drop and k not in IDENTITY
219
223
  and not (k in TRACE_HEADERS and v.startswith('x-throttle-trace: lanes/'))
@@ -309,7 +313,7 @@ def last_lines(text, lines):
309
313
  def new_session_argv(socket, session, cwd, lane_id, ws, command):
310
314
  """The one tmux client lane-mcp runs WITHOUT -N (`lanes_new`): it creates the session, and starts the socket's
311
315
  server when none runs. The lane's identity reaches the session only through these `-e` flags, the canonical
312
- recipe's seven (the client's own environment is creation_env(), an allow-list: whatever client starts a server
316
+ recipe's eight (the client's own environment is creation_env(), an allow-list: whatever client starts a server
313
317
  hands its environment to every later session there, DEF-1559, DEF-1802). `command` is one shell-command string,
314
318
  run by the server's default shell like the recipe's command."""
315
319
  trace = lane_trace(lane_id, ws)
@@ -319,7 +323,7 @@ def new_session_argv(socket, session, cwd, lane_id, ws, command):
319
323
  '-e', f'LANE_ID={lane_id}', '-e', f'LANE_WS={ws}',
320
324
  '-e', f'ANTHROPIC_CUSTOM_HEADERS=x-throttle-trace: {trace}', '-e', f'THROTTLE_TRACE={trace}',
321
325
  '-e', f'KIMI_CODE_CUSTOM_HEADERS=x-throttle-trace: {trace}', '-e', f'LANES_TRACE={trace}',
322
- '-e', f'GROK_HOME={GROK_HOME}', command]
326
+ '-e', f'GROK_HOME={GROK_HOME}', '-e', f'MUSE_CUSTOM_HEADERS=x-throttle-trace: {trace}', command]
323
327
 
324
328
 
325
329
  CREATION_ENV = ('HOME', 'USER', 'LOGNAME', 'PATH', 'LANG', 'LANGUAGE', 'TZ', 'TMUX_TMPDIR') # and every LC_*
@@ -392,9 +396,9 @@ def lane_trace(lane_id, ws):
392
396
 
393
397
  def canonical_recipe(lane_id, ws, socket, session, folder, cmd='bash -l'):
394
398
  """lanes_lib.canonical_recipe: the create recipe that gives a new tmux lane its identity (LANE_ID, LANE_WS, the
395
- lanes/<ws>/<id> trace as ANTHROPIC_CUSTOM_HEADERS, THROTTLE_TRACE, KIMI_CODE_CUSTOM_HEADERS and LANES_TRACE, and
396
- GROK_HOME) through `-e` flags; `new -A` attaches when the session exists. LaneError where the host raises
397
- LanesError."""
399
+ lanes/<ws>/<id> trace as ANTHROPIC_CUSTOM_HEADERS, THROTTLE_TRACE, KIMI_CODE_CUSTOM_HEADERS, LANES_TRACE and (W13)
400
+ MUSE_CUSTOM_HEADERS, and GROK_HOME) through `-e` flags; `new -A` attaches when the session exists. LaneError where
401
+ the host raises LanesError."""
398
402
  trace = lane_trace(lane_id, ws)
399
403
  if trace is None:
400
404
  raise LaneError(f'Invalid lane trace: lanes/{ws}/{lane_id} (allowed: A-Z a-z 0-9 . _ - /, at most 256)',
@@ -411,7 +415,7 @@ def canonical_recipe(lane_id, ws, socket, session, folder, cmd='bash -l'):
411
415
  return (f'tmux -L {shlex.quote(socket)} new -ADs {shlex.quote(session)} -c {shlex.quote(folder)} '
412
416
  f'-e LANE_ID={lane_id} -e LANE_WS={ws} -e ANTHROPIC_CUSTOM_HEADERS={header} '
413
417
  f'-e THROTTLE_TRACE={trace} -e KIMI_CODE_CUSTOM_HEADERS={header} -e LANES_TRACE={trace} '
414
- f'-e GROK_HOME={GROK_HOME} {cmd}')
418
+ f'-e GROK_HOME={GROK_HOME} -e MUSE_CUSTOM_HEADERS={header} {cmd}')
415
419
 
416
420
 
417
421
  def lane_name_problem(name):
@@ -486,8 +490,10 @@ def lane_kind(entry):
486
490
 
487
491
  # The harnesses a process tree can name (W12; lane-signal's HARNESS_COMMS and parent chain, pinned equal by
488
492
  # test_lane_core). A `grok` is Grok Build when its executable is $GROK_HOME/downloads/grok-linux-x86_64, else grok-cli.
489
- HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok'}
490
- HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'unknown')
493
+ HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok',
494
+ 'muse': 'muse', 'zcode-cli': 'zcode'}
495
+ HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'muse', 'zcode', 'unknown')
496
+ MUSE_PREFIX = 'muse-bin-' # W13: Muse Code's comm and binary file name start with it (MEASURED: muse-bin-1.4.10)
491
497
  GROK_BUILD_EXE = '/downloads/grok-linux-x86_64'
492
498
  CLAUDE_EXE = '/claude/versions/' # native Claude Code before 2.1.299 kept its file's name, the version, as comm (MEASURED)
493
499
  PARENT_DEPTH = 8
@@ -515,15 +521,23 @@ def _exe(proc, pid):
515
521
 
516
522
 
517
523
  def process_harness(proc, pid):
518
- """The harness process `pid` is, or None: its comm in HARNESS_COMMS (a `grok` split by /proc/<pid>/exe), else a
519
- native Claude Code file (CLAUDE_EXE) as its executable."""
524
+ """The harness process `pid` is, or None: its comm in HARNESS_COMMS (a `grok` split by /proc/<pid>/exe), a comm
525
+ starting MUSE_PREFIX (Muse Code), else by its executable: a native Claude Code file (CLAUDE_EXE) or a Muse binary
526
+ (a file name starting MUSE_PREFIX)."""
520
527
  found = _stat(proc, pid)
521
528
  if not found:
522
529
  return None
523
530
  harness = HARNESS_COMMS.get(found[0])
524
531
  if harness == 'grok':
525
532
  return 'grok' if _exe(proc, pid).endswith(GROK_BUILD_EXE) else 'grok-cli'
526
- return harness or ('claude' if CLAUDE_EXE in _exe(proc, pid) else None)
533
+ if harness:
534
+ return harness
535
+ if found[0].startswith(MUSE_PREFIX):
536
+ return 'muse'
537
+ exe = _exe(proc, pid)
538
+ if CLAUDE_EXE in exe:
539
+ return 'claude'
540
+ return 'muse' if exe.rpartition('/')[2].startswith(MUSE_PREFIX) else None
527
541
 
528
542
 
529
543
  def parent_harness(proc, pid):
@@ -1013,13 +1027,16 @@ def terminals_add(text, terminal, entry=None):
1013
1027
  MCP_NAME = 'lanes'
1014
1028
  MCP_COMMAND = LZ + '/bin/lane-mcp'
1015
1029
  MCP_ENTRY = {'command': MCP_COMMAND}
1016
- MCP_AGENTS = ('claude', 'codex', 'kimi', 'grok', 'grok-cli')
1030
+ MCP_AGENTS = ('claude', 'codex', 'kimi', 'grok', 'grok-cli', 'muse', 'zcode')
1017
1031
  MCP_FILES = {'claude': '.claude.json', 'codex': '.codex/config.toml', 'kimi': '.kimi-code/mcp.json',
1018
- 'grok': '.grok-build/config.toml', 'grok-cli': '.grok/user-settings.json'}
1032
+ 'grok': '.grok-build/config.toml', 'grok-cli': '.grok/user-settings.json',
1033
+ 'muse': '.config/muse/settings.json', 'zcode': '.zcode/cli/config.json'}
1019
1034
  # W12: claude and codex get their entry in every home; kimi, grok (Grok Build, GROK_HOME) and grok-cli only where their
1020
- # home directory already is a real directory (the harness is installed there), unless named (`--agent`).
1035
+ # home directory already is a real directory (the harness is installed there), unless named (`--agent`). W13: muse
1036
+ # (Muse Code's ~/.config/muse, where its auth.json and settings.json live) and zcode (~/.zcode) the same way.
1021
1037
  MCP_ALWAYS = ('claude', 'codex')
1022
- MCP_HOMES = {'kimi': '.kimi-code', 'grok': '.grok-build', 'grok-cli': '.grok'}
1038
+ MCP_HOMES = {'kimi': '.kimi-code', 'grok': '.grok-build', 'grok-cli': '.grok', 'muse': '.config/muse',
1039
+ 'zcode': '.zcode'}
1023
1040
  TOML_HEADER = re.compile(r'''^[ \t]*\[[ \t]*mcp_servers[ \t]*\.[ \t]*(?:lanes|"lanes"|'lanes')[ \t]*\][ \t]*(?:#.*)?$''')
1024
1041
  # What Codex must hand lane-mcp. Codex starts a stdio MCP server with a CLEARED environment: its defaults (HOME,
1025
1042
  # LOGNAME, PATH, SHELL, USER, LANG, LC_ALL, TERM, TMPDIR, TZ) plus the names in the table's `env_vars` and the
@@ -1380,11 +1397,118 @@ def grok_cli_mcp(text, command=MCP_COMMAND):
1380
1397
  return after, None
1381
1398
 
1382
1399
 
1383
- MCP_EDITORS = {'claude': claude_mcp, 'codex': codex_mcp, 'kimi': kimi_mcp, 'grok': grok_mcp, 'grok-cli': grok_cli_mcp}
1400
+ # W13: Muse Code (muse 1.4.10) reads its user settings from ~/.config/muse/settings.json, a JSON object that must say
1401
+ # "schema_version": 1 (MEASURED 2026-10-16: a file without it is refused by Muse). An MCP server is `mcp_servers.<name>`
1402
+ # {transport, command, args, env, mode}; Muse starts a stdio server with an environment CLEARED to a few names (HOME,
1403
+ # PATH, TERM, LANG, MUSE_SESSION_ID) plus the entry's `env`, whose values interpolate `${VAR}` and `${VAR:-}` from
1404
+ # Muse's own environment (MEASURED 2026-10-16: `${LANE_ID:-}` arrived as the lane id, `${NOPE:-}` as empty). `mode`
1405
+ # defaults to "required", which aborts the session when the server cannot start: ours is "optional".
1406
+ MUSE_SCHEMA_VERSION = 1
1407
+ MUSE_MCP_ENV = {name: '${' + name + ':-}' for name in MCP_ENV_VARS}
1408
+
1409
+
1410
+ def muse_mcp_entry(command=MCP_COMMAND):
1411
+ return {'transport': 'stdio', 'command': command, 'args': [], 'env': dict(MUSE_MCP_ENV), 'mode': 'optional'}
1412
+
1413
+
1414
+ def muse_settings_doc(text, what='~/.config/muse/settings.json'):
1415
+ """(doc, why) of Muse Code's settings.json text: a new document when there is none; `why` when the file is not one
1416
+ Lanes may edit (not an object, or no "schema_version": 1 — a file Muse itself would refuse, or a newer schema)."""
1417
+ if text is None or not text.strip():
1418
+ return {'schema_version': MUSE_SCHEMA_VERSION}, None
1419
+ doc = strict_json(text, what)
1420
+ if not isinstance(doc, dict):
1421
+ return None, f'{what} is not a JSON object — left alone'
1422
+ if doc.get('schema_version') != MUSE_SCHEMA_VERSION or isinstance(doc.get('schema_version'), bool):
1423
+ return None, (f'{what} does not say "schema_version": {MUSE_SCHEMA_VERSION} — left alone (Muse Code refuses '
1424
+ 'such a file; fix it by hand first)')
1425
+ return doc, None
1426
+
1427
+
1428
+ def muse_mcp(text, command=MCP_COMMAND):
1429
+ """~/.config/muse/settings.json with `mcp_servers.lanes` = muse_mcp_entry() -> (new text, why). A new file is
1430
+ {"schema_version": 1, "mcp_servers": {"lanes": …}}; an existing one must already say schema_version 1. An equal
1431
+ entry is present; an earlier install's (a lane-mcp at another path or with other env) is replaced; a `lanes`
1432
+ server that starts something else is left alone. The edit is that one member, every other byte kept, and the
1433
+ result must parse back to the input plus that member."""
1434
+ what = '~/.config/muse/settings.json'
1435
+ entry = muse_mcp_entry(command)
1436
+ doc, why = muse_settings_doc(text, what)
1437
+ if why:
1438
+ return text, why
1439
+ if text is None or not text.strip():
1440
+ return json.dumps(dict(doc, mcp_servers={MCP_NAME: entry}), indent=2) + '\n', None
1441
+ servers = doc.get('mcp_servers')
1442
+ if servers is not None and not isinstance(servers, dict):
1443
+ return text, f'mcp_servers in {what} is not an object — left alone'
1444
+ current = (servers or {}).get(MCP_NAME)
1445
+ if current == entry:
1446
+ return text, None
1447
+ if current is not None and not _an_older_ours(current):
1448
+ shown = current.get('command') if isinstance(current, dict) else current
1449
+ return text, (f'mcp_servers.{MCP_NAME} in {what} is not the entry Lanes writes (command {shown!r}) — left '
1450
+ 'alone (remove it to let Lanes add its own)')
1451
+ if servers is None:
1452
+ after = set_member(text, [], 'mcp_servers', {MCP_NAME: entry})
1453
+ else:
1454
+ after = set_member(text, ['mcp_servers'], MCP_NAME, entry)
1455
+ if strict_json(after, what) != dict(doc, mcp_servers=dict(servers or {}, **{MCP_NAME: entry})):
1456
+ return text, 'the entry could not be added without changing anything else — add it by hand'
1457
+ return after, None
1458
+
1459
+
1460
+ # W13: ZCode CLI (zai-org/ZCode, apps/zcode-cli 0.16.9; READ in its README, not run here) reads MCP servers from
1461
+ # ~/.zcode/cli/config.json `mcp.servers.<name>`; a stdio server {type, command, args, cwd, env, enabled, timeoutMs}
1462
+ # inherits ZCode's own environment, so the lane's variables arrive without an `env`. `features.mcp` false turns MCP off.
1463
+ def _zcode_ours(entry, command):
1464
+ """A ZCode server entry that starts this lane-mcp (its `enabled` and `timeoutMs` are the person's)."""
1465
+ return isinstance(entry, dict) and entry.get('command') == command and not entry.get('args') \
1466
+ and entry.get('type', 'stdio') == 'stdio' and 'url' not in entry
1467
+
1468
+
1469
+ def zcode_mcp(text, command=MCP_COMMAND):
1470
+ """~/.zcode/cli/config.json with `mcp.servers.lanes` = {"type": "stdio", "command": <command>} -> (new text, why).
1471
+ A file whose `features.mcp` is false (the person turned MCP off) is left alone, as is a `lanes` server that starts
1472
+ something else; an earlier install's lane-mcp at another path is replaced. The edit is one member, every other byte
1473
+ kept (the file may hold provider headers), and the result must parse back to the input plus that member."""
1474
+ what = '~/.zcode/cli/config.json'
1475
+ entry = {'type': 'stdio', 'command': command}
1476
+ if text is None or not text.strip():
1477
+ return json.dumps({'mcp': {'servers': {MCP_NAME: entry}}}, indent=2) + '\n', None
1478
+ doc = strict_json(text, what)
1479
+ if not isinstance(doc, dict):
1480
+ return text, f'{what} is not a JSON object — left alone'
1481
+ features = doc.get('features')
1482
+ if isinstance(features, dict) and features.get('mcp') is False:
1483
+ return text, f'features.mcp is false in {what} — MCP is off there; left alone'
1484
+ mcp = doc.get('mcp')
1485
+ servers = mcp.get('servers') if isinstance(mcp, dict) else None
1486
+ if (mcp is not None and not isinstance(mcp, dict)) or (servers is not None and not isinstance(servers, dict)):
1487
+ return text, f'mcp.servers in {what} is not an object — left alone'
1488
+ current = (servers or {}).get(MCP_NAME)
1489
+ if _zcode_ours(current, command):
1490
+ return text, None
1491
+ if current is not None and not _an_older_ours(current):
1492
+ shown = current.get('command') if isinstance(current, dict) else current
1493
+ return text, (f'mcp.servers.{MCP_NAME} in {what} is not the entry Lanes writes (command {shown!r}) — left '
1494
+ 'alone (remove it to let Lanes add its own)')
1495
+ if mcp is None:
1496
+ after = set_member(text, [], 'mcp', {'servers': {MCP_NAME: entry}})
1497
+ elif servers is None:
1498
+ after = set_member(text, ['mcp'], 'servers', {MCP_NAME: entry})
1499
+ else:
1500
+ after = set_member(text, ['mcp', 'servers'], MCP_NAME, entry)
1501
+ if strict_json(after, what) != dict(doc, mcp=dict(mcp or {}, servers=dict(servers or {}, **{MCP_NAME: entry}))):
1502
+ return text, 'the entry could not be added without changing anything else — add it by hand'
1503
+ return after, None
1504
+
1505
+
1506
+ MCP_EDITORS = {'claude': claude_mcp, 'codex': codex_mcp, 'kimi': kimi_mcp, 'grok': grok_mcp, 'grok-cli': grok_cli_mcp,
1507
+ 'muse': muse_mcp, 'zcode': zcode_mcp}
1384
1508
 
1385
1509
 
1386
1510
  def mcp_agents(home):
1387
- """The agents whose entry an install writes by default: MCP_ALWAYS, plus each of kimi, grok and grok-cli whose
1511
+ """The agents whose entry an install writes by default: MCP_ALWAYS, plus each of kimi, grok, grok-cli, muse and zcode whose
1388
1512
  home directory (MCP_HOMES) is a real directory under `home` — never a link (the host writes through the volume)."""
1389
1513
  found = list(MCP_ALWAYS)
1390
1514
  for agent in MCP_AGENTS:
@@ -18,10 +18,10 @@ and writes ~/.lz/state/throttle.json (the shape below, by rename) only when a va
18
18
  since the file's `at` — `at` is the liveness heartbeat lanes-statusd ages:
19
19
 
20
20
  {"v": 1, "at": <epoch s>, "up": bool, "tokened": bool,
21
- "pools": {"claude"|"codex"|"kimi": {"configured", "usable", "cooling", "exhausted", "benched", "auth_failed",
21
+ "pools": {"claude"|"codex"|"kimi"|"muse-code"|"zai": {"configured", "usable", "cooling", "exhausted", "benched", "auth_failed",
22
22
  "overflow_ready", "pressure_phase", "spill_fraction", "overflow_in_flight", "upstream_429_total",
23
23
  "worst_7d", "worst_5h", "soonest_reset"}},
24
- "overflow": {"claude"|"codex"|"kimi": {"spend_today_usd", "budget_usd", "fraction", "alert_level_pct"}},
24
+ "overflow": {"claude"|"codex"|"kimi"|"muse-code"|"zai": {"spend_today_usd", "budget_usd", "fraction", "alert_level_pct"}},
25
25
  "error": null | "<why the daemon did not answer>"}
26
26
 
27
27
  `cooling` is the remainder configured − usable − exhausted − auth_failed − disabled − billing/org-blocked − unhealthy,
@@ -36,7 +36,10 @@ POOLS are claude, codex and (W12, Lanes 1.5.0) kimi: the daemon's `/pools/:pool/
36
36
  generic (`src/proxy.rs` `scoped_pool_state`), and a daemon with no Kimi account answers `/pools/kimi/ready` with
37
37
  `configured_subscriptions: 0` — a pool with nothing configured, not a failure. Kimi has no paid overflow lane (the
38
38
  daemon keeps its `api_overflow` at the default, and its /status still renders a zero overflow block), so only
39
- MONEY_POOLS carry overflow money: `overflow.kimi` is always nulls.
39
+ MONEY_POOLS carry overflow money: `overflow.kimi` is always nulls. W13 (Lanes 1.6.0) adds Throttle's native `muse-code`
40
+ (Meta's Muse Code, listener 8097) and `zai` (Z.ai, listener 8098) subscription pools on the same generic routes; like
41
+ Kimi their overflow is refused by the daemon's config, so their overflow is always nulls too, and their accounts'
42
+ providers are `muse-code` and `zai` (Throttle's Provider names).
40
43
 
41
44
  GET only, 3 s timeout, 1 MiB body cap, no redirect followed (one would carry the bearer elsewhere), no proxy from the
42
45
  environment. Stdlib only. Exit: --once 0 when the daemon answered, 1 when not, 2 on a usage error; --loop runs until
@@ -62,12 +65,12 @@ TIMEOUT = 3 # seconds per GET
62
65
  MAX_BODY = 1 << 20 # bytes: a bigger answer is not the daemon's
63
66
  TOKEN_MAX = 4096 # bytes: a token file is one short line
64
67
  DEFAULT_PORT = 19181
65
- POOLS = ('claude', 'codex', 'kimi')
66
- MONEY_POOLS = frozenset({'claude', 'codex'}) # the pools with a paid overflow lane; kimi's money is always null
68
+ POOLS = ('claude', 'codex', 'kimi', 'muse-code', 'zai')
69
+ MONEY_POOLS = frozenset({'claude', 'codex'}) # the pools with a paid overflow lane; kimi/muse-code/zai money: null
67
70
  # The gauge's numbers (src/proxy.rs render_metrics: 0=calm,1=spilling,2=elevated) as the words the daemon's own JSON
68
71
  # uses for them (src/pressure.rs PressureSnapshot.phase: "closed" is calm), which economy.json and the sidebar read.
69
72
  PHASES = {0: 'closed', 1: 'spilling', 2: 'elevated'}
70
- SUBSCRIPTION_PROVIDERS = frozenset({'anthropic', 'openai', 'kimi'}) # src/proxy.rs is_subscription_provider
73
+ SUBSCRIPTION_PROVIDERS = frozenset({'anthropic', 'openai', 'kimi', 'muse-code', 'zai'}) # src/proxy.rs is_subscription_provider
71
74
  SERIES = {'throttle_pool_spill_fraction': 'spill_fraction', 'throttle_overflow_in_flight': 'overflow_in_flight',
72
75
  'throttle_upstream_429_total': 'upstream_429_total', 'throttle_pool_pressure_phase': 'pressure_phase'}
73
76
  POOL_KEYS = ('configured', 'usable', 'cooling', 'exhausted', 'benched', 'auth_failed', 'overflow_ready',
@@ -70,14 +70,17 @@ for operations, the [runbook](RUNBOOK.md).
70
70
 
71
71
  **The harness is observed (W12, Lanes 1.5.0; spec
72
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
73
+ `agent.harness` is one of `claude | codex | kimi | grok | grok-cli | muse | zcode | shell | unknown` (W13 added `muse`
74
+ and `zcode`; `shell` is the walk's word for a
74
75
  pane and never reaches a published `agent{}`), an OBSERVED fact read from
75
76
  the pane's process tree, not a side effect of which title parser matched: `agent_state.harness_walk` reads
76
77
  `/proc/<pane pid>/task/*/children` (the reads `pane_has_child` already makes) and each process's `comm` in the
77
78
  container's /proc, breadth first from the pane's root down four levels and at most 64 processes, and the first
78
79
  process whose comm is `claude`, `codex`, `kimi`/`kimi-code` or `grok` — or whose `/proc/<pid>/exe` holds
79
80
  `/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
+ names it; since W13 also `muse` (Muse Code's launcher script), `zcode-cli` (ZCode's process title), a comm starting
82
+ `muse-bin-` (MEASURED: `muse-bin-1.4.10`, the cut of `muse-bin-1.4.10-R6427.1`) or an exe whose file name does.
83
+ Codex runs as `bash` → `node` → `codex`, so it is found one level down; two `grok` binaries are told
81
84
  apart by `/proc/<pid>/exe`: `…/downloads/grok-linux-<arch>` is Grok Build (the rule of
82
85
  `harness_grok.harness_of_exe`; the container walks of `lane-signal` and `lane_core` match
83
86
  `/downloads/grok-linux-x86_64`, the one arch measured), anything else grok-cli, an unreadable link `unknown`. Only
@@ -139,12 +142,33 @@ for operations, the [runbook](RUNBOOK.md).
139
142
  it reaches `agent{}` only through the hook sidecar (DEF-1980). The Grok Build file shapes come from its source and
140
143
  user guide; no logged-in session was measured yet (DEF-1981).
141
144
 
145
+ **Muse Code and ZCode** (W13, Lanes 1.6.0; spec
146
+ `docs/superpowers/specs/2026-10-16-lanes-harness-observers-muse-zcode-design.md`; `libexec/harness_muse.py`).
147
+ `agent_state.muse_sessions(root, panes, *, home, now)` forwards to `harness_muse.muse_sessions` (looked up at the
148
+ call like `harness_grok`); `statusd.agent_signals` passes {pane key: Muse pid} for every pane its walk observed as
149
+ `muse`. The join: the pane's Muse pid holds its session's `.session.lock` open (at most 512 fd links read; the
150
+ link must end `…/sessions/YYYY/MM/DD/<id>/.session.lock`, never a subagent's), the lock file names `pid=<pid>`
151
+ (a lock naming another pid is not this pane's); else the runtime registry's `process_generation_hint` `pid=<pid>`
152
+ and the newest 31 day directories searched for that id. Then the last 64 KiB of `session.jsonl`, whole lines only:
153
+ `runtime.user_intent.accepted` or run `started` → `working`; other in-run records keep it (a hook's run is no turn);
154
+ run `terminal` completed / cancelled → `needs_input`, failed → `errored`; `run_retracted` → `needs_input`; no turn
155
+ in a whole file → `idle`; `session.end` → no claim; a session older than an hour → no claim. The prompt the accepted
156
+ and `started` records carry is never read into a verdict (customer content). `session.name.changed` is the name (read from the log's first 32 KiB when the tail holds none: Muse names a session
157
+ near its start).
158
+ Verdict: `harness: muse`, `source: muse-session-file`, the session id, the Muse pid. `merge`'s order: sidecar >
159
+ Claude file > Kimi file > Muse file > Grok observer > Grok title > Muse title (`harness_muse.parse_muse_title`:
160
+ `⠙ <folder>` working, `<folder>` idle, `source: muse-title`) > Codex title > process. Muse and ZCode ring no bell
161
+ (`agent_state.BELL_LESS`): no rung turns their verdict into `done`. ZCode keeps sessions in SQLite and draws no
162
+ title: it reaches `agent{}` through its process (`zcode` / `idle`) and its hooks only. The approval records of
163
+ Muse's log were not captured (DEF-2057): the observer reads `needs_approval` only from a record kind that names an
164
+ approval or permission being asked.
165
+
142
166
  What is published is not `merge`'s word itself but its debounced form, `agent_state.settle`, with three more
143
167
  additive keys (`raw_status`, `raw_since`, `stable_since`): see [Attention queue](#attention-queue).
144
168
 
145
169
  The hook sidecar is `~/.lz/state/hooks/<lane id>.json` in the container, written by `~/.lz/bin/lane-signal`
146
170
  (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
171
+ Code, Grok Build, grok-cli, Muse Code, ZCode): `{v: 1, lane, harness, status, since, at, event, pid, start, session}` — `since` when the status began (kept
148
172
  while the same status is re-signalled), `at` the last event, `pid`/`start` the agent process and its
149
173
  starttime — plus `name`, the agent's own note when it reported one (`lanes_signal`, LNS-35: kept while the status
150
174
  repeats, dropped when it changes). A report (`event: lanes_signal`) stands against the same agent's hooks — the
@@ -161,14 +185,19 @@ for operations, the [runbook](RUNBOOK.md).
161
185
  container `lane ls` read (DEF-1572). A zmx lane's sidecar is therefore named by its zmx session, not by the
162
186
  `<ws>--z-<name>` id statusd publishes, and does not reach that lane's `agent{}` yet — the zmx lane's `agent` key
163
187
  stays lane-run's record (DEF-1573). A published sidecar adds `event` to `agent{}` and has `source: hook`. Hooks are
164
- never installed implicitly: `lanes hooks install --agent claude|codex|kimi|grok|grok-cli --container <name>`
188
+ never installed implicitly: `lanes hooks install --agent claude|codex|kimi|grok|grok-cli|muse|zcode --container <name>`
165
189
  prints the merge and writes only with `--write` (RUNBOOK "Installing agent hooks" and "Harnesses").
166
190
 
167
191
  **The container side** (W12 Track C). `lane-signal`'s default harness is `--harness`, else `LANE_HARNESS`, else
168
192
  the parent processes' (`comm` in the same set, a `grok` split by its exe, a native Claude Code file by its exe),
169
193
  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".
194
+ `kimi-code`/`kimi`, `grok-shell-*` → `grok`, `grok-cli*` → `grok-cli`; W13: `tbh` → `muse`, `zcode*` → `zcode`),
195
+ else the parent rule. W13: when a hook arrives with neither `LANE_ID` nor `TMUX` (Muse Code clears its hooks'
196
+ environment), `lane-signal` reads `LANE_ID`, `LANE_WS`, `TMUX` and `TMUX_PANE` from the agent's own
197
+ `/proc/<pid>/environ` — only for a pid that is a known harness, bounded, never through a link: the rule of
198
+ `lane_core.parent_environ`, pinned equal by `test_harness_w13.SignalIdentityFallback` (DEF-2031); a
199
+ `PostToolUseFailure` with `is_interrupt: true` is `needs_input` (ZCode runs no Stop after an interrupt, DEF-2091).
200
+ The per-harness operator steps are RUNBOOK "Harnesses"; what a person sees, the user guide's "Harnesses".
172
201
 
173
202
  **Fleet and Throttle facts** (W11, spec `docs/superpowers/specs/2026-10-11-lanes-fleet-dashboard-design.md` §2;
174
203
  DM-LANES-THROTTLE-PULSE, DM-LANES-FLEET). Two additive `status.json` keys feed the sidebar's fleet block and
@@ -178,15 +207,16 @@ for operations, the [runbook](RUNBOOK.md).
178
207
  its container's `127.0.0.1` (DEF-1525), so the host cannot poll it. `~/.lz/bin/throttle-pulse` (a contract file,
179
208
  Python 3 stdlib) runs as a loop only in the daemon's container — site.conf `LANES_THROTTLE_CONTAINER`, else the
180
209
  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
210
+ `tmux -L lanes-pulse` session `pulse`. Every 10 s it GETs the open `/pools/{claude,codex,kimi,muse-code,zai}/ready` and
211
+ `/metrics`, and the guarded `/pools/{claude,codex,kimi,muse-code,zai}/status` only with the bearer the operator adopted into
183
212
  `~/.lz/throttle-token` (`lanes throttle adopt-token`; the control port has no loopback exemption, DEF-1930), and
184
213
  writes `~/.lz/state/throttle.json` by rename when a value changed or 60 s passed — never anything else, never a
185
214
  POST (RUNBOOK "Throttle pulse"). The kimi pool (W12) rides the daemon's generic pool routes (`scoped_pool_state`):
186
215
  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
216
+ `overflow` is always null (`MONEY_POOLS` is claude and codex). W13 adds Throttle's native `muse-code` and `zai`
217
+ pools the same way (no paid overflow either: their overflow is null too). The sidebar draws one provider row per
218
+ pool — `◆ anthropic`, `◆ openai`, `◆ moonshot`, `◆ meta`, `◆ z.ai` — so the full block is a rule and eight rows
219
+ from 27 pane rows (`FLEET_FULL`), and the three-row form shows the pool with the smallest usable share, a pool with nothing
190
220
  configured never (DEF-2002). There is no Grok row: Throttle has no xAI pool.
191
221
 
192
222
  *`throttle`.* On every cycle statusd copies that file into `status.json`. It looks the daemon's container up again
@@ -711,6 +741,23 @@ identity so folder permissions work. On Linux it is a systemd user service that
711
741
  user manager holds, looked up before every login, so an agent a desktop session hands over after boot is used
712
742
  without a restart. The installer's remembered `--no-uploads` / `--no-desktop` remove what an earlier install added.
713
743
 
744
+ Desktop `lanes attach` execs `libexec/attach.py`, a PTY supervisor for exactly one Zellij client. It relays raw
745
+ input and output with nonblocking writes, forwards the terminal size, and restores terminal modes on exit.
746
+ `OUTPUT_LIMIT` (1 MiB) and `OUTPUT_AGE` (5 s) bound output waiting for the SSH terminal; no output means no timeout.
747
+ Crossing either bound terminates only its child, with `STOP_GRACE` (1 s) before KILL, and exits 1 for the existing
748
+ `lanes-launch` reconnect loop. The view server, its panes and their tmux/zmx holders remain alive. The existing
749
+ `clients/<device>.<client>.pid` now names the supervisor; its argv still carries the exact Zellij attach command,
750
+ so stale-predecessor cleanup reaches the supervisor and its owned child. No new durable state is introduced.
751
+
752
+ This closes the desktop failure observed on Anarchy on 2026-10-16 (DEF-1965). In the pinned Zellij 0.45.1,
753
+ [`ClientSender`](https://github.com/zellij-org/zellij/blob/v0.45.1/zellij-server/src/os_input_output.rs) removes a
754
+ client when its 5,000-message queue fills, but sends Exit only after draining queued messages. The
755
+ [`input handler`](https://github.com/zellij-org/zellij/blob/v0.45.1/zellij-client/src/input_handler.rs) can then wait
756
+ indefinitely for an action acknowledgement. The transport remains alive while the display cannot interact.
757
+ The supervisor bounds terminal backpressure before that can strand an attachment; it does not patch upstream
758
+ Zellij or cover its separate browser transport. `tests/attach_backpressure.py` proves the unread-terminal control,
759
+ bounded disconnect, holder survival and keyboard switching after reattachment with the installed pin.
760
+
714
761
  The sidebar sends an upload request naming paths, workspace and device. The client checks its folder allowlist,
715
762
  streams an archive over SSH, and reports original errors and progress. The host extracts data into that
716
763
  workspace's `uploads/` folder without overwriting files; an optional paste places the resulting paths in the