@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.
@@ -0,0 +1,531 @@
1
+ """Muse Code observer for agent{} (Lanes W13; spec
2
+ docs/superpowers/specs/2026-10-16-lanes-harness-observers-muse-zcode-design.md §3). Pure reads through the container
3
+ root statusd already holds (`/proc/<container init pid>/root`): no subprocess, no write, nothing done at import. Meta's
4
+ terms forbid reverse engineering the binary: every fact below was read from the files and processes a normal run makes
5
+ (`muse --provider echo` in a scratch HOME) or from the read-session skill Muse ships.
6
+
7
+ Muse Code (Meta, `muse` 1.4.10-R6427.1) runs as ONE process: the launcher `~/.local/bin/muse` (a script) execs
8
+ `~/.local/bin/muse-bin-<version>`, whose comm is the 15-byte cut `muse-bin-1.4.10` [M]. Per session it keeps:
9
+
10
+ * the event log `${XDG_DATA_HOME:-~/.local/share}/muse/sessions/YYYY/MM/DD/<session id>/session.jsonl` [D read-session
11
+ SKILL.md, M]: one JSON record per line, `recorded_at` in epoch MICROseconds, `payload_type` naming the record. The
12
+ records read here: `runtime.session` with `payload.kind` `run` and `payload.event.kind` `started` (a turn opens; its
13
+ `prompt` is the person's text — CUSTOMER CONTENT, never read into a verdict), the in-run kinds (model_*, assistant_*,
14
+ task_stream_linked, context_block_*, … and `payload.kind` `task` records), `terminal` with `terminal` completed |
15
+ failed | cancelled (the turn ends), `run_retracted` (a cancelled turn's prompt restored); `session.name.changed`
16
+ (`payload.new_name`, the session's name: automatic, two words such as `ash-megrez`, or the person's /rename);
17
+ `session.started`; and `session.end` when the process exits [M]. A `task failed` record also appears INSIDE a
18
+ completed turn (an internal task, MEASURED 2026-10-16): only the run's `terminal` says how a turn ended.
19
+ * `.session.lock` beside it, held OPEN by the live process (two fds) and holding `pid=<pid>\\nhost=<host>` [M]: the
20
+ pid <-> session join (`lock_session`, at most FD_MAX fd links read). Fallback: the live registry
21
+ `…/muse/runtime/muse/sessions/<session id>.json` {session_id, session_name, workspace_label,
22
+ process_generation_hint: "pid=<pid>", …}, deleted when the process exits [M] (`registry_session`).
23
+ * The pane title [M]: `⠙ <workspace basename>` (a braille spinner) while a turn runs, `<workspace basename>` between
24
+ turns, a single space when it exits. Muse rings NO bell [M], so nothing here (and nothing in agent_state.merge for a
25
+ Muse pane) turns a verdict into `done` by the bell.
26
+
27
+ `muse_sessions(root, panes, home)` -> {pane key: verdict}, the dict `agent_state.merge` takes from a session file:
28
+ {harness: 'muse', status, name, session_id, pid, since, source: 'muse-session-file', event}. `session_status` maps the
29
+ log's tail (its last TAIL_BYTES, whole lines only): `started` -> working (since: the turn's start); in-run records ->
30
+ working; `terminal` completed / cancelled -> needs_input, failed -> errored; `run_retracted` -> needs_input; no turn in
31
+ a log read from its first byte -> idle; `session.end` -> no claim (the process exited; the title and the process walk
32
+ decide). A log whose newest record and file are older than STALE makes no claim either.
33
+
34
+ NOT measured (needs_operator_capture, DEF-2057): the records an approval leaves. Muse 1.4.10 runs in an Auto-review
35
+ permission mode by default (its footer says so, and a session directory gains `approval-review/`); a turn that waits
36
+ for the person was not captured. APPROVAL_ASKED / APPROVAL_ANSWERED are a conservative guess at the kind names that
37
+ reads `needs_approval` only for a record whose kind says it, never for any other; the hooks' PermissionRequest /
38
+ Notification (`lanes hooks install --agent muse`) are the primary approval signal.
39
+
40
+ Every directory is opened without following a link (a container owns them), a FIFO is never opened blocking, and
41
+ nothing larger than its bound is read.
42
+ """
43
+ import json
44
+ import os
45
+ import re
46
+ import stat
47
+ import time
48
+
49
+ HARNESS = 'muse'
50
+ CONTAINER_HOME = '/home/coder'
51
+ DATA_HOME = ('.local', 'share', 'muse') # ${XDG_DATA_HOME:-~/.local/share}/muse, the default
52
+ SESSIONS = 'sessions'
53
+ REGISTRY = ('runtime', 'muse', 'sessions') # under DATA_HOME: one <session id>.json per live process
54
+ LOG = 'session.jsonl'
55
+ LOCK = '.session.lock'
56
+ SUBAGENT = 'subagent' # sessions/…/<id>/subagent/<child id>/ — a child's own log, never a pane's
57
+
58
+ TAIL_BYTES = 64 << 10 # session.jsonl read from its end per pane per cycle (MEASURED: 113 KB after one echo turn)
59
+ # The session's automatic name is written near the log's start (MEASURED over 13 echo sessions: at byte 13 440–21 730),
60
+ # so after one turn it is no longer in the tail: when the tail names none, the first HEAD_BYTES are read for it.
61
+ HEAD_BYTES = 32 << 10
62
+ JSON_MAX = 64 << 10 # a registry file (MEASURED: 235 B)
63
+ LOCK_MAX = 256 # .session.lock (MEASURED: 21 B, `pid=<pid>\nhost=<host>\n`)
64
+ FD_MAX = 512 # fd links read per Muse process for the lock (MEASURED: 47 open fds)
65
+ REGISTRY_MAX = 256 # registry files looked at per container per cycle
66
+ DAY_DIRS = 31 # newest sessions/YYYY/MM/DD directories searched for a registry session id
67
+ STALE = 3600 # seconds: an older session makes no claim
68
+ NAME_MAX = 120
69
+
70
+ SPINNER = frozenset('⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏') # MEASURED in the title: ⠙ ⠸ ⠼ ⠦ ⠧ (the rest of the braille set Muse may draw)
71
+ SHELL_TITLES = frozenset(('', 'bash', 'sh', 'zsh', 'fish'))
72
+ SOURCE_FILE = 'muse-session-file'
73
+ SOURCE_TITLE = 'muse-title'
74
+ TERMINAL = {'completed': 'needs_input', 'cancelled': 'needs_input', 'failed': 'errored'}
75
+ HOOK_KINDS = frozenset(('hook_run_started', 'hook_run_terminal')) # a hook's run, before and after a turn: not a turn
76
+ # The person's prompt accepted: the turn opens here, before its UserPromptSubmit hook and its `run started` (MEASURED:
77
+ # 1 s apart with a 1 s hook). The record carries the prompt's text (customer content): only its type is read.
78
+ INTENT = 'runtime.user_intent.accepted'
79
+ # [U] needs_operator_capture (DEF-2057): no approval was captured; these read `needs_approval` only for a kind that
80
+ # names an approval or permission being asked, and clear it on one that names the answer (or the turn's end).
81
+ APPROVAL_ASKED = re.compile(r'(?:^|[._])(?:approval|permission)[._](?:requested|pending|required|prompted|prompt)\Z')
82
+ APPROVAL_ANSWERED = re.compile(
83
+ r'(?:^|[._])(?:approval|permission)[._](?:resolved|granted|denied|decided|answered|approved|rejected)\Z')
84
+ SESSION_ID = re.compile(r'[0-9A-Za-z][0-9A-Za-z_-]{7,127}\Z')
85
+ DAY_PART = (re.compile(r'\d{4}\Z'), re.compile(r'\d{2}\Z'), re.compile(r'\d{2}\Z'))
86
+ HINT = re.compile(r'pid=(\d+)\Z')
87
+ LOCK_PID = re.compile(rb'(?m)^pid=(\d+)\s*$')
88
+
89
+ _DIR_FLAGS = os.O_RDONLY | os.O_DIRECTORY | getattr(os, 'O_CLOEXEC', 0)
90
+ _FILE_FLAGS = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | getattr(os, 'O_CLOEXEC', 0)
91
+
92
+
93
+ # ------------------------------------------------------------------------------------------------ pure parsers
94
+
95
+ def parse_muse_title(title):
96
+ """A Muse pane title -> {'harness': 'muse', 'status', 'name'}, or None (no title, a shell's, the exit's single
97
+ space). `⠙ <workspace>` (a braille spinner, a space, the workspace's name) -> working; any other title -> idle, its
98
+ text the name. Called only for a pane whose OBSERVED harness is muse: Codex draws a spinner too."""
99
+ text = title.strip() if isinstance(title, str) else ''
100
+ if text in SHELL_TITLES:
101
+ return None
102
+ if text[0] in SPINNER and (len(text) == 1 or text[1] == ' '):
103
+ return {'harness': HARNESS, 'status': 'working', 'name': _printable(text[1:])}
104
+ return {'harness': HARNESS, 'status': 'idle', 'name': _printable(text)}
105
+
106
+
107
+ def _printable(text):
108
+ if not isinstance(text, str):
109
+ return ''
110
+ return ''.join(ch for ch in text if ch.isprintable()).strip()[:NAME_MAX]
111
+
112
+
113
+ def parse_records(lines):
114
+ """The record dicts of session.jsonl lines (bytes or str); a line that is not one JSON object is skipped."""
115
+ records = []
116
+ for line in lines:
117
+ try:
118
+ record = json.loads(line.decode('utf-8') if isinstance(line, bytes) else line)
119
+ except (UnicodeDecodeError, ValueError):
120
+ continue
121
+ if isinstance(record, dict):
122
+ records.append(record)
123
+ return records
124
+
125
+
126
+ def _seconds(record):
127
+ """Epoch seconds of a record's `recorded_at` (epoch MICROseconds), or None."""
128
+ value = record.get('recorded_at')
129
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or value <= 0:
130
+ return None
131
+ return int(value // 1_000_000)
132
+
133
+
134
+ def _event(record):
135
+ """(payload kind, event kind) of a `runtime.session` record, else (None, None)."""
136
+ payload = record.get('payload')
137
+ if record.get('payload_type') != 'runtime.session' or not isinstance(payload, dict):
138
+ return None, None
139
+ event = payload.get('event')
140
+ kind = event.get('kind') if isinstance(event, dict) else None
141
+ return payload.get('kind'), (kind if isinstance(kind, str) else None)
142
+
143
+
144
+ def session_status(records, whole=True):
145
+ """{'status', 'since', 'event', 'name', 'ended', 'activity'} of one session from its log's records in file order,
146
+ or None when they say nothing about a turn (the tail window holds none; the caller falls back to the title).
147
+
148
+ `ended`: the last session boundary is `session.end` (a later `session.started` clears it). `name`: the last
149
+ `session.name.changed` new_name. `activity`: the newest `recorded_at`, epoch seconds (None when no record has one).
150
+ The status — the last turn boundary wins: the prompt accepted (INTENT) or run `started` -> working (since: the
151
+ first of them); a run `terminal` by its
152
+ `terminal` (TERMINAL; an unknown word still ends the turn: needs_input); `run_retracted` -> needs_input. Inside a
153
+ turn, a record that names an approval being asked (APPROVAL_ASKED, needs_operator_capture) -> needs_approval until
154
+ one names its answer (APPROVAL_ANSWERED) or the turn ends; any other in-run record (`runtime.session` of kind `run`
155
+ or `task`, but not a hook's run, HOOK_KINDS) is working, and when the window starts mid-turn (no boundary seen, not
156
+ the whole file) it alone says working. No turn in a whole file: idle (a session nobody has asked anything yet).
157
+ Never reads `prompt` or any text of a record: only kinds, names and times."""
158
+ status = since = event = None
159
+ name, ended, activity = '', False, None
160
+ in_turn = None # None: no turn boundary seen in this window
161
+ asking = False
162
+ for record in records:
163
+ at = _seconds(record)
164
+ if at is not None:
165
+ activity = at if activity is None else max(activity, at)
166
+ kind = record.get('payload_type')
167
+ payload = record.get('payload') if isinstance(record.get('payload'), dict) else {}
168
+ if kind == 'session.end':
169
+ ended = True
170
+ continue
171
+ if kind == 'session.started':
172
+ ended = False
173
+ continue
174
+ if kind == 'session.name.changed':
175
+ found = _printable(payload.get('new_name'))
176
+ if found:
177
+ name = found
178
+ continue
179
+ group, word = _event(record)
180
+ new = None
181
+ if kind == INTENT or (group == 'run' and word == 'started'):
182
+ if in_turn is not True:
183
+ asking = False
184
+ in_turn, new = True, ('needs_approval' if asking else 'working')
185
+ elif group == 'run' and word == 'terminal':
186
+ in_turn, asking = False, False
187
+ result = payload['event'].get('terminal')
188
+ new = TERMINAL.get(result, 'needs_input') if isinstance(result, str) else 'needs_input'
189
+ elif group == 'run' and word == 'run_retracted':
190
+ in_turn, asking, new = False, False, 'needs_input'
191
+ elif in_turn is False or (in_turn is None and whole):
192
+ continue # between turns (a hook, a resource sample) or before the first: says nothing
193
+ elif word and APPROVAL_ASKED.search(word):
194
+ asking, new = True, 'needs_approval'
195
+ elif word and APPROVAL_ANSWERED.search(word):
196
+ asking, new = False, 'working'
197
+ elif group in ('run', 'task') and word not in HOOK_KINDS:
198
+ new = None if asking else 'working'
199
+ if new is None:
200
+ continue
201
+ if new != status:
202
+ since, event = at, (word or kind)
203
+ status = new
204
+ if status is None and whole:
205
+ status, event = 'idle', 'session.started'
206
+ if status is None:
207
+ return None if not (ended or name) else {'status': None, 'since': None, 'event': None, 'name': name,
208
+ 'ended': ended, 'activity': activity}
209
+ return {'status': status, 'since': since, 'event': event, 'name': name, 'ended': ended, 'activity': activity}
210
+
211
+
212
+ def parse_registry(data):
213
+ """(session id, pid) of a runtime registry file's bytes, or None: `session_id` and `process_generation_hint`
214
+ `pid=<pid>` [M]."""
215
+ try:
216
+ doc = json.loads(data.decode('utf-8')) if data else None
217
+ except (UnicodeDecodeError, ValueError):
218
+ return None
219
+ if not isinstance(doc, dict):
220
+ return None
221
+ sid, hint = doc.get('session_id'), doc.get('process_generation_hint')
222
+ found = HINT.match(hint) if isinstance(hint, str) else None
223
+ if not isinstance(sid, str) or not SESSION_ID.match(sid) or not found:
224
+ return None
225
+ return sid, int(found.group(1))
226
+
227
+
228
+ def lock_parts(link):
229
+ """The container path components of a session directory from an fd link to its `.session.lock`, or None: the
230
+ link must end `…/sessions/YYYY/MM/DD/<session id>/.session.lock` (a subagent's lock is not the pane's session)."""
231
+ if not isinstance(link, str) or not link.startswith('/') or not link.endswith('/' + LOCK):
232
+ return None
233
+ parts = [part for part in link.split('/') if part]
234
+ if len(parts) < 6 or not all(_leaf(part) for part in parts):
235
+ return None
236
+ day, sid = parts[-5:-2], parts[-2]
237
+ if parts[-6] != SESSIONS or not SESSION_ID.match(sid) or not all(p.match(d) for p, d in zip(DAY_PART, day)):
238
+ return None
239
+ return parts[:-1]
240
+
241
+
242
+ # ------------------------------------------------------------------------------------------------ bounded reads
243
+
244
+ def _leaf(name):
245
+ return isinstance(name, str) and bool(name) and '/' not in name and '\0' not in name and name not in ('.', '..')
246
+
247
+
248
+ def _open_dir_at(dir_fd, parts):
249
+ """A new fd of dir_fd/<parts…>, every component a REAL directory opened O_NOFOLLOW."""
250
+ current = os.dup(dir_fd)
251
+ try:
252
+ for part in parts:
253
+ if not _leaf(part):
254
+ raise OSError(f'not one path component: {part!r}')
255
+ child = os.open(part, _DIR_FLAGS | os.O_NOFOLLOW, dir_fd=current)
256
+ os.close(current)
257
+ current = child
258
+ except BaseException:
259
+ os.close(current)
260
+ raise
261
+ return current
262
+
263
+
264
+ def _pread_all(fd, offset, count):
265
+ chunks, size = [], 0
266
+ while size < count:
267
+ chunk = os.pread(fd, count - size, offset + size)
268
+ if not chunk:
269
+ break
270
+ chunks.append(chunk)
271
+ size += len(chunk)
272
+ return b''.join(chunks)
273
+
274
+
275
+ def read_regular_at(dir_fd, name, limit):
276
+ """The bytes of `name` in `dir_fd` when it is a regular file of at most `limit` bytes, else None."""
277
+ if dir_fd is None or not _leaf(name):
278
+ return None
279
+ try:
280
+ fd = os.open(name, _FILE_FLAGS, dir_fd=dir_fd)
281
+ except OSError:
282
+ return None
283
+ try:
284
+ info = os.fstat(fd)
285
+ if not stat.S_ISREG(info.st_mode) or info.st_size > limit:
286
+ return None
287
+ data = _pread_all(fd, 0, limit + 1)
288
+ return data if len(data) <= limit else None
289
+ except OSError:
290
+ return None
291
+ finally:
292
+ os.close(fd)
293
+
294
+
295
+ def read_tail_lines(dir_fd, name, limit=None):
296
+ """(lines, whole, mtime) — the complete lines in the last `limit` bytes of `name`, whether the window reached the
297
+ file's first byte, and the file's mtime — or None (absent, a link, a FIFO, unreadable). A first line cut by the
298
+ window and a last line still being written are dropped, never parsed."""
299
+ limit = TAIL_BYTES if limit is None else limit
300
+ if dir_fd is None or not _leaf(name):
301
+ return None
302
+ try:
303
+ fd = os.open(name, _FILE_FLAGS, dir_fd=dir_fd)
304
+ except OSError:
305
+ return None
306
+ try:
307
+ info = os.fstat(fd)
308
+ if not stat.S_ISREG(info.st_mode):
309
+ return None
310
+ size = info.st_size
311
+ start = max(0, size - limit)
312
+ if start:
313
+ data = _pread_all(fd, start - 1, size - start + 1)
314
+ if data[:1] == b'\n':
315
+ data = data[1:]
316
+ else:
317
+ cut = data.find(b'\n')
318
+ data = b'' if cut < 0 else data[cut + 1:]
319
+ else:
320
+ data = _pread_all(fd, 0, size)
321
+ except OSError:
322
+ return None
323
+ finally:
324
+ os.close(fd)
325
+ if not data.endswith(b'\n'):
326
+ cut = data.rfind(b'\n')
327
+ data = b'' if cut < 0 else data[:cut + 1]
328
+ return [line for line in data.split(b'\n') if line.strip()], start == 0, info.st_mtime
329
+
330
+
331
+ def read_head_lines(dir_fd, name, limit=None):
332
+ """The complete lines in the first `limit` (default HEAD_BYTES) bytes of `name` (a last line cut by the window
333
+ dropped), or None."""
334
+ limit = HEAD_BYTES if limit is None else limit
335
+ if dir_fd is None or not _leaf(name):
336
+ return None
337
+ try:
338
+ fd = os.open(name, _FILE_FLAGS, dir_fd=dir_fd)
339
+ except OSError:
340
+ return None
341
+ try:
342
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
343
+ return None
344
+ data = _pread_all(fd, 0, limit)
345
+ except OSError:
346
+ return None
347
+ finally:
348
+ os.close(fd)
349
+ cut = data.rfind(b'\n')
350
+ return [line for line in data[:cut + 1].split(b'\n') if line.strip()] if cut >= 0 else []
351
+
352
+
353
+ def head_name(lines):
354
+ """The last `session.name.changed` name among records (the head of a log), or ''."""
355
+ found = ''
356
+ for record in parse_records(lines or ()):
357
+ payload = record.get('payload')
358
+ if record.get('payload_type') == 'session.name.changed' and isinstance(payload, dict):
359
+ found = _printable(payload.get('new_name')) or found
360
+ return found
361
+
362
+
363
+ def _proc_link(proc_root, pid, name):
364
+ try:
365
+ return os.readlink(os.path.join(proc_root, str(pid), name))
366
+ except OSError:
367
+ return None
368
+
369
+
370
+ def lock_session(proc_root, pid, limit=None):
371
+ """The container path components of the session directory whose `.session.lock` process `pid` holds open (its fd
372
+ links, the lowest `limit` — default FD_MAX — read), or None."""
373
+ limit = FD_MAX if limit is None else limit
374
+ fd_dir = os.path.join(proc_root, str(int(pid)), 'fd')
375
+ try:
376
+ names = os.listdir(fd_dir)
377
+ except OSError:
378
+ return None
379
+ for name in sorted((n for n in names if n.isdigit()), key=int)[:limit]:
380
+ parts = lock_parts(_proc_link(proc_root, pid, os.path.join('fd', name)))
381
+ if parts:
382
+ return parts
383
+ return None
384
+
385
+
386
+ def _lock_pid(session_fd):
387
+ """The pid `.session.lock` names, or None when it names none (an older Muse, an unreadable file)."""
388
+ data = read_regular_at(session_fd, LOCK, LOCK_MAX)
389
+ found = LOCK_PID.search(data) if data else None
390
+ return int(found.group(1)) if found else None
391
+
392
+
393
+ # ------------------------------------------------------------------------------------------------ the observer
394
+
395
+ class _MuseHome:
396
+ """One container's Muse data directory for one call: the root fd, the registry (read at most once)."""
397
+
398
+ def __init__(self, root, home):
399
+ self.root = str(root)
400
+ self.home = [part for part in str(home).split('/') if part]
401
+ self._registry = None
402
+
403
+ def _open(self, parts):
404
+ base = os.open(self.root, _DIR_FLAGS)
405
+ try:
406
+ return _open_dir_at(base, parts)
407
+ finally:
408
+ os.close(base)
409
+
410
+ def registry(self):
411
+ """{pid: session id} of the live registry's files (at most REGISTRY_MAX)."""
412
+ if self._registry is None:
413
+ self._registry = {}
414
+ try:
415
+ fd = self._open(self.home + list(DATA_HOME) + list(REGISTRY))
416
+ except OSError:
417
+ return self._registry
418
+ try:
419
+ names = sorted(n for n in os.listdir(fd) if n.endswith('.json'))[:REGISTRY_MAX]
420
+ for name in names:
421
+ found = parse_registry(read_regular_at(fd, name, JSON_MAX))
422
+ if found and found[1] not in self._registry:
423
+ self._registry[found[1]] = found[0]
424
+ except OSError:
425
+ pass
426
+ finally:
427
+ os.close(fd)
428
+ return self._registry
429
+
430
+ def session_dir(self, parts):
431
+ try:
432
+ return self._open(parts)
433
+ except OSError:
434
+ return None
435
+
436
+ def find_session(self, session_id):
437
+ """A new fd of sessions/YYYY/MM/DD/<session id> in the default data directory (the newest DAY_DIRS days), or
438
+ None."""
439
+ try:
440
+ top = self._open(self.home + list(DATA_HOME) + [SESSIONS])
441
+ except OSError:
442
+ return None
443
+ try:
444
+ days = []
445
+ for year in sorted((n for n in os.listdir(top) if DAY_PART[0].match(n)), reverse=True):
446
+ for month in sorted((n for n in _listdir_at(top, (year,)) if DAY_PART[1].match(n)), reverse=True):
447
+ for day in sorted((n for n in _listdir_at(top, (year, month)) if DAY_PART[2].match(n)),
448
+ reverse=True):
449
+ days.append((year, month, day))
450
+ if len(days) >= DAY_DIRS:
451
+ break
452
+ if len(days) >= DAY_DIRS:
453
+ break
454
+ if len(days) >= DAY_DIRS:
455
+ break
456
+ for day in days:
457
+ try:
458
+ return _open_dir_at(top, day + (session_id,))
459
+ except OSError:
460
+ continue
461
+ except OSError:
462
+ return None
463
+ finally:
464
+ os.close(top)
465
+ return None
466
+
467
+
468
+ def _listdir_at(fd, parts):
469
+ try:
470
+ sub = _open_dir_at(fd, parts)
471
+ except OSError:
472
+ return []
473
+ try:
474
+ return os.listdir(sub)
475
+ except OSError:
476
+ return []
477
+ finally:
478
+ os.close(sub)
479
+
480
+
481
+ def observe(muse, proc_root, pid, now):
482
+ """The verdict for one Muse process, or None (no session joined, none readable, stale, ended, or saying nothing)."""
483
+ parts = lock_session(proc_root, pid)
484
+ session_fd = muse.session_dir(parts) if parts else None
485
+ session_id = parts[-1] if parts else None
486
+ if session_fd is None:
487
+ session_id = muse.registry().get(int(pid))
488
+ session_fd = muse.find_session(session_id) if session_id else None
489
+ if session_fd is None:
490
+ return None
491
+ try:
492
+ holder = _lock_pid(session_fd)
493
+ if holder is not None and holder != int(pid):
494
+ return None # the lock names another process: not this pane's session
495
+ tail = read_tail_lines(session_fd, LOG)
496
+ found = session_status(parse_records(tail[0]), tail[1]) if tail is not None else None
497
+ if found is not None and not found['name'] and not tail[1]:
498
+ found['name'] = head_name(read_head_lines(session_fd, LOG))
499
+ finally:
500
+ os.close(session_fd)
501
+ if found is None or found['ended'] or found['status'] is None:
502
+ return None
503
+ mtime = tail[2]
504
+ activity = max(stamp for stamp in (found['activity'], mtime) if stamp is not None)
505
+ if now - activity > STALE:
506
+ return None
507
+ return {'harness': HARNESS, 'status': found['status'], 'name': found['name'], 'session_id': session_id,
508
+ 'pid': int(pid), 'since': found['since'] or 0, 'source': SOURCE_FILE, 'event': found['event']}
509
+
510
+
511
+ def muse_sessions(root, panes, *, home=CONTAINER_HOME, now=None):
512
+ """{pane key: verdict} for the panes Muse Code runs in. `root` is the container's root as the host sees it
513
+ (`/proc/<init pid>/root`; its /proc is `root/proc`, the container's pid namespace), `panes` {pane key: the pane's
514
+ Muse pid} (statusd: the panes whose harness walk found `muse`), `home` the container home whose default data
515
+ directory holds the registry. One read per pane: the process's fd links to its `.session.lock`, else the registry's
516
+ `process_generation_hint`; then the session log's tail."""
517
+ now = time.time() if now is None else now
518
+ proc_root = os.path.join(str(root), 'proc')
519
+ muse = _MuseHome(root, home)
520
+ out = {}
521
+ for key, pid in (panes or {}).items():
522
+ try:
523
+ pid = int(pid)
524
+ except (TypeError, ValueError):
525
+ continue
526
+ if pid <= 0:
527
+ continue
528
+ verdict = observe(muse, proc_root, pid, now)
529
+ if verdict is not None:
530
+ out[str(key)] = verdict
531
+ return out
@@ -1384,7 +1384,8 @@ def canonical_recipe(lane_id, ws, socket, session, folder, cmd='bash -l'):
1384
1384
  """The one create recipe that gives a new tmux lane its identity: LANE_ID / LANE_WS for scripts and hooks, the
1385
1385
  lanes/<ws>/<id> trace for Throttle (ANTHROPIC_CUSTOM_HEADERS for Claude Code, KIMI_CODE_CUSTOM_HEADERS for Kimi
1386
1386
  Code, which blanks the Anthropic one, LANES_TRACE for Grok Build's `env_http_headers`, THROTTLE_TRACE for the
1387
- rest) and GROK_HOME (Grok Build's home, never grok-cli's ~/.grok; W12). `tmux new -A` attaches when the session
1387
+ rest, and (W13) MUSE_CUSTOM_HEADERS for Muse Code, measured but undocumented by Meta) and GROK_HOME (Grok Build's
1388
+ home, never grok-cli's ~/.grok; W12). `tmux new -A` attaches when the session
1388
1389
  already exists, so the env lands only on a real create — never through `set-environment`, which would leak into
1389
1390
  unrelated windows. `cmd` is a shell command string, appended as is."""
1390
1391
  trace = lane_trace(lane_id, ws)
@@ -1400,7 +1401,7 @@ def canonical_recipe(lane_id, ws, socket, session, folder, cmd='bash -l'):
1400
1401
  return (f'tmux -L {shlex.quote(socket)} new -ADs {shlex.quote(session)} -c {shlex.quote(folder)} '
1401
1402
  f'-e LANE_ID={lane_id} -e LANE_WS={ws} -e ANTHROPIC_CUSTOM_HEADERS={header} '
1402
1403
  f'-e THROTTLE_TRACE={trace} -e KIMI_CODE_CUSTOM_HEADERS={header} -e LANES_TRACE={trace} '
1403
- f'-e GROK_HOME={CONTAINER_HOME}/.grok-build {cmd}')
1404
+ f'-e GROK_HOME={CONTAINER_HOME}/.grok-build -e MUSE_CUSTOM_HEADERS={header} {cmd}')
1404
1405
 
1405
1406
 
1406
1407
  # What `lane open` gets when it runs a lane's recorded `create` for a lane that does not exist (plan Task 18; the
@@ -312,21 +312,24 @@ def load_economy(doc):
312
312
  # Pinned above the upload strip: Throttle in one line, an availability bar per provider, the host and its top
313
313
  # containers — from status.json's `fleet` and `throttle` objects ONLY (statusd publishes both, spec §2.1/§2.2), so the
314
314
  # block costs no stat, read or fork of the sidebar's own on a tick. A pane of FLEET_FULL rows or more draws a rule and
315
- # six rows (W12 added the Kimi row: FLEET_FULL 24 -> 25, so the body a full block leaves keeps its W11 floor), one of
315
+ # six rows (W12 added the Kimi row: FLEET_FULL 24 -> 25, so the body a full block leaves keeps its W11 floor; W13 the
316
+ # Muse Code and Z.ai rows: eight rows, FLEET_FULL 25 -> 27), one of
316
317
  # FLEET_SHORT..FLEET_FULL − 1 rows three (Throttle, the worse provider, the host), a shorter one none; `i` folds it to
317
318
  # one line (`look.fleet_collapsed` in the owner's profile). The lane list scrolls in the rest.
318
319
  FLEET_SCHEMA = 1 # the `v` of both objects this reader understands (absent = 1; another is ignored)
319
- FLEET_FULL, FLEET_SHORT = 25, 18
320
+ FLEET_FULL, FLEET_SHORT = 27, 18
320
321
  # Own-clock floors beneath the publishers' `stale` flags (DEF-1528's rule, as ECONOMY_STALE): statusd writes `fleet`
321
322
  # every 10 s (a 12 s cycle at its slowest) and the pulse `throttle.at` at least every 60 s, which statusd flags past
322
323
  # 60 s; a status.json that stopped changing (statusd frozen) still turns the block stale here.
323
324
  FLEET_STALE_S, PULSE_STALE_S = 60, 90
324
325
  BAR_CELLS = 10
325
326
  # The pulse's pools, in row order, and the row's word: the vendor (W12, spec 2026-10-15 §6: Kimi Code's pool is
326
- # `kimi`, its vendor Moonshot AI). No grok row until Throttle has an xAI pool (the 2026-10-14 xAI pool review's gates).
327
- PROVIDERS = (('claude', 'anthropic'), ('codex', 'openai'), ('kimi', 'moonshot'))
328
- # The pools whose overflow is money (fleet_money): Kimi has no paid overflow lane, so a kimi figure is never shown —
329
- # the pulse records it as nulls (throttle-pulse MONEY_POOLS), and this reader would not show one if it did not.
327
+ # `kimi`, its vendor Moonshot AI; W13: Throttle's native `muse-code` pool, vendor Meta, and `zai`, vendor Z.ai). No grok
328
+ # row until Throttle has an xAI pool (the 2026-10-14 xAI pool review's gates).
329
+ PROVIDERS = (('claude', 'anthropic'), ('codex', 'openai'), ('kimi', 'moonshot'), ('muse-code', 'meta'),
330
+ ('zai', 'z.ai'))
331
+ # The pools whose overflow is money (fleet_money): Kimi, Muse Code and Z.ai have no paid overflow lane, so their figure is
332
+ # never shown — the pulse records it as nulls (throttle-pulse MONEY_POOLS), and this reader would not show one if it did not.
330
333
  MONEY_POOLS = ('claude', 'codex')
331
334
  LABEL_CELLS = 9 # the label field of the Throttle and provider rows (`anthropic`), then one blank
332
335
  # `429 +N`: how much the pools' upstream_429_total grew over the last SURGE_S seconds of pulse samples this sidebar
@@ -97,7 +97,8 @@ PULSE_REARM_TIMEOUT = 10
97
97
  DAEMON_REFRESH = 300 # site.conf is re-read this often for LANES_THROTTLE_CONTAINER (CONTAINER_REFRESH)
98
98
  PULSE_BIN = ('home', 'coder', '.lz', 'bin')
99
99
  PULSE_PROGRAM = 'throttle-pulse'
100
- PULSE_POOLS = ('claude', 'codex', 'kimi') # throttle-pulse POOLS (W12 adds kimi; its overflow is always null)
100
+ # throttle-pulse POOLS (W12 adds kimi, W13 muse-code and zai; their overflow is always null)
101
+ PULSE_POOLS = ('claude', 'codex', 'kimi', 'muse-code', 'zai')
101
102
  PULSE_COUNTS = ('configured', 'usable', 'cooling', 'exhausted', 'benched', 'auth_failed')
102
103
  PULSE_OVERFLOW = ('spend_today_usd', 'budget_usd', 'fraction', 'alert_level_pct')
103
104
  PULSE_PHASE = re.compile(r'[a-z][a-z0-9_-]{0,31}\Z', re.ASCII)
@@ -348,14 +349,15 @@ def agent_signals(containers, sessions_by_server, now=None):
348
349
  """The per-container inputs `agent_state.merge` needs, read ONCE per container per cycle through the
349
350
  container's root as the host sees it (`/proc/<init pid>/root`), so liveness is judged in the container's pid
350
351
  namespace and nothing is spawned. -> {container: {'claude': claude_by_pane, 'kimi': kimi_by_pane,
351
- 'grok': grok_by_pane}}.
352
+ 'grok': grok_by_pane, 'muse': muse_by_pane}}.
352
353
 
353
354
  Sets `harness` (and `harness_pid` when one was found) on every session row whose harness could be observed, in
354
355
  place (W12, spec §2: `agent_state.HarnessCache` over `harness_walk` — the pane's process tree in the container's
355
356
  /proc); the Kimi Code observer (`agent_state.kimi_sessions`) reads only for the panes found running `kimi`, and the
356
357
  Grok Build observer (`agent_state.grok_sessions` -> `harness_grok.grok_sessions`) gets the ROWS of the panes found
357
358
  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.
359
+ container's GROK_TITLES memory. W13: the Muse Code observer (`agent_state.muse_sessions` ->
360
+ `harness_muse.muse_sessions`) reads only for the panes found running `muse`, by their Muse pid.
359
361
 
360
362
  Also sets `pane_has_child` on the session rows that need it, in place: only a pane whose title Codex wrote
361
363
  (`parse_codex_title`, gated by the observed harness) over a bare-shell foreground can be a harness that ended into
@@ -378,10 +380,10 @@ def agent_signals(containers, sessions_by_server, now=None):
378
380
  except Exception as error:
379
381
  log(f'agents {container}', error)
380
382
  claude = {}
381
- signals[container] = mine = {'claude': claude, 'kimi': {}, 'grok': {}}
383
+ signals[container] = mine = {'claude': claude, 'kimi': {}, 'grok': {}, 'muse': {}}
382
384
  panes = [pane for (owner, _socket), sessions in sessions_by_server.items() if owner == container
383
385
  for pane in sessions.values() if str(pane.get('pane_pid') or '').isdigit()]
384
- observers = {'kimi': {}, 'grok': {}}
386
+ observers = {'kimi': {}, 'grok': {}, 'muse': {}}
385
387
  for pane in panes:
386
388
  try:
387
389
  harness, harness_pid = HARNESS_CACHE.harness(root / 'proc', int(pane['pane_pid']))
@@ -396,6 +398,8 @@ def agent_signals(containers, sessions_by_server, now=None):
396
398
  pane['harness_pid'] = harness_pid
397
399
  if harness == 'kimi' and harness_pid:
398
400
  observers['kimi'][AS.pane_key(pane)] = harness_pid
401
+ elif harness == 'muse' and harness_pid:
402
+ observers['muse'][AS.pane_key(pane)] = harness_pid
399
403
  elif harness in GROK_OBSERVED or (harness == 'unknown' and harness_pid):
400
404
  observers['grok'][AS.pane_key(pane)] = pane
401
405
  if observers['kimi']:
@@ -403,6 +407,11 @@ def agent_signals(containers, sessions_by_server, now=None):
403
407
  mine['kimi'] = AS.kimi_sessions(root, observers['kimi'], now, home)
404
408
  except Exception as error:
405
409
  log(f'kimi {container}', error)
410
+ if observers['muse']:
411
+ try:
412
+ mine['muse'] = AS.muse_sessions(root, observers['muse'], home=L.CONTAINER_HOME, now=now)
413
+ except Exception as error:
414
+ log(f'muse {container}', error)
406
415
  if observers['grok']:
407
416
  titles = GROK_TITLES.setdefault(str(root), {})
408
417
  titled.add(str(root))
@@ -435,7 +444,8 @@ def agent_signals(containers, sessions_by_server, now=None):
435
444
 
436
445
  def merged(pane, signals, sidecar):
437
446
  """`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', {}))
447
+ return AS.merge(pane, signals.get('claude', {}), sidecar, signals.get('kimi', {}), signals.get('grok', {}),
448
+ signals.get('muse', {}))
439
449
 
440
450
 
441
451
  SIDECAR_MAX_AGE = 3600 # a hook sidecar whose last event is older than this loses to the live signals