@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.
@@ -27,9 +27,35 @@ child_liveness(): a per-pane read of /proc/<pid>/task/*/children (this kernel ex
27
27
  MEASURED 2026-10-04 on 6.8.0-106-generic), falling back to one child_parents() /proc walk only when that read
28
28
  fails; a pid whose liveness could not be determined gets no `pane_has_child` key (unknown never demotes).
29
29
  pane_current_command stays a secondary hint (a non-shell pane root such as node is the harness itself, child or not).
30
+
31
+ W12 (spec `docs/superpowers/specs/2026-10-15-lanes-harness-observers-design.md` §2/§3): WHICH harness a pane runs is an
32
+ OBSERVED fact, no longer a side effect of parsing its title. `harness_walk` reads the pane's process tree in the
33
+ container's /proc (the `pane_has_child` reads, depth <= 4, breadth first) and takes the first `comm` in HARNESS_COMMS.
34
+ MEASURED 2026-10-15 on this workspace: Claude Code = `bash` -> `claude`; Codex = `bash` -> `node` (the npm wrapper) ->
35
+ `codex` (the native binary), so Codex is found one level down; Kimi Code 2.1.1 = `bash` -> `kimi-code` (exe
36
+ `~/.kimi-code/bin/kimi`, its /proc/<pid>/cwd equal to its session's state.json `cwd`). Grok Build and grok-cli both run
37
+ as `comm` `grok` and are told apart by /proc/<pid>/exe (evidence `2026-10-15-grok-build-measurement.md`); native Claude
38
+ Code before 2.1.299 runs with its version as `comm` (`2.1.295`, MEASURED 2026-10-15) and is named by its exe (DEF-1993).
39
+ Grok Build's own files are read by libexec/harness_grok.py (Track B) through `grok_sessions`, one contract. The Codex
40
+ title rule then runs only where Codex can be: before W12 any title of two or more words was read as Codex idle, so a
41
+ Kimi lane (static title `check the code and logs for thro`) was published `codex / idle` (DEF-1970).
42
+
43
+ Kimi Code keeps its state in files (`kimi_sessions`): `~/.kimi-code/sessions/wd_<name>_<sha256(root)[:12]>/
44
+ session_<uuid>/state.json` and the session's event log `agents/main/wire.jsonl`. MEASURED 2026-10-15 over the 24
45
+ sessions of this workspace (Kimi Code 2.1.1, protocol of state.json `version` 2): every event carries `time` in epoch
46
+ ms; a turn is `agent.turn.started` + `turn.prompt` ... `context.append_loop_event` (step.begin / tool.call /
47
+ tool.result / step.end / content.part) and `llm.request` ... `turn.ended{reason}` (completed 35, cancelled 9, failed 4)
48
+ then, after the turn's `agent.message.appended` dump, `agent.turn.ended{outcome: done}`; an approval is
49
+ `interaction.request{kind: approval}` answered by `interaction.resolved` of the same id (a `question` is the other kind,
50
+ and a cancelled turn can leave either unanswered); state.json drops `lastTurnReason` when a turn starts and sets it
51
+ (`completed|failed|cancelled`) when one ends.
30
52
  """
53
+ import importlib.util
31
54
  import json
32
55
  import os
56
+ import stat
57
+ import sys
58
+ import time
33
59
  from pathlib import Path
34
60
 
35
61
  SPINNER = set('⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⠁⠂⠄⡀⢀⠠⠐⠈')
@@ -48,6 +74,37 @@ RANK = {'needs': 3, 'working': 2, 'done': 1, None: 0}
48
74
  DWELL = {'needs': 8, 'working': 5, 'done': 5, None: 0}
49
75
  GONE_DWELL = 5
50
76
 
77
+ # W12 harness identity (spec §2). The comm (/proc/<pid>/comm, at most 15 bytes) of each harness, and what it is called.
78
+ HARNESS_COMMS = {'claude': 'claude', 'codex': 'codex', 'kimi': 'kimi', 'kimi-code': 'kimi', 'grok': 'grok'}
79
+ AGENT_HARNESSES = ('claude', 'codex', 'kimi', 'grok', 'grok-cli')
80
+ HARNESSES = AGENT_HARNESSES + ('shell', 'unknown') # the published `agent.harness` enum (DM-LANES-STATUS)
81
+ HARNESS_DEPTH = 4 # levels below the pane's root process walked for a harness
82
+ HARNESS_FANOUT = 64 # processes examined per pane at most: a fork bomb under a pane costs a bounded read
83
+ # Grok Build's /proc/<pid>/exe is `$GROK_HOME/downloads/grok-linux-<arch>` (harness_grok.harness_of_exe, the same rule);
84
+ # any other process named grok is grok-cli.
85
+ GROK_BUILD_DIR, GROK_BUILD_PREFIX = 'downloads', 'grok-linux-'
86
+ # Native Claude Code runs `~/.local/share/claude/versions/<version>`; before 2.1.299 that file name, the version, was its
87
+ # comm (MEASURED 2026-10-15: `2.1.295`), so a process whose exe holds this is Claude Code whatever its comm (DEF-1993;
88
+ # lane-signal's and lane_core's CLAUDE_EXE, the same rule).
89
+ CLAUDE_EXE = '/claude/versions/'
90
+
91
+ # W12 Kimi Code observer (spec §3).
92
+ KIMI_SESSIONS = ('.kimi-code', 'sessions') # under the container home
93
+ KIMI_STALE = 3600 # a session whose newest event or state write is older than this makes no claim
94
+ KIMI_TAIL = 64 << 10 # bytes of agents/main/wire.jsonl read from its end (whole lines only)
95
+ KIMI_STATE_MAX = 256 << 10 # a bigger state.json is not read (MEASURED 2026-10-15: the largest of 24 is 41 KB)
96
+ KIMI_MAX_WORKDIRS = 256
97
+ KIMI_MAX_SESSIONS = 2048 # session directories looked at (one stat each) per container per cycle at most
98
+ KIMI_NAME_MAX = 120 # state.json `title` is the session's first prompt, cut: the published `name` is cut again
99
+ KIMI_ENDED = {'completed': 'needs_input', 'cancelled': 'needs_input', 'failed': 'errored'}
100
+ # W12 Grok Build observer (spec §4; libexec/harness_grok.py): Lanes runs Grok Build under GROK_HOME=~/.grok-build, a
101
+ # directory name relative to the container home (harness_grok.GROK_HOME_DEFAULT and CONTAINER_HOME, the same values).
102
+ GROK_HOME = '.grok-build'
103
+ CONTAINER_HOME = '/home/coder'
104
+ GROK_UNKNOWN = 'unknown' # the Grok observer's word for a pane it sees but cannot join (leader mode): no agent{}
105
+ _DIR_FLAGS = os.O_RDONLY | os.O_DIRECTORY | getattr(os, 'O_CLOEXEC', 0)
106
+ _FILE_FLAGS = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | getattr(os, 'O_CLOEXEC', 0)
107
+
51
108
 
52
109
  def pane_has_child(proc_root, pid):
53
110
  """Does `pid` have any child process? Reads every /proc/<pid>/task/<tid>/children (a child is listed under
@@ -111,6 +168,147 @@ def child_liveness(proc_root, pids):
111
168
  return out
112
169
 
113
170
 
171
+ def children_of(proc_root, pid):
172
+ """The child pids of `pid` (every /proc/<pid>/task/<tid>/children, as `pane_has_child` reads them) as a frozenset,
173
+ or None when any of those reads fails — unknown, never "no children"."""
174
+ task = Path(proc_root, str(pid), 'task')
175
+ try:
176
+ tids = os.listdir(task)
177
+ except OSError:
178
+ return None
179
+ if not tids:
180
+ return None
181
+ kids = set()
182
+ for tid in tids:
183
+ try:
184
+ words = Path(task, tid, 'children').read_text().split()
185
+ except OSError:
186
+ return None
187
+ kids.update(int(word) for word in words if word.isdigit())
188
+ return frozenset(kids)
189
+
190
+
191
+ def _comm(proc_root, pid):
192
+ try:
193
+ return Path(proc_root, str(pid), 'comm').read_text().strip() or None
194
+ except OSError:
195
+ return None
196
+
197
+
198
+ def _exe(proc_root, pid):
199
+ """/proc/<pid>/exe without an auto-update's ` (deleted)` suffix, or None when it cannot be read."""
200
+ try:
201
+ exe = os.readlink(Path(proc_root, str(pid), 'exe'))
202
+ except OSError:
203
+ return None
204
+ return exe[:-len(' (deleted)')] if exe.endswith(' (deleted)') else exe
205
+
206
+
207
+ def _named(proc_root, pid, comm):
208
+ """The harness a process whose comm is in HARNESS_COMMS runs. `grok` is Grok Build when /proc/<pid>/exe is
209
+ `…/downloads/grok-linux-<arch>` (harness_grok.harness_of_exe's rule), grok-cli for any other file; an unreadable
210
+ exe is `unknown`, never a guess (statusd still hands that pane to the Grok observer, which names it Grok Build only
211
+ when active_sessions.json names its pid)."""
212
+ harness = HARNESS_COMMS[comm]
213
+ if harness != 'grok':
214
+ return harness
215
+ exe = _exe(proc_root, pid)
216
+ if exe is None:
217
+ return 'unknown'
218
+ parent, _slash, name = exe.rpartition('/')
219
+ return 'grok' if name.startswith(GROK_BUILD_PREFIX) and parent.rpartition('/')[2] == GROK_BUILD_DIR else 'grok-cli'
220
+
221
+
222
+ def _harness_of(proc_root, pid, comm):
223
+ """The harness process `pid` (whose comm is `comm`) is, or None: a comm in HARNESS_COMMS (`_named`), else — for a
224
+ process that is not a shell — native Claude Code by its executable (CLAUDE_EXE, DEF-1993). A shell's exe is its
225
+ shell, so shells cost no readlink."""
226
+ if comm in HARNESS_COMMS:
227
+ return _named(proc_root, pid, comm)
228
+ if comm is None or comm.lstrip('-') in SHELL_COMMANDS:
229
+ return None
230
+ exe = _exe(proc_root, pid)
231
+ return 'claude' if exe is not None and CLAUDE_EXE in exe else None
232
+
233
+
234
+ def harness_walk(proc_root, pid, kids=None):
235
+ """(harness, harness pid) of the pane whose root process is `pid`, read in `proc_root` (the container's /proc as
236
+ the host sees it). Breadth first from the root itself down HARNESS_DEPTH levels of children, the first process
237
+ whose comm is in HARNESS_COMMS names it (`_named`), so a harness that runs another as a tool is still the lane's
238
+ harness. A process whose comm is not a harness's but whose executable is a native Claude Code file (CLAUDE_EXE:
239
+ before 2.1.299 its comm was its version) is Claude Code (`_harness_of`, DEF-1993). No harness found: `shell` when
240
+ every process read is a shell (SHELL_COMMANDS) and the tree ended within the depth, else `unknown` (another
241
+ program, a comm or children read that failed, the depth or HARNESS_FANOUT cut) with harness pid 0. (None, 0) when
242
+ the root's own children cannot be read: the harness was not observed at all. `kids` = the root's children when the
243
+ caller has read them already (`HarnessCache`)."""
244
+ kids = children_of(proc_root, pid) if kids is None else kids
245
+ if kids is None:
246
+ return None, 0
247
+ comm = _comm(proc_root, pid)
248
+ found = _harness_of(proc_root, pid, comm)
249
+ if found:
250
+ return found, int(pid)
251
+ only_shells = comm is not None and comm.lstrip('-') in SHELL_COMMANDS
252
+ level, seen = sorted(kids), 0
253
+ for _depth in range(HARNESS_DEPTH):
254
+ below = []
255
+ for child in level:
256
+ seen += 1
257
+ if seen > HARNESS_FANOUT:
258
+ return 'unknown', 0
259
+ name = _comm(proc_root, child)
260
+ found = _harness_of(proc_root, child, name)
261
+ if found:
262
+ return found, child
263
+ if name is None or name.lstrip('-') not in SHELL_COMMANDS:
264
+ only_shells = False
265
+ grand = children_of(proc_root, child)
266
+ if grand is None:
267
+ only_shells = False
268
+ else:
269
+ below.extend(sorted(grand))
270
+ level = below
271
+ if not level:
272
+ break
273
+ return ('shell' if only_shells and not level else 'unknown'), 0
274
+
275
+
276
+ class HarnessCache:
277
+ """statusd's per-pane memory of `harness_walk`, keyed by (proc root, pane pid): each cycle reads the pane's own
278
+ children and the comm of the harness it found; the tree is walked again when that child set changed, when that
279
+ process exited or its comm is no longer the one it had when it was found (an exec in place — compared as the comm
280
+ itself, so a native Claude Code named by its version, whose comm is no HARNESS_COMMS key, is re-walked too), or
281
+ when the last answer was not a harness (`shell`/`unknown` are never kept: a wrapper such as Codex's `node` starts
282
+ its harness one level down without changing the pane's own children). `prune` drops the panes a cycle did not
283
+ see."""
284
+
285
+ def __init__(self):
286
+ self.entries = {}
287
+ self.walks = 0 # how many trees were walked (the tests' view of a cache hit)
288
+
289
+ def harness(self, proc_root, pid):
290
+ key = (str(proc_root), int(pid))
291
+ kids = children_of(proc_root, pid)
292
+ if kids is None:
293
+ self.entries.pop(key, None)
294
+ return None, 0
295
+ hit = self.entries.get(key)
296
+ if hit and hit[0] == kids and hit[3] is not None and _comm(proc_root, hit[2]) == hit[3]:
297
+ return hit[1], hit[2]
298
+ self.walks += 1
299
+ harness, harness_pid = harness_walk(proc_root, pid, kids)
300
+ comm = _comm(proc_root, harness_pid) if harness_pid else None
301
+ if harness in AGENT_HARNESSES and harness_pid and comm is not None:
302
+ self.entries[key] = (kids, harness, harness_pid, comm)
303
+ else:
304
+ self.entries.pop(key, None)
305
+ return harness, harness_pid
306
+
307
+ def prune(self, keep):
308
+ for key in set(self.entries) - set(keep):
309
+ del self.entries[key]
310
+
311
+
114
312
  def _harness_ended(pane_row):
115
313
  """True only when the row says the pane's root process has NO child (`pane_has_child is False`) and the
116
314
  foreground is a bare shell: the harness exited into that shell, whatever title it left behind. A live
@@ -138,8 +336,15 @@ def _same_process(proc_root, pid, proc_start):
138
336
  return False
139
337
 
140
338
 
141
- def parse_codex_title(title):
142
- """A Codex pane title -> {'harness','status','name'}, or None for a shell / non-agent title."""
339
+ def parse_codex_title(title, harness=None):
340
+ """A Codex pane title -> {'harness','status','name'}, or None for a shell / non-agent title. `harness` is the pane's
341
+ OBSERVED harness (`harness_walk`): a pane running another harness (claude, kimi, grok, grok-cli) is never Codex's.
342
+ On a Codex pane any title is Codex's (a task line: idle); on a pane running something else (`unknown`) only the two
343
+ shapes nothing else draws — `[ ! ] Action Required` and a braille spinner — are; on a bare shell (`shell`, a title
344
+ tmux kept after Codex exited) and where the harness could not be observed (None) also Codex's idle `<task> | <dir>`.
345
+ The pre-W12 catch-all, "any title of two or more words is Codex idle", is gone (DEF-1970)."""
346
+ if harness not in (None, 'codex', 'unknown', 'shell'):
347
+ return None
143
348
  t = (title or '').strip()
144
349
  head = t.split('|')[0]
145
350
  if t in SHELL_TITLES or (': ' in head and head.startswith(('coder@', 'root@'))):
@@ -151,7 +356,7 @@ def parse_codex_title(title):
151
356
  return {'harness': 'codex', 'status': 'working', 'name': t[1:].split('|')[0].strip()}
152
357
  if t.startswith('✳'):
153
358
  return None # Claude's static title: presence only; the session file carries the state
154
- if '|' in t or len(t.split()) >= 2:
359
+ if harness == 'codex' or (harness != 'unknown' and '|' in t):
155
360
  return {'harness': 'codex', 'status': 'idle', 'name': head.strip()}
156
361
  return None
157
362
 
@@ -229,30 +434,417 @@ def read_sidecar_dict(d, now, max_age, proc_root=None):
229
434
  'pid': pid, 'source': 'hook', 'event': str(d.get('event') or '')}
230
435
 
231
436
 
232
- def merge(pane_row, claude_by_pane, sidecar):
233
- """Precedence: hook sidecar (event-driven, both harnesses) > Claude session file > Codex title > bell.
437
+ def pane_key(pane_row):
438
+ """`<session>:@<win>.%<pane>`, the key Claude's session files name a pane by and every per-pane index here uses."""
439
+ return f"{pane_row.get('session_name')}:{pane_row.get('window_id')}.{pane_row.get('pane_id')}"
440
+
441
+
442
+ def _open_at(dir_fd, parts):
443
+ """fd of <dir_fd>/<parts…>, every component a REAL directory opened with O_NOFOLLOW (a symlink is OSError, never
444
+ followed: through /proc/<pid>/root an absolute link would resolve against the HOST's root). `dir_fd` stays open;
445
+ the caller closes the returned fd. The `lanes_lib.open_real_dir` rule, kept here so agent_state imports nothing of
446
+ Lanes."""
447
+ fd = None
448
+ try:
449
+ for part in parts:
450
+ if not part or '/' in part or part in ('.', '..'):
451
+ raise OSError(f'not one directory entry: {part!r}')
452
+ child = os.open(part, _DIR_FLAGS | os.O_NOFOLLOW, dir_fd=dir_fd if fd is None else fd)
453
+ if fd is not None:
454
+ os.close(fd)
455
+ fd = child
456
+ except BaseException:
457
+ if fd is not None:
458
+ os.close(fd)
459
+ raise
460
+ return fd
461
+
462
+
463
+ def _mtime_at(dir_fd, name):
464
+ """The mtime of `name` in `dir_fd` when it is a regular file (not followed), else None."""
465
+ try:
466
+ info = os.stat(name, dir_fd=dir_fd, follow_symlinks=False)
467
+ except OSError:
468
+ return None
469
+ return info.st_mtime if stat.S_ISREG(info.st_mode) else None
470
+
471
+
472
+ def _read_at(dir_fd, name, limit, tail=False):
473
+ """(bytes, whole) of `name` in `dir_fd` when it is a regular file, never a link (O_NOFOLLOW) and never blocking on a
474
+ FIFO (O_NONBLOCK), else None. Whole file: refused (None) above `limit` bytes. `tail`: its last `limit` bytes, read
475
+ from one byte earlier so the caller can tell a cut first line from a whole one (`wire_events`); `whole` is True
476
+ when that is the entire file."""
477
+ try:
478
+ fd = os.open(name, _FILE_FLAGS, dir_fd=dir_fd)
479
+ except OSError:
480
+ return None
481
+ try:
482
+ info = os.fstat(fd)
483
+ if not stat.S_ISREG(info.st_mode) or (info.st_size > limit and not tail):
484
+ return None
485
+ start = max(0, info.st_size - limit - 1) if tail else 0
486
+ if start:
487
+ os.lseek(fd, start, os.SEEK_SET)
488
+ want = limit + 1
489
+ chunks, size = [], 0
490
+ while size < want:
491
+ chunk = os.read(fd, want - size)
492
+ if not chunk:
493
+ break
494
+ chunks.append(chunk)
495
+ size += len(chunk)
496
+ data = b''.join(chunks)
497
+ if not tail and len(data) > limit:
498
+ return None # grew past the bound between fstat and read
499
+ return data, start == 0
500
+ except OSError:
501
+ return None
502
+ finally:
503
+ os.close(fd)
504
+
505
+
506
+ def wire_events(data, whole):
507
+ """The events (JSON objects, file order) on the WHOLE lines of a wire.jsonl read: a tail that does not start at the
508
+ file's start loses everything up to its first newline (a cut line — or, when the byte before the window was a
509
+ newline, an empty one), and the text after the last newline is a line still being written. A line that is not a
510
+ JSON object is skipped."""
511
+ lines = data.split(b'\n')
512
+ lines = lines[:-1] if whole else lines[1:-1]
513
+ out = []
514
+ for line in lines:
515
+ try:
516
+ event = json.loads(line)
517
+ except ValueError:
518
+ continue
519
+ if isinstance(event, dict):
520
+ out.append(event)
521
+ return out
522
+
523
+
524
+ def _ms(value):
525
+ """Epoch seconds from an epoch-ms field, or None."""
526
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or value <= 0:
527
+ return None
528
+ return int(value // 1000)
529
+
530
+
531
+ def kimi_status(state, events, whole=False):
532
+ """{'status', 'since', 'event'} of one Kimi Code session from its state.json (`state`, a dict) and the events of
533
+ its agents/main/wire.jsonl tail in file order (`wire_events`; `whole` = the tail is the entire file), or None.
534
+
535
+ Spec §3, as MEASURED (module docstring): `agent.turn.started` / `turn.prompt` open a turn -> working (since: the
536
+ turn's start); inside an open turn `context.append_loop_event` (tool.call among them), `llm.request` and
537
+ `turn.steer` keep it working (and are working on their own when the tail starts mid-turn); an
538
+ `interaction.request{kind: approval}` with no `interaction.resolved` of its id -> needs_approval (any other kind —
539
+ `question` — -> needs_input, a person is asked either way); `turn.ended{reason}` completed/cancelled ->
540
+ needs_input, failed -> errored; `agent.turn.ended{outcome}` the same (done -> needs_input) unless `turn.ended`
541
+ already ended that turn, and an end of a turn older than the one open is ignored; a turn start or end drops any
542
+ request left unanswered (a cancelled turn leaves its question so). With no such event in the tail: a whole file
543
+ without a turn is a session nobody has asked anything (idle); else state.json decides — `lastTurnReason` as
544
+ `turn.ended`'s reason, none (Kimi drops it when a turn starts) with a `lastPrompt` -> working, none without -> idle.
545
+ `since` is epoch seconds (the ms `time` floored), 0 when the event carried none."""
546
+ status = since = event = None
547
+ pending = {}
548
+ open_turn = None
549
+ ended = set()
550
+ turns = False
551
+ for item in events:
552
+ kind = item.get('type')
553
+ at = _ms(item.get('time'))
554
+ turn = item.get('turnId')
555
+ turn = None if turn is None or isinstance(turn, (dict, list)) else str(turn)
556
+ if kind in ('agent.turn.started', 'turn.prompt'):
557
+ turns = True
558
+ if status != 'working' or (turn is not None and open_turn is not None and turn != open_turn):
559
+ since, event = at, kind
560
+ status = 'working'
561
+ open_turn = turn if turn is not None else open_turn
562
+ pending.clear()
563
+ elif kind in ('context.append_loop_event', 'llm.request', 'turn.steer'):
564
+ turns = True
565
+ if status is None:
566
+ status, since, event = 'working', at, kind
567
+ elif kind == 'interaction.request':
568
+ turns = True
569
+ if item.get('id') is not None:
570
+ pending[str(item['id'])] = (str(item.get('kind') or ''), at)
571
+ elif kind == 'interaction.resolved':
572
+ pending.pop(str(item.get('id')), None)
573
+ elif kind in ('turn.ended', 'agent.turn.ended'):
574
+ turns = True
575
+ if turn is not None and ((open_turn is not None and turn != open_turn)
576
+ or (kind == 'agent.turn.ended' and turn in ended)):
577
+ continue
578
+ if kind == 'turn.ended':
579
+ word = KIMI_ENDED.get(item.get('reason'), 'needs_input')
580
+ if turn is not None:
581
+ ended.add(turn)
582
+ else:
583
+ word = 'errored' if item.get('outcome') in ('failed', 'error', 'errored') else 'needs_input'
584
+ status, since, event = word, at, kind
585
+ open_turn = None
586
+ pending.clear()
587
+ if pending:
588
+ approvals = [at for asked, at in pending.values() if asked == 'approval']
589
+ asked = approvals or [at for _kind, at in pending.values()]
590
+ stamps = [at for at in asked if at]
591
+ return {'status': 'needs_approval' if approvals else 'needs_input', 'since': min(stamps) if stamps else 0,
592
+ 'event': 'interaction.request'}
593
+ if status:
594
+ return {'status': status, 'since': since or 0, 'event': event}
595
+ updated = _ms(state.get('updatedAt')) or 0
596
+ reason = state.get('lastTurnReason')
597
+ if whole and not turns:
598
+ word = 'idle'
599
+ elif isinstance(reason, str) and reason:
600
+ word = KIMI_ENDED.get(reason, 'needs_input')
601
+ else:
602
+ word = 'working' if state.get('lastPrompt') else 'idle'
603
+ return {'status': word, 'since': updated, 'event': 'state.json'}
604
+
605
+
606
+ def _kimi_session(workdir_fd, name, cwds, now):
607
+ """(activity, cwd, verdict without pid) of one `session_<uuid>` directory whose state.json `cwd` is in `cwds`, or
608
+ None: not ours, unreadable, or stale — neither file written within KIMI_STALE (two stats, nothing read) or its
609
+ newest content time (state.json `updatedAt`, the last event's `time`) older than that."""
610
+ try:
611
+ session_fd = os.open(name, _DIR_FLAGS | os.O_NOFOLLOW, dir_fd=workdir_fd)
612
+ except OSError:
613
+ return None
614
+ main_fd = None
615
+ try:
616
+ try:
617
+ main_fd = _open_at(session_fd, ('agents', 'main'))
618
+ except OSError:
619
+ main_fd = None
620
+ stamps = [m for m in (_mtime_at(session_fd, 'state.json'),
621
+ _mtime_at(main_fd, 'wire.jsonl') if main_fd is not None else None) if m]
622
+ if not stamps or now - max(stamps) > KIMI_STALE:
623
+ return None
624
+ got = _read_at(session_fd, 'state.json', KIMI_STATE_MAX)
625
+ if not got:
626
+ return None
627
+ try:
628
+ state = json.loads(got[0])
629
+ except ValueError:
630
+ return None
631
+ if not isinstance(state, dict) or state.get('archived') is True:
632
+ return None
633
+ cwd = state.get('cwd')
634
+ if not isinstance(cwd, str) or cwd not in cwds:
635
+ return None
636
+ events, whole = [], False
637
+ tail = _read_at(main_fd, 'wire.jsonl', KIMI_TAIL, tail=True) if main_fd is not None else None
638
+ if tail:
639
+ events, whole = wire_events(*tail), tail[1]
640
+ times = [t for t in [_ms(state.get('updatedAt'))] + [_ms(e.get('time')) for e in events[-1:]] if t]
641
+ activity = max(times) if times else max(stamps)
642
+ if now - activity > KIMI_STALE:
643
+ return None
644
+ found = kimi_status(state, events, whole)
645
+ sid = str(state.get('id') or name)
646
+ title = state.get('title')
647
+ verdict = {'harness': 'kimi', 'status': found['status'],
648
+ 'name': title[:KIMI_NAME_MAX] if isinstance(title, str) else '',
649
+ 'session_id': sid[len('session_'):] if sid.startswith('session_') else sid,
650
+ 'since': found['since'], 'source': 'kimi-session-file', 'event': found['event']}
651
+ return activity, cwd, verdict
652
+ finally:
653
+ if main_fd is not None:
654
+ os.close(main_fd)
655
+ os.close(session_fd)
656
+
657
+
658
+ def kimi_sessions(root, panes, now=None, home=('home', 'coder')):
659
+ """{pane key: agent verdict} for the panes running Kimi Code. `root` is the container's root as the host sees it
660
+ (`/proc/<init pid>/root`), `panes` {pane key: the pane's kimi pid} (statusd: the panes whose `harness_walk` found
661
+ `kimi`). Spec §3: a pane's kimi pid is joined to the session whose state.json `cwd` equals /proc/<pid>/cwd (read in
662
+ the container's /proc), the newest by activity (`_kimi_session`) when several are fresh; two kimi panes in one cwd
663
+ are AMBIGUOUS — neither is claimed, both fall back to the hooks, the title and the bell (DEF-1971). Every directory
664
+ from `home` down to `agents/main` is opened without following a link, every file read is bounded (state.json at
665
+ most KIMI_STATE_MAX bytes, wire.jsonl's last KIMI_TAIL), at most KIMI_MAX_WORKDIRS × KIMI_MAX_SESSIONS directories
666
+ are looked at, and a stale session (> KIMI_STALE) makes no claim. Verdict: {harness: kimi, status, name (state
667
+ title, cut), session_id (the session uuid), pid, since, source: kimi-session-file, event (the wire event that
668
+ decided, or `state.json`)}."""
669
+ now = time.time() if now is None else now
670
+ cwd_of = {}
671
+ for key, pid in (panes or {}).items():
672
+ try:
673
+ cwd_of[key] = os.readlink(Path(root, 'proc', str(int(pid)), 'cwd'))
674
+ except (OSError, ValueError, TypeError):
675
+ continue
676
+ by_cwd = {}
677
+ for key, cwd in cwd_of.items():
678
+ by_cwd.setdefault(cwd, []).append(key)
679
+ wanted = {cwd: keys[0] for cwd, keys in by_cwd.items() if len(keys) == 1}
680
+ if not wanted:
681
+ return {}
682
+ try:
683
+ root_fd = os.open(str(root), _DIR_FLAGS)
684
+ except OSError:
685
+ return {}
686
+ try:
687
+ top = _open_at(root_fd, tuple(home) + KIMI_SESSIONS)
688
+ except OSError:
689
+ return {}
690
+ finally:
691
+ os.close(root_fd)
692
+ best = {}
693
+ try:
694
+ budget = KIMI_MAX_SESSIONS
695
+ try:
696
+ workdirs = sorted(n for n in os.listdir(top) if n.startswith('wd_'))[:KIMI_MAX_WORKDIRS]
697
+ except OSError:
698
+ workdirs = []
699
+ for workdir in workdirs:
700
+ try:
701
+ workdir_fd = os.open(workdir, _DIR_FLAGS | os.O_NOFOLLOW, dir_fd=top)
702
+ except OSError:
703
+ continue
704
+ try:
705
+ try:
706
+ names = sorted(n for n in os.listdir(workdir_fd) if n.startswith('session_'))
707
+ except OSError:
708
+ names = []
709
+ for name in names:
710
+ if budget <= 0:
711
+ break
712
+ budget -= 1
713
+ found = _kimi_session(workdir_fd, name, wanted, now)
714
+ if found and (found[1] not in best or found[0] > best[found[1]][0]):
715
+ best[found[1]] = found
716
+ finally:
717
+ os.close(workdir_fd)
718
+ finally:
719
+ os.close(top)
720
+ out = {}
721
+ for cwd, (_activity, _cwd, verdict) in best.items():
722
+ key = wanted[cwd]
723
+ out[key] = dict(verdict, pid=int(panes[key]))
724
+ return out
725
+
726
+
727
+ def _harness_grok():
728
+ """libexec/harness_grok.py (W12 Track B) when it ships, else None: LOOKED UP, not assumed, and imported only at the
729
+ call, so it may import this module."""
730
+ module = sys.modules.get('harness_grok')
731
+ if module is None:
732
+ if importlib.util.find_spec('harness_grok') is None:
733
+ return None
734
+ import harness_grok as module
735
+ return module
736
+
737
+
738
+ def grok_sessions(root, panes, grok_home=GROK_HOME, *, home=CONTAINER_HOME, titles=None):
739
+ """The Grok Build observer: `harness_grok.grok_sessions(root, panes, grok_home, home=home, titles=titles)` ->
740
+ {pane key: verdict}, or {} when the module does not ship. ONE contract, Track B's: `root` the container's root as the
741
+ host sees it, `panes` {pane key: the pane's tmux row} (statusd passes the rows it walked, so the observer has the
742
+ title, the bell and the observed `harness`; a bare pane pid works too), `grok_home` relative to `home` unless
743
+ absolute, `titles` the caller's memory between calls (statusd.GROK_TITLES: the blinking `⚠ Action Required`
744
+ prefix). An error inside the module is the caller's to log — statusd does. Kept: verdicts whose status is a
745
+ vocabulary word, and the observer's GROK_UNKNOWN (leader mode: a Grok Build it sees but cannot join to a session),
746
+ which `merge` publishes as no agent{} rather than a guess."""
747
+ if not panes:
748
+ return {}
749
+ module = _harness_grok()
750
+ if module is None:
751
+ return {}
752
+ got = module.grok_sessions(root, panes, grok_home, home=home, titles=titles)
753
+ if not isinstance(got, dict):
754
+ return {}
755
+ return {key: dict(verdict) for key, verdict in got.items()
756
+ if isinstance(verdict, dict) and (verdict.get('status') in STATUSES or verdict.get('status') == GROK_UNKNOWN)}
757
+
758
+
759
+ def grok_title(pane_row, bell, since):
760
+ """merge's Grok title rung for a pane observed running Grok Build that no observer claimed:
761
+ `harness_grok.parse_grok_title` of its title (`⚠ Action Required …` needs_approval, a braille spinner working,
762
+ `grok` / `<name> - grok` idle — done with the bell), shaped as the observer's own title verdict (`source:
763
+ grok-title`), or None (no module, not a Grok title). Without the observer's title memory a blinking prefix is read
764
+ as it is this cycle."""
765
+ module = _harness_grok()
766
+ parse = getattr(module, 'parse_grok_title', None) if module is not None else None
767
+ parsed = parse(pane_row.get('pane_title')) if parse is not None else None
768
+ if not isinstance(parsed, dict) or parsed.get('status') not in STATUSES:
769
+ return None
770
+ try:
771
+ pid = int(pane_row.get('harness_pid') or 0)
772
+ except (TypeError, ValueError):
773
+ pid = 0
774
+ status = 'done' if parsed['status'] == 'idle' and bell else parsed['status']
775
+ return {'harness': 'grok', 'status': status, 'name': str(parsed.get('name') or ''), 'session_id': '',
776
+ 'pid': max(pid, 0), 'since': since, 'source': 'grok-title'}
777
+
778
+
779
+ def merge(pane_row, claude_by_pane, sidecar, kimi_by_pane=None, grok_by_pane=None):
780
+ """Precedence: hook sidecar (event-driven, every harness) > Claude session file > Kimi session file > Grok observer
781
+ (`grok_sessions`: its session file, else the title with its blink memory) > the Grok title (`grok_title`, a pane
782
+ observed as `grok` nothing above claimed) > Codex title > the observed harness itself > the bell.
234
783
  Any Codex title — idle, spinner or Action Required, bell or not — over a bare-shell pane whose shell has no
235
784
  child process (`pane_has_child is False`, statusd's row key) publishes `shell`: the harness ended and tmux kept
236
785
  its last title, so no ghost idle/done/working/needs_approval. Unknown liveness leaves the title in charge.
237
- Returns None when nothing says an agent is here — the sidebar then draws a plain shell."""
786
+
787
+ W12: `pane_row['harness']` is the pane's OBSERVED harness (statusd, `HarnessCache`; absent = not observed). It gates
788
+ the Codex title (`parse_codex_title`) and, when it names a harness, is the verdict's `harness` whatever the signal
789
+ said (a Claude-installed hook firing inside another harness says `claude`). The Kimi observer's `needs_input` with
790
+ the window bell set is `done` — a turn ended and the harness rang — except a question still asked of the person
791
+ (`event: interaction.request`); the Grok observer's verdicts carry its own bell rule (only a completed turn or an
792
+ idle title is `done`) and are taken as they come. The Grok observer's GROK_UNKNOWN (leader mode: its Grok Build runs a session it
793
+ cannot join to the pane) publishes no agent{}: nothing below may guess one. Last rung: a pane whose observed harness
794
+ nothing above claimed is that harness, `idle`, `done` with the bell (`source: process`, `pid` the harness pid statusd
795
+ found). Returns None when nothing says an agent is here — the sidebar then draws a plain shell."""
796
+ observed = pane_row.get('harness')
797
+ verdict = _verdict(pane_row, claude_by_pane, sidecar, kimi_by_pane or {}, grok_by_pane or {}, observed)
798
+ if verdict is not None and observed in AGENT_HARNESSES:
799
+ verdict['harness'] = observed
800
+ return verdict
801
+
802
+
803
+ def _verdict(pane_row, claude_by_pane, sidecar, kimi_by_pane, grok_by_pane, observed):
238
804
  if sidecar and sidecar.get('status') in STATUSES:
239
805
  return dict(sidecar, source=sidecar.get('source') or 'hook')
240
- key = f"{pane_row.get('session_name')}:{pane_row.get('window_id')}.{pane_row.get('pane_id')}"
806
+ key = pane_key(pane_row)
241
807
  if key in claude_by_pane:
242
808
  return dict(claude_by_pane[key])
243
- title = parse_codex_title(pane_row.get('pane_title'))
244
809
  bell = pane_row.get('window_bell_flag') == '1'
810
+ # The Kimi observer leaves the bell to merge; the Grok observer applies it itself (harness_grok: only a completed
811
+ # turn or an idle title is `done` — a cancelled or interrupted turn stays needs_input, spec §4).
812
+ for index, rings in ((kimi_by_pane, True), (grok_by_pane, False)):
813
+ found = index.get(key)
814
+ if isinstance(found, dict) and found.get('status') == GROK_UNKNOWN:
815
+ return None # seen, not joinable (Grok's leader mode): no guess below
816
+ if isinstance(found, dict) and found.get('status') in STATUSES:
817
+ found = dict(found)
818
+ if rings and found['status'] == 'needs_input' and bell and found.get('event') != 'interaction.request':
819
+ found['status'] = 'done'
820
+ return found
821
+ try:
822
+ activity = int(pane_row.get('window_activity') or 0)
823
+ except ValueError:
824
+ activity = 0
825
+ if observed == 'grok':
826
+ found = grok_title(pane_row, bell, activity)
827
+ if found:
828
+ return found
829
+ title = parse_codex_title(pane_row.get('pane_title'), observed)
245
830
  if title:
246
831
  if _harness_ended(pane_row):
247
832
  title['status'] = 'shell' # stale title on a childless shell: the harness ended, no ghost of any kind
248
833
  elif title['status'] == 'idle' and bell:
249
834
  title['status'] = 'done'
250
835
  title['source'] = 'title'
251
- try:
252
- title['since'] = int(pane_row.get('window_activity') or 0)
253
- except ValueError:
254
- title['since'] = 0
836
+ title['since'] = activity
255
837
  return title
838
+ if observed in AGENT_HARNESSES:
839
+ found = {'harness': observed, 'status': 'done' if bell else 'idle', 'name': '', 'source': 'process',
840
+ 'since': activity}
841
+ try:
842
+ harness_pid = int(pane_row.get('harness_pid') or 0)
843
+ except (TypeError, ValueError):
844
+ harness_pid = 0
845
+ if harness_pid > 0:
846
+ found['pid'] = harness_pid
847
+ return found
256
848
  return None
257
849
 
258
850