summaryrefslogtreecommitdiff
path: root/9harness/src/active.zig
diff options
context:
space:
mode:
Diffstat (limited to '9harness/src/active.zig')
-rw-r--r--9harness/src/active.zig1006
1 files changed, 0 insertions, 1006 deletions
diff --git a/9harness/src/active.zig b/9harness/src/active.zig
deleted file mode 100644
index fb55cf6..0000000
--- a/9harness/src/active.zig
+++ /dev/null
@@ -1,1006 +0,0 @@
-//! The derived view behind `/active`: one record per live agent,
-//! normalized across harnesses.
-//!
-//! v1 of the tree mirrors files. This module answers a different
-//! question — *what is running right now* — by reading `/proc` and then
-//! asking each harness in its own terms which stored session the
-//! process is writing. That ladder (`fd` -> `dir` -> `db`, with the
-//! route named so a wrong guess is visible) is `~/.local/bin/zmxify`'s,
-//! ported here so the knowledge lives somewhere tested.
-//!
-//! Everything here composes paths from the proc root, a pinned harness
-//! root and a pid. No client byte reaches the filesystem, and the proc
-//! root is a parameter (`--proc`) so the scan, the liveness guard and
-//! the exclusions are testable against a fixture tree.
-const std = @import("std");
-const Io = std.Io;
-
-// ---- comptime bounds ---------------------------------------------------------
-
-/// Live agents held at once. A machine running more than this many
-/// interactive harnesses at the same time is not the case to optimize.
-pub const max_live: usize = 32;
-/// Subagents listed under one agent.
-pub const max_agents: usize = 32;
-/// Longest directory a process may run in.
-pub const cwd_capacity: usize = 512;
-/// Longest absolute path this module composes (proc root, harness root).
-pub const path_capacity: usize = 1024;
-/// Longest session id, title, name or model answered.
-pub const text_capacity: usize = 160;
-/// `<harness>-<pid>`.
-pub const name_capacity: usize = 32;
-/// Bytes of a `/proc` file or a transcript head read at once.
-pub const probe_capacity: usize = 8192;
-
-/// The harnesses this view knows how to recognize.
-/// The order is `tree.Root`'s, and `tree.zig` asserts that at comptime:
-/// the two enums index the same pinned roots, so a mismatch would hand
-/// one harness another's base path.
-pub const Kind = enum(u8) {
- claude,
- codex,
- omp,
- hermes,
- dsh,
-
- pub fn text(k: Kind) []const u8 {
- return @tagName(k);
- }
-};
-
-/// How a session was resolved — reported, so a wrong guess is visible
-/// rather than silent. `none` means the process is listed with what
-/// `/proc` knows and nothing more, and cannot be moved: hermes is the
-/// only harness that always lands here.
-pub const Via = enum(u8) {
- none,
- registry,
- fd,
- dir,
-
- pub fn text(v: Via) []const u8 {
- return @tagName(v);
- }
-};
-
-/// A fixed-capacity byte buffer. Everything this module remembers is
-/// bounded at comptime, like the rest of the daemon.
-fn Buf(comptime n: usize) type {
- return struct {
- bytes: [n]u8 = undefined,
- len: u16 = 0,
-
- const Self = @This();
-
- pub fn set(b: *Self, v: []const u8) void {
- const take = @min(v.len, n);
- @memcpy(b.bytes[0..take], v[0..take]);
- b.len = @intCast(take);
- }
-
- pub fn slice(b: *const Self) []const u8 {
- return b.bytes[0..b.len];
- }
-
- pub fn clear(b: *Self) void {
- b.len = 0;
- }
- };
-}
-
-/// One live agent.
-pub const Live = struct {
- kind: Kind = .claude,
- pid: u32 = 0,
- ppid: u32 = 0,
- /// `/proc/<pid>/stat` field 22. Two processes can share a pid over
- /// time; they cannot share a pid and a start time, so this is what
- /// makes a remembered slot safe to trust later.
- starttime: u64 = 0,
- /// Unix seconds, from the process's start time against boot time.
- started: i64 = 0,
- via: Via = .none,
- name: Buf(name_capacity) = .{},
- cwd: Buf(cwd_capacity) = .{},
- session: Buf(text_capacity) = .{},
- /// Absolute path of the session transcript, empty when unresolved.
- transcript: Buf(path_capacity) = .{},
- /// Directory holding the subagent transcripts, empty when the
- /// harness has no such layout.
- agent_dir: Buf(path_capacity) = .{},
-};
-
-// ---- pure helpers: the part that carries zmxify's knowledge -------------------
-
-/// A command line word that marks a daemon, a server or a helper — a
-/// process wearing the harness's name that is not an interactive
-/// session and must never be listed or acted on.
-const not_session = [_][]const u8{
- "__omp_worker_daemon_broker",
- "acp",
- "mcp",
- "mcp-server",
- "app-server",
- "exec-server",
- "gateway",
- "dashboard",
- "serve",
- "daemon",
- "ps",
- "render",
- "export",
-};
-
-/// The last path component of `p`.
-pub fn basename(p: []const u8) []const u8 {
- if (std.mem.lastIndexOfScalar(u8, p, '/')) |i| return p[i + 1 ..];
- return p;
-}
-
-/// Iterates a `/proc/<pid>/cmdline`: NUL-separated argv, usually with a
-/// trailing NUL. An empty cmdline (a kernel thread) yields nothing.
-pub const Argv = struct {
- rest: []const u8,
-
- pub fn init(cmdline: []const u8) Argv {
- return .{ .rest = cmdline };
- }
-
- pub fn next(a: *Argv) ?[]const u8 {
- while (a.rest.len > 0 and a.rest[0] == 0) a.rest = a.rest[1..];
- if (a.rest.len == 0) return null;
- const end = std.mem.indexOfScalar(u8, a.rest, 0) orelse a.rest.len;
- const word = a.rest[0..end];
- a.rest = a.rest[@min(end + 1, a.rest.len)..];
- return word;
- }
-};
-
-/// The harness a command line runs, if any. `argv[0]`'s basename names
-/// it, except for hermes, which runs as a python module; any word from
-/// `not_session` disqualifies the whole command line.
-pub fn harnessOf(cmdline: []const u8) ?Kind {
- var it: Argv = .init(cmdline);
- const argv0 = it.next() orelse return null;
- const exe_name = basename(argv0);
-
- var found: ?Kind = null;
- if (std.mem.eql(u8, exe_name, "claude")) {
- found = .claude;
- } else if (std.mem.eql(u8, exe_name, "omp")) {
- found = .omp;
- } else if (std.mem.eql(u8, exe_name, "codex")) {
- found = .codex;
- } else if (std.mem.eql(u8, exe_name, "hermes")) {
- found = .hermes;
- } else if (std.mem.eql(u8, exe_name, "dsh")) {
- found = .dsh;
- } else if (std.mem.startsWith(u8, exe_name, "python") or std.mem.startsWith(u8, exe_name, "pypy")) {
- var py: Argv = .init(cmdline);
- while (py.next()) |w| {
- if (std.mem.endsWith(u8, w, "hermes") or std.mem.endsWith(u8, w, "hermes_cli.main")) {
- found = .hermes;
- break;
- }
- }
- }
- if (found == null) return null;
-
- var words: Argv = .init(cmdline);
- while (words.next()) |w| {
- for (not_session) |bad| if (std.mem.eql(u8, w, bad)) return null;
- }
- return found;
-}
-
-/// Field `n` (1-based) of a `/proc/<pid>/stat` line. Field 2 is the
-/// executable name in parentheses and may itself hold spaces and
-/// parentheses, so the fields are counted from the *last* `)`, never by
-/// splitting the whole line. Only fields from 3 on are reachable, which
-/// is all any caller wants.
-pub fn statField(stat: []const u8, n: usize) ?u64 {
- if (n < 3) return null;
- const close = std.mem.lastIndexOfScalar(u8, stat, ')') orelse return null;
- var rest = stat[close + 1 ..];
- var field: usize = 3; // state, the first field after the name
- while (true) {
- while (rest.len > 0 and rest[0] == ' ') rest = rest[1..];
- if (rest.len == 0) return null;
- const end = std.mem.indexOfScalar(u8, rest, ' ') orelse rest.len;
- if (field == n) return std.fmt.parseInt(u64, rest[0..end], 10) catch null;
- rest = rest[end..];
- field += 1;
- }
-}
-
-/// The process's start time in clock ticks since boot. Two processes can
-/// share a pid over time; they cannot share a pid and a start time, so
-/// this is what makes a remembered slot safe to trust later.
-pub fn startTimeOf(stat: []const u8) ?u64 {
- return statField(stat, 22);
-}
-
-/// The parent pid.
-pub fn parentOf(stat: []const u8) ?u32 {
- const v = statField(stat, 4) orelse return null;
- return std.math.cast(u32, v);
-}
-
-/// The value of `"key"` in a small flat JSON object: a quoted string
-/// without its quotes, or a bare token (a number, `true`, `null`).
-/// Nested objects are not searched, which is what the callers want —
-/// these are the harnesses' own one-level session records.
-pub fn jsonField(text: []const u8, key: []const u8) ?[]const u8 {
- var quoted_buf: [64]u8 = undefined;
- if (key.len + 2 > quoted_buf.len) return null;
- quoted_buf[0] = '"';
- @memcpy(quoted_buf[1 .. 1 + key.len], key);
- quoted_buf[1 + key.len] = '"';
- const quoted = quoted_buf[0 .. key.len + 2];
-
- var from: usize = 0;
- while (std.mem.indexOfPos(u8, text, from, quoted)) |at| {
- from = at + quoted.len;
- var rest = text[from..];
- while (rest.len > 0 and (rest[0] == ' ' or rest[0] == '\t')) rest = rest[1..];
- if (rest.len == 0 or rest[0] != ':') continue;
- rest = rest[1..];
- while (rest.len > 0 and (rest[0] == ' ' or rest[0] == '\t')) rest = rest[1..];
- if (rest.len == 0) return null;
- if (rest[0] == '"') {
- const end = std.mem.indexOfScalar(u8, rest[1..], '"') orelse return null;
- return rest[1 .. 1 + end];
- }
- if (rest[0] == '{' or rest[0] == '[') continue; // not a leaf; keep looking
- const end = std.mem.indexOfAny(u8, rest, ",}\n\r \t") orelse rest.len;
- if (end == 0) return null;
- return rest[0..end];
- }
- return null;
-}
-
-/// The session id in a transcript's file name. omp writes
-/// `<timestamp>_<uuid>.jsonl`, so the id is what follows the last `_`.
-/// codex writes `rollout-<timestamp>-<uuid>.jsonl`, where the
-/// timestamp holds dashes too, so the id is the last 36 bytes rather
-/// than anything found by splitting. Claude Code writes
-/// `<uuid>.jsonl`, all of it the id.
-pub fn sessionIdOf(kind: Kind, file_name: []const u8) []const u8 {
- var stem = file_name;
- if (std.mem.endsWith(u8, stem, ".jsonl")) stem = stem[0 .. stem.len - ".jsonl".len];
- switch (kind) {
- .omp => if (std.mem.lastIndexOfScalar(u8, stem, '_')) |i| return stem[i + 1 ..],
- .codex => {
- const uuid_len = "01a0bcd2-5653-7cc0-aa36-676f6467a72a".len;
- if (stem.len >= uuid_len) return stem[stem.len - uuid_len ..];
- },
- else => {},
- }
- return stem;
-}
-
-/// The store directory a harness derives from a working directory.
-/// omp strips `$HOME` and turns every `/` into `-`; Claude Code turns
-/// every character that is not a letter or a digit into `-`, over the
-/// whole path. Returns the slug written into `out`.
-pub fn slugOf(kind: Kind, cwd: []const u8, home: []const u8, out: []u8) ?[]const u8 {
- var path = cwd;
- if (kind == .omp and home.len > 0 and std.mem.startsWith(u8, path, home)) {
- path = path[home.len..];
- }
- if (path.len > out.len) return null;
- for (path, 0..) |c, i| {
- out[i] = switch (kind) {
- .omp => if (c == '/') '-' else c,
- else => if (std.ascii.isAlphanumeric(c)) c else '-',
- };
- }
- return out[0..path.len];
-}
-
-/// Is `name` a zmx session name? zmx allows only these bytes in a
-/// label, and the move path never composes anything else.
-pub fn legalZmxName(name: []const u8) bool {
- if (name.len == 0 or name.len > 64) return false;
- for (name) |c| {
- if (!std.ascii.isAlphanumeric(c) and c != '.' and c != '_' and c != '-') return false;
- }
- return true;
-}
-
-
-// ---- reading /proc and the harness stores ------------------------------------
-
-/// Where the view reads from. Every path below is composed from these
-/// and a pid; nothing here is ever built from a client's bytes.
-pub const Sources = struct {
- io: Io,
- /// The proc filesystem root — `/proc`, or a fixture tree under test.
- proc: []const u8,
- /// $HOME, for the store-slug spellings.
- home: []const u8,
- /// The pinned harness roots, indexed by `@intFromEnum(Kind)`; an
- /// empty base means that harness is unreachable.
- roots: [5][]const u8 = @splat(""),
- /// The daemon's own pid: it and its ancestors are never listed, so
- /// 9harness can neither show nor act on the tree it lives in. Zero
- /// disables the check (fixtures have no ancestry).
- self_pid: u32 = 0,
- /// Seconds between the epoch and boot, from `/proc/stat`'s `btime`.
- /// Zero when it could not be read; `started` is then zero too.
- btime: i64 = 0,
-};
-
-/// USER_HZ: the unit of `/proc/<pid>/stat`'s time fields. Linux reports
-/// them in hundredths of a second whatever CONFIG_HZ is.
-const user_hz: u64 = 100;
-
-fn readSmall(io: Io, path: []const u8, buf: []u8) ?[]const u8 {
- const out = Io.Dir.readFile(.cwd(), io, path, buf) catch return null;
- return out;
-}
-
-fn linkOf(io: Io, path: []const u8, buf: []u8) ?[]const u8 {
- const n = Io.Dir.readLinkAbsolute(io, path, buf) catch return null;
- return buf[0..n];
-}
-
-fn statOf(io: Io, path: []const u8) ?Io.Dir.Stat {
- return Io.Dir.statFile(.cwd(), io, path, .{ .follow_symlinks = true }) catch null;
-}
-
-fn mtimeOf(io: Io, path: []const u8) i64 {
- const st = statOf(io, path) orelse return 0;
- return st.mtime.toSeconds();
-}
-
-/// `parts` joined with `/` into `buf`, or null when they do not fit.
-fn join(buf: []u8, parts: []const []const u8) ?[]const u8 {
- var n: usize = 0;
- for (parts, 0..) |p, i| {
- if (i > 0) {
- if (n + 1 > buf.len) return null;
- buf[n] = '/';
- n += 1;
- }
- if (n + p.len > buf.len) return null;
- @memcpy(buf[n..][0..p.len], p);
- n += p.len;
- }
- return buf[0..n];
-}
-
-/// `<root>` of a harness, or null when it is not pinned.
-fn rootOf(s: Sources, k: Kind) ?[]const u8 {
- const base = s.roots[@intFromEnum(k)];
- return if (base.len == 0) null else base;
-}
-
-/// The directory a harness keeps its session transcripts under.
-fn sessionRoot(s: Sources, k: Kind, buf: []u8) ?[]const u8 {
- const base = rootOf(s, k) orelse return null;
- return switch (k) {
- // The omp root is ~/.omp and its mirror hangs off agent/.
- .omp => join(buf, &.{ base, "agent", "sessions" }),
- .claude => join(buf, &.{ base, "projects" }),
- .dsh => join(buf, &.{ base, "sessions" }),
- // codex names each rollout after its session, so the file it
- // holds open is the whole answer — no sqlite needed.
- .codex => join(buf, &.{ base, "sessions" }),
- // hermes keeps its sessions only in sqlite, and the only hermes
- // processes that run are the gateway and the dashboard, which
- // are never sessions. Nothing to resolve.
- .hermes => null,
- };
-}
-
-// ---- the table ----------------------------------------------------------------
-
-pub const Slot = struct {
- used: bool = false,
- /// Bumped whenever the slot comes to hold a different process, so a
- /// node id minted for the old one stops resolving.
- gen: u8 = 0,
- seen: bool = false,
- live: Live = .{},
-};
-
-pub const Table = struct {
- slots: [max_live]Slot = @splat(.{}),
-
- /// The slot holding this (pid, starttime), if any.
- fn slotOfPid(t: *Table, pid: u32, starttime: u64) ?*Slot {
- for (&t.slots) |*sl| {
- if (sl.used and sl.live.pid == pid and sl.live.starttime == starttime) return sl;
- }
- return null;
- }
-
- fn freeSlot(t: *Table) ?*Slot {
- for (&t.slots) |*sl| if (!sl.used) return sl;
- return null;
- }
-
- /// The live record at `index`, when its generation still matches.
- pub fn at(t: *Table, index: usize, gen: u8) ?*Live {
- if (index >= t.slots.len) return null;
- const sl = &t.slots[index];
- if (!sl.used or sl.gen != gen) return null;
- return &sl.live;
- }
-
- pub fn indexOf(t: *Table, l: *const Live) usize {
- const base = @intFromPtr(&t.slots[0]);
- return (@intFromPtr(l) - @sizeOf(Slot) + @sizeOf(Slot) - @offsetOf(Slot, "live") - base) / @sizeOf(Slot);
- }
-
- /// Rescans `/proc`. Slots keep their index and generation while the
- /// same process is still there, so a handle opened before the scan
- /// keeps working; a slot that comes to hold a different process
- /// bumps its generation and the old node ids stop resolving.
- pub fn scan(t: *Table, s: Sources) void {
- for (&t.slots) |*sl| sl.seen = false;
-
- var anc: [64]u32 = @splat(0);
- const anc_n = ancestry(s, &anc);
-
- var buf: [path_capacity]u8 = undefined;
- if (Io.Dir.openDirAbsolute(s.io, s.proc, .{ .iterate = true })) |dir| {
- defer Io.Dir.close(dir, s.io);
- var rb: [Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined;
- var it = Io.Dir.Reader.init(dir, &rb);
- while (true) {
- const entry = (it.next(s.io) catch break) orelse break;
- const pid = std.fmt.parseInt(u32, entry.name, 10) catch continue;
- var skip = false;
- for (anc[0..anc_n]) |a| if (a == pid) {
- skip = true;
- };
- if (skip) continue;
- t.consider(s, pid, &buf);
- }
- } else |_| {}
-
- for (&t.slots) |*sl| {
- if (sl.used and !sl.seen) {
- sl.used = false;
- sl.gen +%= 1;
- }
- }
- }
-
- /// Looks at one pid and takes it if it is an agent.
- fn consider(t: *Table, s: Sources, pid: u32, buf: []u8) void {
- var num: [24]u8 = undefined;
- const pid_text = std.fmt.bufPrint(&num, "{d}", .{pid}) catch return;
- var dir_buf: [path_capacity]u8 = undefined;
- const pid_dir = join(&dir_buf, &.{ s.proc, pid_text }) orelse return;
-
- var stat_buf: [probe_capacity]u8 = undefined;
- const stat_path = join(buf, &.{ pid_dir, "stat" }) orelse return;
- const stat = readSmall(s.io, stat_path, &stat_buf) orelse return;
- const starttime = startTimeOf(stat) orelse return;
-
- var cmd_buf: [probe_capacity]u8 = undefined;
- const cmd_path = join(buf, &.{ pid_dir, "cmdline" }) orelse return;
- const cmdline = readSmall(s.io, cmd_path, &cmd_buf) orelse return;
- const kind = harnessOf(cmdline) orelse return;
-
- // Already known and still the same process: keep the slot and
- // everything resolved for it.
- if (t.slotOfPid(pid, starttime)) |sl| {
- sl.seen = true;
- return;
- }
-
- const sl = t.freeSlot() orelse return; // full: the rest simply do not list
- sl.* = .{ .used = true, .gen = sl.gen +% 1, .seen = true, .live = .{} };
- const l = &sl.live;
- l.kind = kind;
- l.pid = pid;
- l.ppid = parentOf(stat) orelse 0;
- l.starttime = starttime;
- l.started = if (s.btime == 0) 0 else s.btime + @as(i64, @intCast(starttime / user_hz));
-
- var name_buf: [name_capacity]u8 = undefined;
- l.name.set(std.fmt.bufPrint(&name_buf, "{s}-{d}", .{ kind.text(), pid }) catch kind.text());
-
- const cwd_path = join(buf, &.{ pid_dir, "cwd" }) orelse return;
- var cwd_buf: [cwd_capacity]u8 = undefined;
- if (linkOf(s.io, cwd_path, &cwd_buf)) |cwd| l.cwd.set(cwd);
-
- resolveSession(l, s, pid_dir);
- }
-};
-
-/// The daemon's own pid and every parent of it, so the tree it lives in
-/// is never listed. Returns how many were written.
-fn ancestry(s: Sources, out: []u32) usize {
- if (s.self_pid == 0) return 0;
- var n: usize = 0;
- var pid = s.self_pid;
- var buf: [path_capacity]u8 = undefined;
- var stat_buf: [probe_capacity]u8 = undefined;
- while (pid > 1 and n < out.len) {
- out[n] = pid;
- n += 1;
- var num: [24]u8 = undefined;
- const pid_text = std.fmt.bufPrint(&num, "{d}", .{pid}) catch break;
- const path = join(&buf, &.{ s.proc, pid_text, "stat" }) orelse break;
- const stat = readSmall(s.io, path, &stat_buf) orelse break;
- const parent = parentOf(stat) orelse break;
- if (parent == 0 or parent == pid) break;
- pid = parent;
- }
- return n;
-}
-
-/// Asks the harness, in its own terms, which stored session this
-/// process is writing. Leaves `via = .none` when it cannot tell: the
-/// view never invents a session it did not find.
-fn resolveSession(l: *Live, s: Sources, pid_dir: []const u8) void {
- switch (l.kind) {
- .claude => resolveClaude(l, s),
- .omp => resolveOmp(l, s, pid_dir),
- .dsh, .codex => resolveByFd(l, s, pid_dir),
- // hermes has no per-session file to find.
- .hermes => {},
- }
-}
-
-/// Claude Code maintains `sessions/<pid>.json` itself, so there is
-/// nothing to guess — only to check. `procStart` must match the
-/// process's start time, or the record belongs to a dead predecessor
-/// that happened to share the pid.
-fn resolveClaude(l: *Live, s: Sources) void {
- const base = rootOf(s, .claude) orelse return;
- var buf: [path_capacity]u8 = undefined;
- var num: [24]u8 = undefined;
- const file = std.fmt.bufPrint(&num, "{d}.json", .{l.pid}) catch return;
- const path = join(&buf, &.{ base, "sessions", file }) orelse return;
- var text_buf: [probe_capacity]u8 = undefined;
- const text = readSmall(s.io, path, &text_buf) orelse return;
-
- const proc_start = jsonField(text, "procStart") orelse return;
- const claimed = std.fmt.parseInt(u64, proc_start, 10) catch return;
- if (claimed != l.starttime) return; // a record left by a reused pid
-
- const sid = jsonField(text, "sessionId") orelse return;
- if (sid.len == 0 or sid.len > text_capacity) return;
- l.session.set(sid);
- l.via = .registry;
-
- var slug_buf: [cwd_capacity]u8 = undefined;
- const slug = slugOf(.claude, l.cwd.slice(), s.home, &slug_buf) orelse return;
- var tr_buf: [path_capacity]u8 = undefined;
- var file_buf: [text_capacity + 8]u8 = undefined;
- const tr_name = std.fmt.bufPrint(&file_buf, "{s}.jsonl", .{sid}) catch return;
- const tr = join(&tr_buf, &.{ base, "projects", slug, tr_name }) orelse return;
- if (statOf(s.io, tr) != null) l.transcript.set(tr);
-}
-
-/// omp: the transcript it holds open, else the store directory named
-/// after its working directory.
-fn resolveOmp(l: *Live, s: Sources, pid_dir: []const u8) void {
- resolveByFd(l, s, pid_dir);
- if (l.via == .none) resolveOmpByDir(l, s);
- if (l.transcript.len == 0) return;
- const tr = l.transcript.slice();
- l.session.set(sessionIdOf(.omp, basename(tr)));
- // The subagents are sibling files in the directory named after the
- // session file, one `<Agent>.jsonl` each.
- if (std.mem.endsWith(u8, tr, ".jsonl")) {
- l.agent_dir.set(tr[0 .. tr.len - ".jsonl".len]);
- }
-}
-
-fn resolveOmpByDir(l: *Live, s: Sources) void {
- var root_buf: [path_capacity]u8 = undefined;
- const root = sessionRoot(s, .omp, &root_buf) orelse return;
- var slug_buf: [cwd_capacity]u8 = undefined;
- const slug = slugOf(.omp, l.cwd.slice(), s.home, &slug_buf) orelse return;
- var dir_buf: [path_capacity]u8 = undefined;
- const dir_path = join(&dir_buf, &.{ root, slug }) orelse return;
- var newest_buf: [path_capacity]u8 = undefined;
- const newest = newestUnder(s.io, dir_path, ".jsonl", &newest_buf) orelse return;
- l.transcript.set(newest);
- l.via = .dir;
-}
-
-/// The newest entry of `dir_path` whose name ends in `suffix`, as a
-/// full path written into `out`.
-fn newestUnder(io: Io, dir_path: []const u8, suffix: []const u8, out: []u8) ?[]const u8 {
- const dir = Io.Dir.openDirAbsolute(io, dir_path, .{ .iterate = true }) catch return null;
- defer Io.Dir.close(dir, io);
- var rb: [Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined;
- var it = Io.Dir.Reader.init(dir, &rb);
- var best: i64 = -1;
- var best_len: usize = 0;
- var probe: [path_capacity]u8 = undefined;
- while (true) {
- const entry = (it.next(io) catch break) orelse break;
- if (!std.mem.endsWith(u8, entry.name, suffix)) continue;
- const path = join(&probe, &.{ dir_path, entry.name }) orelse continue;
- const when = mtimeOf(io, path);
- if (when <= best) continue;
- if (path.len > out.len) continue;
- @memcpy(out[0..path.len], path);
- best = when;
- best_len = path.len;
- }
- return if (best_len == 0) null else out[0..best_len];
-}
-
-/// The route that needs no knowledge of how a harness names its store:
-/// whatever session file the process is holding open. The newest wins,
-/// so a harness that keeps an old transcript open for reading does not
-/// outvote the one it is writing.
-fn resolveByFd(l: *Live, s: Sources, pid_dir: []const u8) void {
- var root_buf: [path_capacity]u8 = undefined;
- const root = sessionRoot(s, l.kind, &root_buf) orelse return;
-
- var fd_buf: [path_capacity]u8 = undefined;
- const fd_dir_path = join(&fd_buf, &.{ pid_dir, "fd" }) orelse return;
- const fd_dir = Io.Dir.openDirAbsolute(s.io, fd_dir_path, .{ .iterate = true }) catch return;
- defer Io.Dir.close(fd_dir, s.io);
- var rb: [Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined;
- var it = Io.Dir.Reader.init(fd_dir, &rb);
-
- var best: i64 = -1;
- var best_path: [path_capacity]u8 = undefined;
- var best_len: usize = 0;
- while (true) {
- const entry = (it.next(s.io) catch break) orelse break;
- var link_path_buf: [path_capacity]u8 = undefined;
- const link_path = join(&link_path_buf, &.{ fd_dir_path, entry.name }) orelse continue;
- var target_buf: [path_capacity]u8 = undefined;
- const target = linkOf(s.io, link_path, &target_buf) orelse continue;
- if (!std.mem.startsWith(u8, target, root)) continue;
- if (target.len <= root.len or target[root.len] != '/') continue;
- if (!sessionFile(l.kind, target[root.len + 1 ..])) continue;
- const when = mtimeOf(s.io, target);
- if (when <= best or target.len > best_path.len) continue;
- @memcpy(best_path[0..target.len], target);
- best = when;
- best_len = target.len;
- }
- if (best_len == 0) return;
- const found = best_path[0..best_len];
- l.transcript.set(found);
- l.via = .fd;
- if (l.kind == .codex) l.session.set(sessionIdOf(.codex, basename(found)));
- if (l.kind == .dsh) {
- // ~/.dsh/sessions/<cwd-slug>/session-<uuid>/session.jsonl.zstd:
- // the id is the directory the transcript sits in.
- const rel = found[root.len + 1 ..];
- var parts = std.mem.splitScalar(u8, rel, '/');
- _ = parts.next();
- if (parts.next()) |id| l.session.set(id);
- }
-}
-
-/// Is this path, relative to the harness's session root, one of its
-/// session transcripts rather than a subagent's or something else?
-fn sessionFile(kind: Kind, rel: []const u8) bool {
- var depth: usize = 0;
- var parts = std.mem.splitScalar(u8, rel, '/');
- while (parts.next()) |_| depth += 1;
- return switch (kind) {
- // <slug>/<file>.jsonl exactly: a third component is a subagent.
- .omp => depth == 2 and std.mem.endsWith(u8, rel, ".jsonl"),
- .claude => depth == 2 and std.mem.endsWith(u8, rel, ".jsonl"),
- .dsh => depth == 3 and std.mem.startsWith(u8, rel, "-"),
- // YYYY/MM/DD/rollout-<when>-<uuid>.jsonl
- .codex => depth == 4 and std.mem.endsWith(u8, rel, ".jsonl") and
- std.mem.startsWith(u8, basename(rel), "rollout-"),
- .hermes => false,
- };
-}
-
-
-// ---- fields derived when they are read ---------------------------------------
-
-/// The value of `key` in a NUL-separated environment block, as
-/// `/proc/<pid>/environ` stores it.
-pub fn envValue(environ: []const u8, key: []const u8) ?[]const u8 {
- var it: Argv = .init(environ);
- while (it.next()) |pair| {
- if (pair.len <= key.len) continue;
- if (!std.mem.startsWith(u8, pair, key)) continue;
- if (pair[key.len] != '=') continue;
- return pair[key.len + 1 ..];
- }
- return null;
-}
-
-/// The title key each harness writes into the head of a transcript.
-fn titleKey(k: Kind) []const u8 {
- return switch (k) {
- .claude => "aiTitle",
- else => "title",
- };
-}
-
-/// A field from the first few records of a transcript, where the
-/// harnesses put the session's title and the model it runs. Only the
-/// head is read: these records are written when the session opens.
-pub fn headField(io: Io, kind: Kind, transcript: []const u8, which: enum { title, model }, out: []u8) ?[]const u8 {
- if (transcript.len == 0) return null;
- var head: [probe_capacity]u8 = undefined;
- const text = readSmall(io, transcript, &head) orelse return null;
- const key = switch (which) {
- .title => titleKey(kind),
- .model => "model",
- };
- const v = jsonField(text, key) orelse return null;
- if (v.len == 0 or v.len > out.len) return null;
- @memcpy(out[0..v.len], v);
- return out[0..v.len];
-}
-
-/// The zmx session a process runs inside, from its environment.
-pub fn zmxOf(s: Sources, pid: u32, out: []u8) ?[]const u8 {
- var num: [24]u8 = undefined;
- const pid_text = std.fmt.bufPrint(&num, "{d}", .{pid}) catch return null;
- var path_buf: [path_capacity]u8 = undefined;
- const path = join(&path_buf, &.{ s.proc, pid_text, "environ" }) orelse return null;
- var env_buf: [probe_capacity]u8 = undefined;
- const env = readSmall(s.io, path, &env_buf) orelse return null;
- const v = envValue(env, "ZMX_SESSION") orelse return null;
- if (v.len == 0 or v.len > out.len) return null;
- @memcpy(out[0..v.len], v);
- return out[0..v.len];
-}
-
-/// A field of Claude Code's own session record, which is the only
-/// harness that publishes a name and a status.
-pub fn registryField(s: Sources, l: *const Live, key: []const u8, out: []u8) ?[]const u8 {
- if (l.kind != .claude) return null;
- const base = rootOf(s, .claude) orelse return null;
- var num: [24]u8 = undefined;
- const file = std.fmt.bufPrint(&num, "{d}.json", .{l.pid}) catch return null;
- var path_buf: [path_capacity]u8 = undefined;
- const path = join(&path_buf, &.{ base, "sessions", file }) orelse return null;
- var text_buf: [probe_capacity]u8 = undefined;
- const text = readSmall(s.io, path, &text_buf) orelse return null;
- // The record must still describe this process, not a reused pid.
- const proc_start = jsonField(text, "procStart") orelse return null;
- const claimed = std.fmt.parseInt(u64, proc_start, 10) catch return null;
- if (claimed != l.starttime) return null;
- const v = jsonField(text, key) orelse return null;
- if (v.len == 0 or v.len > out.len) return null;
- @memcpy(out[0..v.len], v);
- return out[0..v.len];
-}
-
-/// Is this process still the one the slot remembers? A pid alone is not
-/// an identity: the kernel reuses them.
-pub fn stillAlive(s: Sources, l: *const Live) bool {
- var num: [24]u8 = undefined;
- const pid_text = std.fmt.bufPrint(&num, "{d}", .{l.pid}) catch return false;
- var path_buf: [path_capacity]u8 = undefined;
- const path = join(&path_buf, &.{ s.proc, pid_text, "stat" }) orelse return false;
- var stat_buf: [probe_capacity]u8 = undefined;
- const stat = readSmall(s.io, path, &stat_buf) orelse return false;
- const now = startTimeOf(stat) orelse return false;
- return now == l.starttime;
-}
-
-/// Seconds between the epoch and boot, from `/proc/stat`'s `btime`
-/// line. Zero when it cannot be read.
-pub fn bootTime(io: Io, proc: []const u8) i64 {
- var path_buf: [path_capacity]u8 = undefined;
- const path = join(&path_buf, &.{ proc, "stat" }) orelse return 0;
- var buf: [probe_capacity]u8 = undefined;
- const text = readSmall(io, path, &buf) orelse return 0;
- var lines = std.mem.splitScalar(u8, text, '\n');
- while (lines.next()) |line| {
- if (!std.mem.startsWith(u8, line, "btime ")) continue;
- return std.fmt.parseInt(i64, std.mem.trim(u8, line["btime ".len..], " \r"), 10) catch 0;
- }
- return 0;
-}
-
-
-// ---- subagents ----------------------------------------------------------------
-
-/// Longest subagent name answered.
-pub const agent_name_capacity: usize = 64;
-
-/// The subagents of one session: omp writes each as `<Name>.jsonl` in a
-/// directory named after the session file. Sorted, so a listing that
-/// spans several reads keeps its cursor.
-pub const Agents = struct {
- names: [max_agents][agent_name_capacity]u8 = undefined,
- lens: [max_agents]u8 = @splat(0),
- count: usize = 0,
-
- pub fn name(a: *const Agents, i: usize) []const u8 {
- if (i >= a.count) return "";
- return a.names[i][0..a.lens[i]];
- }
-
- pub fn indexOf(a: *const Agents, want: []const u8) ?usize {
- for (0..a.count) |i| if (std.mem.eql(u8, a.name(i), want)) return i;
- return null;
- }
-};
-
-pub fn agentsOf(io: Io, l: *const Live, out: *Agents) void {
- out.count = 0;
- const dir_path = l.agent_dir.slice();
- if (dir_path.len == 0) return;
- const dir = Io.Dir.openDirAbsolute(io, dir_path, .{ .iterate = true }) catch return;
- defer Io.Dir.close(dir, io);
- var rb: [Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined;
- var it = Io.Dir.Reader.init(dir, &rb);
- while (out.count < max_agents) {
- const entry = (it.next(io) catch break) orelse break;
- if (!std.mem.endsWith(u8, entry.name, ".jsonl")) continue;
- const stem = entry.name[0 .. entry.name.len - ".jsonl".len];
- if (stem.len == 0 or stem.len > agent_name_capacity) continue;
- @memcpy(out.names[out.count][0..stem.len], stem);
- out.lens[out.count] = @intCast(stem.len);
- out.count += 1;
- }
- // Insertion sort: the list is at most `max_agents` long and already
- // nearly ordered, and this keeps the struct allocation-free.
- var i: usize = 1;
- while (i < out.count) : (i += 1) {
- var j = i;
- while (j > 0 and std.mem.order(u8, out.name(j), out.name(j - 1)) == .lt) : (j -= 1) {
- std.mem.swap([agent_name_capacity]u8, &out.names[j], &out.names[j - 1]);
- std.mem.swap(u8, &out.lens[j], &out.lens[j - 1]);
- }
- }
-}
-
-/// The transcript path of subagent `i`, written into `out`.
-pub fn agentPath(l: *const Live, a: *const Agents, i: usize, out: []u8) ?[]const u8 {
- if (i >= a.count) return null;
- var name_buf: [agent_name_capacity + 8]u8 = undefined;
- const file = std.fmt.bufPrint(&name_buf, "{s}.jsonl", .{a.name(i)}) catch return null;
- return join(out, &.{ l.agent_dir.slice(), file });
-}
-
-// ---- unit tests ---------------------------------------------------------------
-
-const testing = std.testing;
-
-/// A `/proc/<pid>/cmdline`: NUL-separated argv, written out literally so
-/// the tests read the way the kernel stores it.
-fn cmd(comptime words: []const []const u8) []const u8 {
- comptime var out: []const u8 = "";
- inline for (words) |w| out = out ++ w ++ "\x00";
- return out;
-}
-
-test "active: a command line names its harness, or nothing" {
- try testing.expectEqual(Kind.claude, harnessOf(cmd(&.{ "claude", "--dangerously-skip-permissions" })).?);
- try testing.expectEqual(Kind.omp, harnessOf(cmd(&.{"omp"})).?);
- try testing.expectEqual(Kind.codex, harnessOf(cmd(&.{ "/usr/bin/codex", "resume" })).?);
- try testing.expectEqual(Kind.dsh, harnessOf(cmd(&.{"/home/goblin/.local/bin/dsh"})).?);
- // hermes runs as a python module, named by a later word.
- try testing.expectEqual(Kind.hermes, harnessOf(cmd(&.{ "/venv/bin/python", "-m", "hermes_cli.main" })).?);
- // Not a harness at all.
- try testing.expect(harnessOf(cmd(&.{ "bash", "-c", "claude" })) == null);
- try testing.expect(harnessOf("") == null);
- try testing.expect(harnessOf("\x00\x00") == null);
-}
-
-test "active: a daemon or helper wearing a harness name is never a session" {
- // The live machine's own gateway and dashboard, which must not list.
- try testing.expect(harnessOf(cmd(&.{ "python3", "-m", "hermes_cli.main", "gateway", "run" })) == null);
- try testing.expect(harnessOf(cmd(&.{ "python3", "/x/hermes-dashboard", "dashboard" })) == null);
- try testing.expect(harnessOf(cmd(&.{ "claude", "mcp-server" })) == null);
- try testing.expect(harnessOf(cmd(&.{ "omp", "__omp_worker_daemon_broker" })) == null);
- try testing.expect(harnessOf(cmd(&.{ "codex", "app-server" })) == null);
- // A directory that merely contains the word is still a session: the
- // exclusion matches a whole argument, never a substring.
- try testing.expectEqual(Kind.claude, harnessOf(cmd(&.{ "claude", "--cwd", "/home/goblin/serve-me" })).?);
-}
-
-test "active: starttime is counted from the last ')' in a stat line" {
- // Fields: pid (comm) state ppid ... starttime is field 22.
- var line: [512]u8 = undefined;
- var w = Io.Writer.fixed(&line);
- try w.print("345104 (claude) S 344980", .{});
- for (5..22) |i| try w.print(" {d}", .{i * 10});
- try w.print(" 1625063 rest of the line", .{});
- try testing.expectEqual(@as(u64, 1625063), startTimeOf(w.buffered()).?);
-
- // A process whose name holds spaces and parentheses — the reason
- // the fields are never counted from the start of the line.
- var nasty: [512]u8 = undefined;
- var nw = Io.Writer.fixed(&nasty);
- try nw.print("42 (we i)rd (name) ) R 1", .{});
- for (5..22) |i| try nw.print(" {d}", .{i});
- try nw.print(" 999 x", .{});
- try testing.expectEqual(@as(u64, 999), startTimeOf(nw.buffered()).?);
-
- try testing.expect(startTimeOf("no parens here") == null);
- try testing.expect(startTimeOf("1 (short) S 2 3") == null);
-}
-
-test "active: json fields come out of a harness's own session record" {
- // The shape Claude Code writes to ~/.claude/sessions/<pid>.json.
- const rec =
- \\{
- \\ "pid": 345104,
- \\ "sessionId": "1f9a74f7-b04f-4092-8b98-df11316e03c0",
- \\ "cwd": "/home/goblin",
- \\ "version": "2.1.278",
- \\ "peerFeatures": ["notify_idle", "artifact_yield"],
- \\ "name": "goblin-e5",
- \\ "status": "busy",
- \\ "procStart": "1625063"
- \\}
- ;
- try testing.expectEqualStrings("345104", jsonField(rec, "pid").?);
- try testing.expectEqualStrings("1f9a74f7-b04f-4092-8b98-df11316e03c0", jsonField(rec, "sessionId").?);
- try testing.expectEqualStrings("/home/goblin", jsonField(rec, "cwd").?);
- try testing.expectEqualStrings("goblin-e5", jsonField(rec, "name").?);
- try testing.expectEqualStrings("busy", jsonField(rec, "status").?);
- try testing.expectEqualStrings("1625063", jsonField(rec, "procStart").?);
- // An array or object value is skipped, not half-parsed.
- try testing.expect(jsonField(rec, "peerFeatures") == null);
- try testing.expect(jsonField(rec, "absent") == null);
- // A key that is a prefix of another must not match it.
- try testing.expect(jsonField("{\"sessionIdent\":\"x\"}", "sessionId") == null);
-}
-
-test "active: session ids and store slugs follow each harness's spelling" {
- try testing.expectEqualStrings(
- "01a0c46a-7a06-7676-aef1-add9d9340c83",
- sessionIdOf(.omp, "2026-09-21T14-41-47-526Z_01a0c46a-7a06-7676-aef1-add9d9340c83.jsonl"),
- );
- try testing.expectEqualStrings(
- "42898558-2fa0-41f2-a2c6-1357e5cf6836",
- sessionIdOf(.claude, "42898558-2fa0-41f2-a2c6-1357e5cf6836.jsonl"),
- );
- // codex's timestamp holds dashes too, so the id is the last 36
- // bytes and never what splitting on `-` would give.
- try testing.expectEqualStrings(
- "01a0bcd2-5653-7cc0-aa36-676f6467a72a",
- sessionIdOf(.codex, "rollout-2026-09-20T00-18-16-01a0bcd2-5653-7cc0-aa36-676f6467a72a.jsonl"),
- );
-
- var buf: [256]u8 = undefined;
- // omp strips $HOME and keeps everything but the slashes.
- try testing.expectEqualStrings(
- "-00-projects-0x4200.cafe-cloud9",
- slugOf(.omp, "/home/goblin/00-projects/0x4200.cafe/cloud9", "/home/goblin", &buf).?,
- );
- // Claude Code dashes every byte that is not a letter or a digit.
- try testing.expectEqualStrings(
- "-home-goblin-00-projects-0x4200-cafe-cloud9",
- slugOf(.claude, "/home/goblin/00-projects/0x4200.cafe/cloud9", "/home/goblin", &buf).?,
- );
- // A path longer than the buffer is refused, never truncated into a
- // slug that would name the wrong store.
- var tiny: [4]u8 = undefined;
- try testing.expect(slugOf(.omp, "/home/goblin/somewhere", "/home/goblin", &tiny) == null);
-}
-
-test "active: a zmx session name is checked before it is ever used" {
- try testing.expect(legalZmxName("harness"));
- try testing.expect(legalZmxName("claude-cloud9.2_x"));
- try testing.expect(!legalZmxName(""));
- try testing.expect(!legalZmxName("has space"));
- try testing.expect(!legalZmxName("../escape"));
- try testing.expect(!legalZmxName("semi;colon"));
- try testing.expect(!legalZmxName("nul\x00byte"));
- try testing.expect(!legalZmxName("x" ** 65));
-}
-
-test "active: an environment block answers by key, not by prefix" {
- const env = "PATH=/usr/bin\x00ZMX_SESSION=harness\x00HOME=/home/goblin\x00";
- try testing.expectEqualStrings("harness", envValue(env, "ZMX_SESSION").?);
- try testing.expectEqualStrings("/home/goblin", envValue(env, "HOME").?);
- try testing.expect(envValue(env, "ZMX") == null); // a prefix is not the key
- try testing.expect(envValue(env, "NOPE") == null);
- try testing.expect(envValue("", "HOME") == null);
- // An entry with no value is a value of zero length, not a miss.
- try testing.expectEqualStrings("", envValue("EMPTY=\x00", "EMPTY").?);
-}