summaryrefslogtreecommitdiff
path: root/9harness/src/tree.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-21 16:49:20 -0300
committerGabriel Schneider <[email protected]>2026-09-21 16:49:20 -0300
commitdddd556accea6b6ea7802cd3f622f8b3cf8eb43f (patch)
tree64a1cc34d44f9be6ec266d852fdef6f5b842d004 /9harness/src/tree.zig
parentb4db588dd5b92d647b661c2dc17b40925af92348 (diff)
downloadcloud9-dddd556accea6b6ea7802cd3f622f8b3cf8eb43f.tar.gz
cloud9-dddd556accea6b6ea7802cd3f622f8b3cf8eb43f.zip
9harness: /active, and zmxify as a write to a file
The mirror answers what files exist. /active answers what is running: one directory per live agent, normalized across harnesses, fields as small text files, synthesized per request. /active/claude/345104/{pid,cwd,session,via,name,status,title, model,started,zmx,transcript,agents/} This is /proc's shape, and deliberately: a directory per object named by pid under a directory per harness, rather than a compound `claude-345104` that would make you parse a name to recover a field that is already the directory above it. There is no `updated` file — that is the mtime of `transcript`, which stat already carries. Each harness is asked in its own terms, and the route is reported in `via` so a wrong guess is visible rather than silent. Claude Code publishes sessions/<pid>.json itself, with procStart as a pid-reuse guard, so nothing there is guessed. omp and dsh are found by the transcript they hold open, omp falling back to the store named after its cwd. codex's rollout file carries the session id in its *name*, so its sqlite is never opened. hermes is the one gap and needs none: its sessions live only in sqlite, and the only hermes processes that run are the gateway and the dashboard, which are not sessions. Liveness is /proc/<pid> plus a matching start time: a pid alone is not an identity. The daemon never lists its own ancestry, so it cannot show or act on the tree serving the request. The write path, and why it is a file and not a ctl: writing a zmx session name into an agent's `zmx` moves it there. The file means which zmx session this agent lives in, and writing makes that true. A ctl taking verbs is the ordinary Plan 9 spelling, and an executable script served in the tree is the spelling zmx's own `attach` uses, but a script that shells out to a local binary lies over a remote mount — it would run against a session that is not on the client's machine. A write is served where the authority is. Every refusal comes before anything is destroyed: the name must be zmx's label charset, unused by a live session, and the agent's session must have resolved, because nothing is killed that has nowhere to come back to. The command is fixed per harness and no client byte reaches exec. It is off unless --allow-move: this is the one place the tree is not read-only, and anything that can mount it could otherwise kill an agent. 9harness/zmxify replaces the 307-line rc script. It parses no /proc, opens no fd table and queries no database; it lists /active, offers the rows to fzf and writes the chosen name. It no longer excludes the caller's own session, which the old one had to: that script did the killing itself, so killing its own parent lost the session it was rescuing. The daemon completes the kill and the re-exec whether or not the client is still connected — verified by hanging up immediately after sending the write — so zmxifying the terminal you are sitting in now works, which is the common case. --proc DIR is the fixture seam: the scan, the liveness guard, the exclusions and the ancestry rule are unit-tested against a fake process tree, never the live one. Suites: 87/87 root, 48/48 9ns, 26/26 9harness (+5 for the view), 60/60 9proc, 144/144 programs-test, 51+88 9ns integration, 44/0 9harness end-to-end (+16, including a real move against a fake harness and a fake zmx), 29/0 9proc debug, 213/0 9ns adversarial, freestanding green.
Diffstat (limited to '9harness/src/tree.zig')
-rw-r--r--9harness/src/tree.zig890
1 files changed, 879 insertions, 11 deletions
diff --git a/9harness/src/tree.zig b/9harness/src/tree.zig
index 6525e58..b6ff4a4 100644
--- a/9harness/src/tree.zig
+++ b/9harness/src/tree.zig
@@ -41,6 +41,7 @@ const fs = cloud9.fs;
const E = fs.E;
const Io = std.Io;
const linux = std.os.linux;
+pub const active = @import("active.zig");
// ---- comptime bounds ---------------------------------------------------------
@@ -84,9 +85,10 @@ pub const Top = enum(u8) {
hermes,
dsh,
skills,
+ active,
/// Directory entries of the root, in listing order.
- pub const listed = [_]Top{ .pid, .uptime, .claude, .codex, .omp, .hermes, .dsh, .skills };
+ pub const listed = [_]Top{ .pid, .uptime, .claude, .codex, .omp, .hermes, .dsh, .skills, .active };
pub fn fileName(t: Top) []const u8 {
return @tagName(t);
@@ -94,7 +96,7 @@ pub const Top = enum(u8) {
pub fn dir(t: Top) bool {
return switch (t) {
- .root, .claude, .codex, .omp, .hermes, .dsh, .skills => true,
+ .root, .claude, .codex, .omp, .hermes, .dsh, .skills, .active => true,
.pid, .uptime => false,
};
}
@@ -171,7 +173,82 @@ fn mountOwner(r: Root, rel_path: []const u8) Top {
// ---- node ids and the path table ---------------------------------------------
-pub const Kind = enum(u8) { top = 0, path = 1 };
+pub const Kind = enum(u8) { top = 0, path = 1, active = 2 };
+
+comptime {
+ // The derived view indexes the same pinned roots the mirror does.
+ for (std.enums.values(Root)) |r| {
+ if (!std.mem.eql(u8, @tagName(r), @tagName(@as(active.Kind, @enumFromInt(@intFromEnum(r))))))
+ @compileError("tree.Root and active.Kind must agree, index by index");
+ }
+}
+
+/// A node under `/active`. Everything but a harness directory names a
+/// slot of the live table and the generation it had when the id was
+/// minted, so an id outlives its process only long enough to answer
+/// ENOENT.
+pub const AFile = enum(u8) {
+ /// `/active/<harness>`
+ harness_dir,
+ /// `/active/<harness>/<pid>`
+ entry,
+ pid,
+ ppid,
+ started,
+ cwd,
+ name,
+ title,
+ session,
+ model,
+ via,
+ zmx,
+ status,
+ transcript,
+ /// `/active/<harness>/<pid>/agents`
+ agents,
+ /// `/active/<harness>/<pid>/agents/<name>`
+ agent,
+ agent_model,
+ agent_transcript,
+
+ /// The fields of one entry, in listing order. A field with no value
+ /// is not listed and does not resolve.
+ pub const entry_files = [_]AFile{
+ .pid, .ppid, .started, .cwd, .name, .title,
+ .session, .model, .via, .zmx, .status, .transcript,
+ };
+ pub const agent_files = [_]AFile{ .agent_model, .agent_transcript };
+
+ pub fn fileName(f: AFile) []const u8 {
+ return switch (f) {
+ .agent_model => "model",
+ .agent_transcript => "transcript",
+ else => @tagName(f),
+ };
+ }
+};
+
+/// A resolved `/active` node.
+pub const Act = struct {
+ file: AFile,
+ /// Only for `harness_dir`.
+ kind: active.Kind = .claude,
+ slot: u8 = 0,
+ gen: u8 = 0,
+ agent: u8 = 0,
+};
+
+pub fn activeNode(a: Act) u64 {
+ const serial: u48 = switch (a.file) {
+ .harness_dir => @intFromEnum(a.kind),
+ else => @as(u48, a.slot) | (@as(u48, a.gen) << 8) | (@as(u48, a.agent) << 16),
+ };
+ return @bitCast(Node{
+ .idx = @intFromEnum(a.file),
+ .kind = @intFromEnum(Kind.active),
+ .serial = serial,
+ });
+}
pub const Node = packed struct(u64) {
/// A Top index (kind == .top) or a Root index (kind == .path).
@@ -306,15 +383,71 @@ pub const Harness = struct {
list_dirs: [list_capacity]bool = undefined,
list_offs: [list_capacity]u32 = undefined,
+ // The derived view (`/active`). An empty `proc` base turns it off,
+ // which is the default: a test must opt in to reading a process
+ // tree, and never the live one.
+ live: active.Table = .{},
+ proc_buf: [base_capacity]u8 = @splat(0),
+ proc_len: u16 = 0,
+ home_buf: [base_capacity]u8 = @splat(0),
+ home_len: u16 = 0,
+ zmx_buf: [base_capacity]u8 = @splat(0),
+ zmx_len: u16 = 0,
+ runtime_buf: [base_capacity]u8 = @splat(0),
+ runtime_len: u16 = 0,
+ envp: ?cloud9.post.Env = null,
+ self_pid: u32 = 0,
+ btime: i64 = 0,
+ allow_move: bool = false,
+ act_text: [1024]u8 = undefined,
+ act_name: [64]u8 = undefined,
+ act_path: [active.path_capacity]u8 = undefined,
+
pub const InitOptions = struct {
io: Io,
pid: u32,
/// Base path of each root, or "" when the root is not pinned.
bases: [5][]const u8,
+ /// The proc filesystem `/active` reads. Empty — the default —
+ /// leaves the derived view out of the tree entirely.
+ proc: []const u8 = "",
+ /// $HOME, for the harnesses' store-slug spellings.
+ home: []const u8 = "",
+ /// The zmx binary a move execs, resolved to an absolute path by
+ /// the caller. Never client bytes.
+ zmx: []const u8 = "zmx",
+ /// $XDG_RUNTIME_DIR, where a move looks for the zmx session it
+ /// is about to create.
+ runtime: []const u8 = "",
+ /// The daemon's own environment block, handed to the harness a
+ /// move re-execs. Null leaves a moved agent with an empty one.
+ envp: ?cloud9.post.Env = null,
+ /// Whether writing `/active/<h>/<pid>/zmx` may move a session.
+ allow_move: bool = false,
};
pub fn init(h: *Harness, o: InitOptions) void {
h.* = .{ .io = o.io, .pid = o.pid, .started_sec = Io.Timestamp.now(o.io, .real).toSeconds() };
+ if (o.proc.len > 0 and o.proc.len <= base_capacity) {
+ @memcpy(h.proc_buf[0..o.proc.len], o.proc);
+ h.proc_len = @intCast(o.proc.len);
+ h.self_pid = o.pid;
+ h.btime = active.bootTime(o.io, o.proc);
+ }
+ if (o.home.len <= base_capacity) {
+ @memcpy(h.home_buf[0..o.home.len], o.home);
+ h.home_len = @intCast(o.home.len);
+ }
+ if (o.zmx.len > 0 and o.zmx.len <= base_capacity) {
+ @memcpy(h.zmx_buf[0..o.zmx.len], o.zmx);
+ h.zmx_len = @intCast(o.zmx.len);
+ }
+ if (o.runtime.len <= base_capacity) {
+ @memcpy(h.runtime_buf[0..o.runtime.len], o.runtime);
+ h.runtime_len = @intCast(o.runtime.len);
+ }
+ h.allow_move = o.allow_move;
+ h.envp = o.envp;
inline for (0..5) |i| {
const path = o.bases[i];
h.base_set[i] = path.len > 0 and path.len <= base_capacity;
@@ -388,6 +521,18 @@ pub const Harness = struct {
if (serialOf(slot, e.gen) != n.serial) return null;
return .{ .path = e };
},
+ .active => {
+ const f = std.enums.fromInt(AFile, n.idx) orelse return null;
+ if (f == .harness_dir) {
+ const k = std.enums.fromInt(active.Kind, @as(u8, @truncate(n.serial))) orelse return null;
+ return .{ .act = .{ .file = f, .kind = k } };
+ }
+ const slot: u8 = @truncate(n.serial);
+ const gen: u8 = @truncate(n.serial >> 8);
+ const agent: u8 = @truncate(n.serial >> 16);
+ const l = h.live.at(slot, gen) orelse return null;
+ return .{ .act = .{ .file = f, .kind = l.kind, .slot = slot, .gen = gen, .agent = agent } };
+ },
}
}
@@ -399,6 +544,7 @@ pub const Harness = struct {
pub const Target = union(enum) {
top: Top,
path: *Entry,
+ act: Act,
};
// ---- attributes ---------------------------------------------------------------
@@ -508,6 +654,7 @@ fn attrFor(h: *Harness, t: Target) ?fs.Attr {
.top => |top| {
switch (top) {
.root => return .{ .name = "/", .node = root, .dir = true, .mode = 0o555 },
+ .active => return .{ .name = "active", .node = topNode(.active), .dir = true, .mode = 0o555 },
.pid, .uptime => return .{
.name = top.fileName(),
.node = topNode(top),
@@ -530,6 +677,7 @@ fn attrFor(h: *Harness, t: Target) ?fs.Attr {
}
},
.path => |e| return statAttr(h, e),
+ .act => |a| return activeAttr(h, a),
}
}
@@ -544,6 +692,442 @@ fn attrOfMount(h: *Harness, r: Root, rel_path: []const u8) ?fs.Attr {
return statAttr(h, e);
}
+// ---- /active: the derived view -------------------------------------------------
+
+fn sourcesOf(h: *Harness) active.Sources {
+ var roots: [5][]const u8 = @splat("");
+ for (0..5) |i| {
+ if (h.base_set[i]) roots[i] = h.base_buf[i][0..h.base_len[i]];
+ }
+ return .{
+ .io = h.io,
+ .proc = h.proc_buf[0..h.proc_len],
+ .home = h.home_buf[0..h.home_len],
+ .roots = roots,
+ .self_pid = h.self_pid,
+ .btime = h.btime,
+ };
+}
+
+/// Rescans the process tree. Done on a listing of `/active` and on a
+/// lookup that misses, which is what makes an agent visible the moment
+/// it starts and gone the moment it exits.
+fn rescan(h: *Harness) void {
+ if (h.proc_len == 0) return;
+ h.live.scan(sourcesOf(h));
+}
+
+fn hasLive(h: *Harness, k: active.Kind) bool {
+ for (&h.live.slots) |*sl| {
+ if (sl.used and sl.live.kind == k) return true;
+ }
+ return false;
+}
+
+/// `<text>\n` staged where an answer can point at it.
+fn line(h: *Harness, text: []const u8) ?[]const u8 {
+ return std.fmt.bufPrint(&h.act_text, "{s}\n", .{text}) catch null;
+}
+
+/// The value of a field file, or null when this agent has none — an
+/// absent field is not listed and does not resolve, so the tree never
+/// answers a blank where it does not know.
+fn activeText(h: *Harness, a: Act) ?[]const u8 {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ const src = sourcesOf(h);
+ var scratch: [active.text_capacity]u8 = undefined;
+ return switch (a.file) {
+ .pid => std.fmt.bufPrint(&h.act_text, "{d}\n", .{l.pid}) catch null,
+ .ppid => if (l.ppid == 0) null else std.fmt.bufPrint(&h.act_text, "{d}\n", .{l.ppid}) catch null,
+ .started => if (l.started == 0) null else std.fmt.bufPrint(&h.act_text, "{d}\n", .{l.started}) catch null,
+ .cwd => if (l.cwd.len == 0) null else line(h, l.cwd.slice()),
+ .session => if (l.session.len == 0) null else line(h, l.session.slice()),
+ .via => line(h, l.via.text()),
+ .name => line(h, active.registryField(src, l, "name", &scratch) orelse return null),
+ .status => line(h, active.registryField(src, l, "status", &scratch) orelse return null),
+ .title => line(h, active.headField(h.io, l.kind, l.transcript.slice(), .title, &scratch) orelse return null),
+ .model => line(h, active.headField(h.io, l.kind, l.transcript.slice(), .model, &scratch) orelse return null),
+ .agent_model => blk: {
+ const path = activePath(h, a) orelse break :blk null;
+ break :blk line(h, active.headField(h.io, l.kind, path, .model, &scratch) orelse return null);
+ },
+ // `zmx` is always there, so it can always be written to; empty
+ // means the agent runs outside zmx.
+ .zmx => if (active.zmxOf(src, l.pid, &scratch)) |v| line(h, v) else h.act_text[0..0],
+ else => null,
+ };
+}
+
+/// The absolute path behind a transcript-shaped file.
+fn activePath(h: *Harness, a: Act) ?[]const u8 {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ switch (a.file) {
+ .transcript => return if (l.transcript.len == 0) null else l.transcript.slice(),
+ .agent_transcript, .agent_model => {
+ var ag: active.Agents = .{};
+ active.agentsOf(h.io, l, &ag);
+ return active.agentPath(l, &ag, a.agent, &h.act_path);
+ },
+ else => return null,
+ }
+}
+
+/// Opens an absolute path without following a symlink at the last
+/// component. These paths are derived from a pinned root or from the
+/// fd the harness itself holds, never from client bytes, but a name
+/// swapped underneath must still fail rather than redirect.
+fn openAbsNoFollow(path: []const u8) ?i32 {
+ if (path.len == 0 or path.len >= active.path_capacity) return null;
+ var z: [active.path_capacity]u8 = @splat(0);
+ @memcpy(z[0..path.len], path);
+ const rc = linux.open(@ptrCast(&z), .{
+ .ACCMODE = .RDONLY,
+ .NOFOLLOW = true,
+ .CLOEXEC = true,
+ .NONBLOCK = true,
+ }, 0);
+ if (linux.errno(rc) != .SUCCESS) return null;
+ return @intCast(rc);
+}
+
+fn activeAttr(h: *Harness, a: Act) ?fs.Attr {
+ switch (a.file) {
+ .harness_dir => {
+ if (!hasLive(h, a.kind)) return null;
+ return .{ .name = a.kind.text(), .node = activeNode(a), .dir = true, .mode = 0o555 };
+ },
+ .entry => {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ const nm = std.fmt.bufPrint(&h.act_name, "{d}", .{l.pid}) catch return null;
+ return .{ .name = nm, .node = activeNode(a), .dir = true, .mode = 0o555 };
+ },
+ .agents => {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ if (l.agent_dir.len == 0) return null;
+ return .{ .name = "agents", .node = activeNode(a), .dir = true, .mode = 0o555 };
+ },
+ .agent => {
+ const l = h.live.at(a.slot, a.gen) orelse return null;
+ var ag: active.Agents = .{};
+ active.agentsOf(h.io, l, &ag);
+ if (a.agent >= ag.count) return null;
+ const nm = ag.name(a.agent);
+ @memcpy(h.act_name[0..nm.len], nm);
+ return .{ .name = h.act_name[0..nm.len], .node = activeNode(a), .dir = true, .mode = 0o555 };
+ },
+ .transcript, .agent_transcript => {
+ const path = activePath(h, a) orelse return null;
+ const st = Io.Dir.statFile(.cwd(), h.io, path, .{ .follow_symlinks = false }) catch return null;
+ if (st.kind != .file) return null;
+ return .{
+ .name = a.file.fileName(),
+ .node = activeNode(a),
+ .size = st.size,
+ .mode = 0o444,
+ .mtime = @truncate(@as(u64, @bitCast(st.mtime.toSeconds()))),
+ };
+ },
+ else => {
+ const text = activeText(h, a) orelse return null;
+ // Only `zmx` is ever writable, and only when a move is allowed.
+ const mode: u16 = if (a.file == .zmx and h.allow_move) 0o644 else 0o444;
+ return .{ .name = a.file.fileName(), .node = activeNode(a), .size = text.len, .mode = mode };
+ },
+ }
+}
+
+/// The slot holding `pid` for this harness, if any.
+fn slotOfPid(h: *Harness, k: active.Kind, pid: u32) ?Act {
+ for (&h.live.slots, 0..) |*sl, i| {
+ if (!sl.used or sl.live.kind != k or sl.live.pid != pid) continue;
+ return .{ .file = .entry, .kind = k, .slot = @intCast(i), .gen = sl.gen };
+ }
+ return null;
+}
+
+fn activeLookup(h: *Harness, req: fs.Req, a: Act, name: []const u8) Answer {
+ switch (a.file) {
+ .harness_dir => {
+ const pid = std.fmt.parseInt(u32, name, 10) catch return fail(req.tag, E.NOENT);
+ var found = slotOfPid(h, a.kind, pid);
+ if (found == null) {
+ rescan(h);
+ found = slotOfPid(h, a.kind, pid);
+ }
+ const act = found orelse return fail(req.tag, E.NOENT);
+ return activeReply(h, req, act, name);
+ },
+ .entry => {
+ if (std.mem.eql(u8, name, "agents")) {
+ return activeReply(h, req, .{ .file = .agents, .kind = a.kind, .slot = a.slot, .gen = a.gen }, name);
+ }
+ for (AFile.entry_files) |f| {
+ if (!std.mem.eql(u8, f.fileName(), name)) continue;
+ return activeReply(h, req, .{ .file = f, .kind = a.kind, .slot = a.slot, .gen = a.gen }, name);
+ }
+ return fail(req.tag, E.NOENT);
+ },
+ .agents => {
+ const l = h.live.at(a.slot, a.gen) orelse return fail(req.tag, E.NOENT);
+ var ag: active.Agents = .{};
+ active.agentsOf(h.io, l, &ag);
+ const i = ag.indexOf(name) orelse return fail(req.tag, E.NOENT);
+ return activeReply(h, req, .{
+ .file = .agent,
+ .kind = a.kind,
+ .slot = a.slot,
+ .gen = a.gen,
+ .agent = @intCast(i),
+ }, name);
+ },
+ .agent => {
+ for (AFile.agent_files) |f| {
+ if (!std.mem.eql(u8, f.fileName(), name)) continue;
+ return activeReply(h, req, .{
+ .file = f,
+ .kind = a.kind,
+ .slot = a.slot,
+ .gen = a.gen,
+ .agent = a.agent,
+ }, name);
+ }
+ return fail(req.tag, E.NOENT);
+ },
+ else => return fail(req.tag, E.NOTDIR),
+ }
+}
+
+fn activeReply(h: *Harness, req: fs.Req, a: Act, name: []const u8) Answer {
+ const attr = activeAttr(h, a) orelse return fail(req.tag, E.NOENT);
+ var with_name = attr;
+ with_name.name = name;
+ return .{ .reply = .{ .tag = req.tag, .attr = with_name } };
+}
+
+fn activeReaddir(h: *Harness, req: fs.Req, a: Act) Answer {
+ var st: Staging = .{ .buf = &h.stage_buf, .skip = req.off };
+ switch (a.file) {
+ .harness_dir => {
+ for (&h.live.slots, 0..) |*sl, i| {
+ if (!sl.used or sl.live.kind != a.kind) continue;
+ var num: [24]u8 = undefined;
+ const nm = std.fmt.bufPrint(&num, "{d}", .{sl.live.pid}) catch continue;
+ st.add(activeNode(.{
+ .file = .entry,
+ .kind = a.kind,
+ .slot = @intCast(i),
+ .gen = sl.gen,
+ }), true, nm);
+ }
+ },
+ .entry => {
+ const l = h.live.at(a.slot, a.gen) orelse return fail(req.tag, E.NOENT);
+ for (AFile.entry_files) |f| {
+ const child: Act = .{ .file = f, .kind = a.kind, .slot = a.slot, .gen = a.gen };
+ if (activeAttr(h, child) == null) continue; // no value: not listed
+ st.add(activeNode(child), false, f.fileName());
+ }
+ if (l.agent_dir.len > 0) {
+ st.add(activeNode(.{ .file = .agents, .kind = a.kind, .slot = a.slot, .gen = a.gen }), true, "agents");
+ }
+ },
+ .agents => {
+ const l = h.live.at(a.slot, a.gen) orelse return fail(req.tag, E.NOENT);
+ var ag: active.Agents = .{};
+ active.agentsOf(h.io, l, &ag);
+ for (0..ag.count) |i| {
+ st.add(activeNode(.{
+ .file = .agent,
+ .kind = a.kind,
+ .slot = a.slot,
+ .gen = a.gen,
+ .agent = @intCast(i),
+ }), true, ag.name(i));
+ }
+ },
+ .agent => {
+ for (AFile.agent_files) |f| {
+ const child: Act = .{ .file = f, .kind = a.kind, .slot = a.slot, .gen = a.gen, .agent = a.agent };
+ if (activeAttr(h, child) == null) continue;
+ st.add(activeNode(child), false, f.fileName());
+ }
+ },
+ else => return fail(req.tag, E.NOTDIR),
+ }
+ return .{ .reply = .{ .tag = req.tag }, .bytes = h.stage_buf[0..st.len] };
+}
+
+fn activeRead(h: *Harness, req: fs.Req, a: Act) Answer {
+ switch (a.file) {
+ .harness_dir, .entry, .agents, .agent => return fail(req.tag, E.ISDIR),
+ .transcript, .agent_transcript => {
+ const path = activePath(h, a) orelse return fail(req.tag, E.NOENT);
+ const fd = openAbsNoFollow(path) orelse return fail(req.tag, E.NOENT);
+ const file: Io.File = .{ .handle = fd, .flags = .{ .nonblocking = false } };
+ defer file.close(h.io);
+ const st = file.stat(h.io) catch return fail(req.tag, E.IO);
+ if (st.kind == .directory) return fail(req.tag, E.ISDIR);
+ if (st.kind != .file) return fail(req.tag, E.PERM);
+ _ = linux.fcntl(fd, linux.F.SETFL, 0);
+ const want = @min(req.size, h.data_buf.len);
+ const n = file.readPositionalAll(h.io, h.data_buf[0..want], req.off) catch
+ return fail(req.tag, E.IO);
+ return .{ .reply = .{ .tag = req.tag }, .bytes = h.data_buf[0..n] };
+ },
+ else => {
+ const text = activeText(h, a) orelse return fail(req.tag, E.NOENT);
+ return window(req, text);
+ },
+ }
+}
+
+// ---- the move: writing a zmx session name into an agent's `zmx` ----------------
+
+/// How long a move waits, in 100ms ticks: for the agent to take the
+/// hangup, then to die outright, then for the new zmx session to post.
+const term_ticks: usize = 50;
+const kill_ticks: usize = 30;
+const post_ticks: usize = 100;
+
+fn napOneTick(h: *Harness) void {
+ h.io.sleep(.fromMilliseconds(100), .awake) catch {};
+}
+
+/// Is this name already a live zmx session? zmx posts each session into
+/// the registry, so the registry is the answer — no process scanning.
+fn zmxPosted(h: *Harness, name: []const u8) bool {
+ if (h.runtime_len == 0) return false;
+ var buf: [active.path_capacity]u8 = undefined;
+ const path = std.fmt.bufPrint(&buf, "{s}/9p/zmx/{s}", .{ h.runtime_buf[0..h.runtime_len], name }) catch return true;
+ return Io.Dir.statFile(.cwd(), h.io, path, .{ .follow_symlinks = true }) != error.FileNotFound;
+}
+
+fn procGone(h: *Harness, pid: u32) bool {
+ var buf: [active.path_capacity]u8 = undefined;
+ const path = std.fmt.bufPrint(&buf, "{s}/{d}", .{ h.proc_buf[0..h.proc_len], pid }) catch return false;
+ _ = Io.Dir.statFile(.cwd(), h.io, path, .{ .follow_symlinks = true }) catch return true;
+ return false;
+}
+
+/// SIGTERM, then SIGKILL, then give up. The agent's session was
+/// resolved before this ran, so whatever happens it has somewhere to
+/// come back to.
+fn killAndWait(h: *Harness, pid: u32) bool {
+ _ = linux.kill(@intCast(pid), .TERM);
+ for (0..term_ticks) |_| {
+ if (procGone(h, pid)) return true;
+ napOneTick(h);
+ }
+ _ = linux.kill(@intCast(pid), .KILL);
+ for (0..kill_ticks) |_| {
+ if (procGone(h, pid)) return true;
+ napOneTick(h);
+ }
+ return false;
+}
+
+/// `zmx run <name> -d <harness> <resume flag> <resume value>`, in the
+/// agent's own directory. Every word but `<name>` is fixed by the
+/// harness; `<name>` was checked against zmx's label charset before
+/// anything was killed. No byte a client wrote reaches `exec` as a
+/// command.
+fn spawnZmx(h: *Harness, l: *const active.Live, name: []const u8, resume_value: []const u8) bool {
+ if (h.zmx_len == 0) return false;
+ const harness_argv0: []const u8 = @tagName(l.kind);
+ const resume_flag: []const u8 = switch (l.kind) {
+ .codex => "resume",
+ else => "--resume",
+ };
+
+ // One buffer holds every NUL-terminated word; `argv` points into it.
+ var words: [8 * active.path_capacity]u8 = undefined;
+ var used: usize = 0;
+ var argv: [9]?[*:0]const u8 = @splat(null);
+ var n: usize = 0;
+ const parts = [_][]const u8{
+ h.zmx_buf[0..h.zmx_len], "run", name, "-d",
+ harness_argv0, resume_flag, resume_value,
+ };
+ for (parts) |w| {
+ if (used + w.len + 1 > words.len or n + 1 >= argv.len) return false;
+ @memcpy(words[used..][0..w.len], w);
+ words[used + w.len] = 0;
+ argv[n] = @ptrCast(&words[used]);
+ n += 1;
+ used += w.len + 1;
+ }
+
+ var cwd_z: [active.cwd_capacity + 1]u8 = @splat(0);
+ if (l.cwd.len >= cwd_z.len) return false;
+ @memcpy(cwd_z[0..l.cwd.len], l.cwd.slice());
+
+ const rc = linux.fork();
+ if (linux.errno(rc) != .SUCCESS) return false;
+ if (rc == 0) {
+ // The child: only async-signal-safe calls from here.
+ _ = linux.chdir(@ptrCast(&cwd_z));
+ const empty = [_:null]?[*:0]const u8{};
+ const envp: cloud9.post.Env = h.envp orelse &empty;
+ _ = linux.execve(argv[0].?, @ptrCast(&argv), envp);
+ linux.exit(127);
+ }
+ // Reap the forked `zmx`, which returns as soon as the session is up.
+ const child: i32 = @intCast(rc);
+ var status: u32 = 0;
+ for (0..post_ticks) |_| {
+ const w = linux.waitpid(child, &status, 1); // WNOHANG
+ if (w == @as(usize, @intCast(child))) break;
+ napOneTick(h);
+ }
+ return true;
+}
+
+/// A move: kill the agent and bring it back inside a zmx session of the
+/// name written. Refusals come before anything is destroyed.
+fn moveToZmx(h: *Harness, req: fs.Req, a: Act) Answer {
+ if (!h.allow_move) return fail(req.tag, E.PERM);
+ const name = std.mem.trim(u8, req.data, " \t\r\n");
+ if (!active.legalZmxName(name)) return fail(req.tag, E.INVAL);
+
+ const l = h.live.at(a.slot, a.gen) orelse return fail(req.tag, E.NOENT);
+ // Nothing is killed that has nowhere to come back to.
+ if (l.via == .none or l.session.len == 0) return fail(req.tag, E.PERM);
+ if (l.cwd.len == 0) return fail(req.tag, E.PERM);
+ const resume_value: []const u8 = switch (l.kind) {
+ // omp resumes by the transcript it wrote, the others by id.
+ .omp => l.transcript.slice(),
+ .claude, .codex, .hermes => l.session.slice(),
+ // dsh has no resume form worth guessing at.
+ .dsh => return fail(req.tag, E.PERM),
+ };
+ if (resume_value.len == 0) return fail(req.tag, E.PERM);
+
+ // Already there: setting a value it already has changes nothing.
+ var have: [active.text_capacity]u8 = undefined;
+ if (active.zmxOf(sourcesOf(h), l.pid, &have)) |current| {
+ if (std.mem.eql(u8, current, name)) {
+ return .{ .reply = .{ .tag = req.tag, .written = @intCast(req.data.len) } };
+ }
+ }
+ if (zmxPosted(h, name)) return fail(req.tag, E.EXIST);
+ // The slot could have gone stale between the scan and this write.
+ if (!active.stillAlive(sourcesOf(h), l)) return fail(req.tag, E.NOENT);
+
+ // Everything below this line destroys something.
+ var snapshot = l.*;
+ if (!killAndWait(h, snapshot.pid)) return fail(req.tag, E.IO);
+ if (!spawnZmx(h, &snapshot, name, resume_value)) return fail(req.tag, E.IO);
+ for (0..post_ticks) |_| {
+ if (zmxPosted(h, name)) {
+ rescan(h);
+ return .{ .reply = .{ .tag = req.tag, .written = @intCast(req.data.len) } };
+ }
+ napOneTick(h);
+ }
+ rescan(h);
+ return fail(req.tag, E.IO);
+}
+
// ---- dispatch ------------------------------------------------------------------
/// Answers one engine request. The caller holds `h.mutex` and replies
@@ -553,15 +1137,39 @@ pub fn handle(h: *Harness, req: fs.Req) Answer {
return switch (req.op) {
.lookup => lookup(h, req, t),
.getattr => attrReply(h, req.tag, t),
- .setattr => fail(req.tag, E.PERM),
+ .setattr => setattrReq(h, req, t),
.open => open(h, req, t),
.release => release(h, req, t),
.readdir => readdir(h, req, t),
.read => read(h, req, t),
- .write => fail(req.tag, E.PERM),
+ .write => writeReq(h, req, t),
};
}
+/// Truncation is how a shell's `>` opens a file before writing it. The
+/// `zmx` file has no length of its own — a write replaces the value —
+/// so on the one writable file a zero-length truncate is a no-op
+/// rather than a refusal. Everything else still answers EPERM.
+fn setattrReq(h: *Harness, req: fs.Req, t: Target) Answer {
+ switch (t) {
+ .act => |a| if (a.file == .zmx and h.allow_move and req.truncate) {
+ return .{ .reply = .{ .tag = req.tag } };
+ },
+ else => {},
+ }
+ return fail(req.tag, E.PERM);
+}
+
+/// The tree answers EPERM to every write but one: a zmx session name
+/// into a live agent's `zmx`, which moves it there.
+fn writeReq(h: *Harness, req: fs.Req, t: Target) Answer {
+ switch (t) {
+ .act => |a| if (a.file == .zmx) return moveToZmx(h, req, a),
+ else => {},
+ }
+ return fail(req.tag, E.PERM);
+}
+
fn attrReply(h: *Harness, tag: u64, t: Target) Answer {
const a = attrFor(h, t) orelse return fail(tag, E.NOENT);
return .{ .reply = .{ .tag = tag, .attr = a } };
@@ -586,7 +1194,7 @@ fn lookup(h: *Harness, req: fs.Req, t: Target) Answer {
// The engine asks for "." when cloning a fid; the reference is
// paid for path targets here like any other lookup result.
switch (t) {
- .top => return attrReply(h, req.tag, t),
+ .top, .act => return attrReply(h, req.tag, t),
.path => |e| {
e.refs += 1;
return lookupAttr(h, req, t, name);
@@ -618,6 +1226,16 @@ fn lookup(h: *Harness, req: fs.Req, t: Target) Answer {
const m = top.mirror().?;
return lookupBelow(h, req, m.root, m.rel, name);
},
+ .active => {
+ const k = blk: for (std.enums.values(active.Kind)) |k| {
+ if (std.mem.eql(u8, k.text(), name)) break :blk k;
+ } else return fail(req.tag, E.NOENT);
+ if (!hasLive(h, k)) {
+ rescan(h);
+ if (!hasLive(h, k)) return fail(req.tag, E.NOENT);
+ }
+ return activeReply(h, req, .{ .file = .harness_dir, .kind = k }, name);
+ },
.pid, .uptime => return fail(req.tag, E.NOTDIR),
},
.path => |e| {
@@ -627,6 +1245,7 @@ fn lookup(h: *Harness, req: fs.Req, t: Target) Answer {
} else return fail(req.tag, E.NOENT);
return lookupBelow(h, req, e.root, rel_path, name);
},
+ .act => |a| return activeLookup(h, req, a, name),
}
}
@@ -673,6 +1292,21 @@ fn lookupParent(h: *Harness, req: fs.Req, t: Target) Answer {
}
break :blk .{ .top = mountOwner(e.root, rel_path) };
},
+ .act => |a| switch (a.file) {
+ .harness_dir => .{ .top = .active },
+ .entry => .{ .act = .{ .file = .harness_dir, .kind = a.kind } },
+ .agents => .{ .act = .{ .file = .entry, .kind = a.kind, .slot = a.slot, .gen = a.gen } },
+ .agent => .{ .act = .{ .file = .agents, .kind = a.kind, .slot = a.slot, .gen = a.gen } },
+ .agent_model, .agent_transcript => .{ .act = .{
+ .file = .agent,
+ .kind = a.kind,
+ .slot = a.slot,
+ .gen = a.gen,
+ .agent = a.agent,
+ } },
+ // Every other file hangs directly off its entry.
+ else => .{ .act = .{ .file = .entry, .kind = a.kind, .slot = a.slot, .gen = a.gen } },
+ },
};
return attrReply(h, req.tag, parent);
}
@@ -687,17 +1321,26 @@ fn unref(e: *Entry) void {
fn open(h: *Harness, req: fs.Req, t: Target) Answer {
_ = attrFor(h, t) orelse return fail(req.tag, E.NOENT); // still there?
- // Read-only tree: any open that would write or truncate is refused.
+ // Read-only tree, with exactly one exception: the `zmx` file of a
+ // live agent, and only when the daemon was started to allow moves.
+ // Truncation is meaningless there (a write replaces the value), so
+ // it is accepted rather than refused, which is what `>` needs.
+ const writable = switch (t) {
+ .act => |a| a.file == .zmx and h.allow_move,
+ else => false,
+ };
const rw = req.omode & 3;
- if (rw == cloud9.owrite or rw == cloud9.ordwr) return fail(req.tag, E.PERM);
- if (req.omode & cloud9.otrunc != 0) return fail(req.tag, E.PERM);
+ if (!writable) {
+ if (rw == cloud9.owrite or rw == cloud9.ordwr) return fail(req.tag, E.PERM);
+ if (req.omode & cloud9.otrunc != 0) return fail(req.tag, E.PERM);
+ }
return .{ .reply = .{ .tag = req.tag, .handle = 1 } };
}
fn release(h: *Harness, req: fs.Req, t: Target) Answer {
_ = h;
switch (t) {
- .top => {},
+ .top, .act => {},
.path => |e| unref(e),
}
return .{ .reply = .{ .tag = req.tag } };
@@ -714,6 +1357,7 @@ fn read(h: *Harness, req: fs.Req, t: Target) Answer {
},
else => return fail(req.tag, E.ISDIR),
},
+ .act => |a| return activeRead(h, req, a),
.path => |e| {
// `O_NONBLOCK` so a fifo left in a harness root cannot park the
// daemon in `open`; the kind check below refuses it anyway.
@@ -792,6 +1436,13 @@ fn readdir(h: *Harness, req: fs.Req, t: Target) Answer {
}
},
.pid, .uptime => return fail(req.tag, E.NOTDIR),
+ .active => {
+ rescan(h);
+ for (std.enums.values(active.Kind)) |k| {
+ if (!hasLive(h, k)) continue;
+ st.add(activeNode(.{ .file = .harness_dir, .kind = k }), true, k.text());
+ }
+ },
.omp, .hermes, .dsh => {
const m = top.mirror().?;
if (h.base(m.root) == null) return fail(req.tag, E.NOENT);
@@ -814,6 +1465,7 @@ fn readdir(h: *Harness, req: fs.Req, t: Target) Answer {
.overflow => return fail(req.tag, E.NFILE),
}
},
+ .act => |a| return activeReaddir(h, req, a),
}
return .{ .reply = .{ .tag = req.tag }, .bytes = h.stage_buf[0..st.len] };
}
@@ -904,6 +1556,14 @@ const testing = std.testing;
/// ~/.claude or any other live harness root.
const Rig = struct {
dir: testing.TmpDir,
+ /// Build a fixture process tree under <home>/proc and point
+ /// `/active` at it. Off by default: a unit test must opt in to
+ /// reading a process tree, and it is never the live one.
+ with_proc: bool = false,
+ /// The pid the daemon believes it is, for the ancestry exclusion.
+ self_pid: u32 = 4242,
+ zmx: []const u8 = "zmx",
+ allow_move: bool = false,
path_buf: [std.fs.max_path_bytes]u8 = undefined,
home: []const u8 = undefined,
h: *Harness = undefined,
@@ -931,7 +1591,24 @@ const Rig = struct {
bases[i] = try std.fmt.bufPrint(&base_buf[i], "{s}/{s}", .{ rig.home, home_dirs[i] });
}
rig.harness_mem = undefined;
- rig.harness_mem.init(.{ .io = io, .pid = 4242, .bases = bases });
+ var proc_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const proc_root: []const u8 = if (rig.with_proc)
+ try std.fmt.bufPrint(&proc_buf, "{s}/proc", .{rig.home})
+ else
+ "";
+ if (rig.with_proc) {
+ try Io.Dir.cwd().createDirPath(io, proc_root);
+ try rig.put("proc/stat", "cpu 1 2 3\nbtime 1000000\nprocesses 7\n");
+ }
+ rig.harness_mem.init(.{
+ .io = io,
+ .pid = rig.self_pid,
+ .bases = bases,
+ .proc = proc_root,
+ .home = rig.home,
+ .zmx = rig.zmx,
+ .allow_move = rig.allow_move,
+ });
rig.h = &rig.harness_mem;
}
@@ -964,6 +1641,58 @@ const Rig = struct {
fn lookupName(rig: *Rig, tag: u64, dir_node: u64, name: []const u8) Answer {
return handle(rig.h, .{ .tag = tag, .op = .lookup, .node = dir_node, .data = name });
}
+
+ /// Writes one fake process into the fixture `/proc`: the `stat`
+ /// line (with the start time in field 22, after a name that holds
+ /// the spaces and parentheses a real one can), the NUL-separated
+ /// `cmdline`, and a `cwd` symlink.
+ fn fakeProc(rig: *Rig, o: struct {
+ pid: u32,
+ ppid: u32 = 1,
+ comm: []const u8 = "x",
+ starttime: u64 = 5000,
+ argv: []const u8,
+ cwd: []const u8 = "",
+ }) !void {
+ var rel: [128]u8 = undefined;
+ var stat_text: [512]u8 = undefined;
+ var w = Io.Writer.fixed(&stat_text);
+ try w.print("{d} ({s}) S {d}", .{ o.pid, o.comm, o.ppid });
+ for (5..22) |i| try w.print(" {d}", .{i});
+ try w.print(" {d} 0 0\n", .{o.starttime});
+ try rig.put(try std.fmt.bufPrint(&rel, "proc/{d}/stat", .{o.pid}), w.buffered());
+ try rig.put(try std.fmt.bufPrint(&rel, "proc/{d}/cmdline", .{o.pid}), o.argv);
+
+ const target = if (o.cwd.len > 0) o.cwd else rig.home;
+ var link_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const link = try std.fmt.bufPrintZ(&link_buf, "{s}/proc/{d}/cwd", .{ rig.home, o.pid });
+ Io.Dir.cwd().symLink(testing.io, target, link, .{}) catch {};
+ }
+
+ /// Removes a fake process, as an exit would.
+ fn reapProc(rig: *Rig, pid: u32) !void {
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const dir_path = try std.fmt.bufPrint(&buf, "{s}/proc/{d}", .{ rig.home, pid });
+ const d = try Io.Dir.openDirAbsolute(testing.io, dir_path, .{ .iterate = true });
+ var rb: [Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined;
+ var it = Io.Dir.Reader.init(d, &rb);
+ var names: [8][64]u8 = undefined;
+ var lens: [8]usize = @splat(0);
+ var n: usize = 0;
+ while (n < names.len) {
+ const e = (it.next(testing.io) catch break) orelse break;
+ @memcpy(names[n][0..e.name.len], e.name);
+ lens[n] = e.name.len;
+ n += 1;
+ }
+ Io.Dir.close(d, testing.io);
+ for (0..n) |i| {
+ var one: [std.fs.max_path_bytes]u8 = undefined;
+ const path = try std.fmt.bufPrint(&one, "{s}/{s}", .{ dir_path, names[i][0..lens[i]] });
+ Io.Dir.deleteFileAbsolute(testing.io, path) catch {};
+ }
+ try Io.Dir.cwd().deleteDir(testing.io, dir_path);
+ }
};
const home_dirs = [5][]const u8{ ".claude", ".codex", ".omp", ".hermes", ".dsh" };
@@ -1327,3 +2056,142 @@ test "tree: a listing spanning several reads loses no entry" {
try testing.expect(reads > 1); // the listing really did span several reads
for (seen) |s| try testing.expect(s);
}
+
+test {
+ _ = active; // the derived view's own tests run with the tree's
+}
+
+// ---- /active: the derived view ------------------------------------------------
+
+/// Walks `/active` down to one agent's directory, answering its node.
+fn activeEntryNode(rig: *Rig, harness: []const u8, pid: []const u8) !u64 {
+ const act = rig.lookupName(90, root, "active");
+ try testing.expect(act.reply.status == .ok);
+ // A listing is what rescans, so it comes before the walk.
+ _ = handle(rig.h, .{ .tag = 91, .op = .readdir, .node = act.reply.attr.node, .off = 0, .size = msize });
+ const h_dir = rig.lookupName(92, act.reply.attr.node, harness);
+ try testing.expect(h_dir.reply.status == .ok);
+ const entry = rig.lookupName(93, h_dir.reply.attr.node, pid);
+ try testing.expect(entry.reply.status == .ok);
+ return entry.reply.attr.node;
+}
+
+fn activeField(rig: *Rig, entry: u64, name: []const u8) ![]const u8 {
+ const f = rig.lookupName(94, entry, name);
+ try testing.expect(f.reply.status == .ok);
+ const r = handle(rig.h, .{ .tag = 95, .op = .read, .node = f.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(r.reply.status == .ok);
+ return r.bytes;
+}
+
+test "active: a fixture process tree lists by harness and by pid" {
+ var rig: Rig = .{ .dir = undefined, .with_proc = true };
+ try rig.start();
+ defer rig.end();
+
+ // A Claude Code session, resolved the way the harness publishes it.
+ try rig.fakeProc(.{ .pid = 1001, .comm = "claude", .starttime = 5000, .argv = "claude\x00--print\x00" });
+ var rec: [512]u8 = undefined;
+ try rig.put(".claude/sessions/1001.json", try std.fmt.bufPrint(&rec,
+ \\{{"pid":1001,"sessionId":"sess-abc","cwd":"{s}",
+ \\ "name":"fixture-one","status":"busy","procStart":"5000"}}
+ , .{rig.home}));
+ var slug_buf: [512]u8 = undefined;
+ const slug = active.slugOf(.claude, rig.home, rig.home, &slug_buf).?;
+ var tr: [640]u8 = undefined;
+ try rig.put(
+ try std.fmt.bufPrint(&tr, ".claude/projects/{s}/sess-abc.jsonl", .{slug}),
+ "{\"type\":\"summary\",\"aiTitle\":\"Fixture Title\"}\n",
+ );
+
+ const entry = try activeEntryNode(&rig, "claude", "1001");
+ try testing.expectEqualStrings("1001\n", try activeField(&rig, entry, "pid"));
+ try testing.expectEqualStrings("sess-abc\n", try activeField(&rig, entry, "session"));
+ try testing.expectEqualStrings("registry\n", try activeField(&rig, entry, "via"));
+ try testing.expectEqualStrings("fixture-one\n", try activeField(&rig, entry, "name"));
+ try testing.expectEqualStrings("busy\n", try activeField(&rig, entry, "status"));
+ try testing.expectEqualStrings("Fixture Title\n", try activeField(&rig, entry, "title"));
+ // started is the boot time plus the process's own, in seconds.
+ try testing.expectEqualStrings("1000050\n", try activeField(&rig, entry, "started"));
+ // The transcript is served as the file it is, not as a field.
+ const t = rig.lookupName(96, entry, "transcript");
+ try testing.expect(t.reply.status == .ok);
+ const bytes = handle(rig.h, .{ .tag = 97, .op = .read, .node = t.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(std.mem.indexOf(u8, bytes.bytes, "Fixture Title") != null);
+ // A field this harness does not publish is absent, not blank.
+ try expectNoent(rig.lookupName(98, entry, "model"));
+}
+
+test "active: a daemon or helper is never listed as an agent" {
+ var rig: Rig = .{ .dir = undefined, .with_proc = true };
+ try rig.start();
+ defer rig.end();
+ // The shapes actually running on this machine.
+ try rig.fakeProc(.{ .pid = 1010, .comm = "python3", .argv = "python3\x00-m\x00hermes_cli.main\x00gateway\x00run\x00" });
+ try rig.fakeProc(.{ .pid = 1011, .comm = "omp", .argv = "omp\x00__omp_worker_daemon_broker\x00" });
+ try rig.fakeProc(.{ .pid = 1012, .comm = "claude", .argv = "claude\x00mcp-server\x00" });
+ // ...and one real session, so an empty answer cannot pass by default.
+ try rig.fakeProc(.{ .pid = 1013, .comm = "claude", .argv = "claude\x00" });
+
+ const act = rig.lookupName(1, root, "active");
+ const listing = handle(rig.h, .{ .tag = 2, .op = .readdir, .node = act.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(stageHas(listing.bytes, "claude"));
+ try testing.expect(!stageHas(listing.bytes, "hermes"));
+ try testing.expect(!stageHas(listing.bytes, "omp"));
+ const claude = rig.lookupName(3, act.reply.attr.node, "claude");
+ const pids = handle(rig.h, .{ .tag = 4, .op = .readdir, .node = claude.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(stageHas(pids.bytes, "1013"));
+ try testing.expect(!stageHas(pids.bytes, "1012")); // the mcp server
+}
+
+test "active: a pid reused by another process stops resolving" {
+ var rig: Rig = .{ .dir = undefined, .with_proc = true };
+ try rig.start();
+ defer rig.end();
+ try rig.fakeProc(.{ .pid = 1020, .comm = "claude", .starttime = 5000, .argv = "claude\x00" });
+ const entry = try activeEntryNode(&rig, "claude", "1020");
+ try testing.expect(handle(rig.h, .{ .tag = 5, .op = .getattr, .node = entry }).reply.status == .ok);
+
+ // The same pid, a different process: only the start time says so.
+ try rig.fakeProc(.{ .pid = 1020, .comm = "claude", .starttime = 9999, .argv = "claude\x00" });
+ const act = rig.lookupName(6, root, "active");
+ _ = handle(rig.h, .{ .tag = 7, .op = .readdir, .node = act.reply.attr.node, .off = 0, .size = msize });
+ try expectNoent(handle(rig.h, .{ .tag = 8, .op = .getattr, .node = entry }));
+ // The pid is still there — as the new process, under a new node.
+ const fresh = try activeEntryNode(&rig, "claude", "1020");
+ try testing.expect(fresh != entry);
+}
+
+test "active: a process that exits leaves the tree" {
+ var rig: Rig = .{ .dir = undefined, .with_proc = true };
+ try rig.start();
+ defer rig.end();
+ try rig.fakeProc(.{ .pid = 1030, .comm = "claude", .argv = "claude\x00" });
+ const entry = try activeEntryNode(&rig, "claude", "1030");
+ try testing.expect(handle(rig.h, .{ .tag = 9, .op = .getattr, .node = entry }).reply.status == .ok);
+
+ try rig.reapProc(1030);
+ const act = rig.lookupName(10, root, "active");
+ const listing = handle(rig.h, .{ .tag = 11, .op = .readdir, .node = act.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(!stageHas(listing.bytes, "claude"));
+ try expectNoent(handle(rig.h, .{ .tag = 12, .op = .getattr, .node = entry }));
+ try expectNoent(rig.lookupName(13, act.reply.attr.node, "claude"));
+}
+
+test "active: the daemon never lists the process tree it lives in" {
+ // 1041 is the daemon, its parent 1040 is a harness: showing it would
+ // let a client act on the tree serving it.
+ var rig: Rig = .{ .dir = undefined, .with_proc = true, .self_pid = 1041 };
+ try rig.start();
+ defer rig.end();
+ try rig.fakeProc(.{ .pid = 1040, .comm = "claude", .argv = "claude\x00" });
+ try rig.fakeProc(.{ .pid = 1041, .comm = "9harness", .ppid = 1040, .argv = "9harness\x00" });
+ try rig.fakeProc(.{ .pid = 1042, .comm = "claude", .argv = "claude\x00" });
+
+ const act = rig.lookupName(14, root, "active");
+ const claude = rig.lookupName(15, act.reply.attr.node, "claude");
+ try testing.expect(claude.reply.status == .ok);
+ const pids = handle(rig.h, .{ .tag = 16, .op = .readdir, .node = claude.reply.attr.node, .off = 0, .size = msize });
+ try testing.expect(stageHas(pids.bytes, "1042")); // an unrelated session lists
+ try testing.expect(!stageHas(pids.bytes, "1040")); // its own parent does not
+}