diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-25 16:40:49 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-25 17:52:33 -0300 |
| commit | 1c1b192d4ef59199a7196229d56901b1f6512678 (patch) | |
| tree | 9f6e4c1efcf877d1231aa58975ccd923ad4f2f2e /9agents/src/main.zig | |
| parent | 73602127d15d10a1932b6fe916bd18a608054980 (diff) | |
| download | cloud9-main.tar.gz cloud9-main.zip | |
Each message of a claude or codex agent's transcript is a file named
<id>-<kind>: the id is its position in the transcript from 0, as 8
digits so the names sort; the kind is user, assistant, thinking, system
or agent, and a tool call is named after its tool (00000002-bash) and
its result after the call (00000003-bash-result). A file is the message
as text (a tool call: its name, then its input); its mtime is when it
was said.
chat.zig indexes a transcript incrementally, keyed by (dev, ino) and
checked by birth time and a hash of its first and last indexed bytes, so
a file rewritten in place is indexed again. It holds offsets, never
bytes: every read reads the file, and a read resumes where the last one
stopped so a long message is not decoded from its start each time. The
indexes and the runner live in lazily backed mappings (NORESERVE,
NOHUGEPAGE), and the fid table is 32768 so a kernel mount can hold every
message of a long chat.
A 9agents started inside a user namespace (from a 9ns-wrapped terminal)
cannot read /proc/<pid>/fd of processes outside it, so the fd route
never resolved codex there; /active now also finds a codex session
through the thread writer locks it holds (/proc/locks), via = lock.
Fixes to /active found on the way: a fifo in place of a transcript or
record no longer blocks the daemon; a transcript is opened from its
pinned root a component at a time, so a symlinked directory or a
sessionId with .. cannot reach outside it; slot generations are 24 bits,
so a stale node id cannot come to name another agent; listing a harness
rescans; an agent that had not resolved yet is asked again.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to '9agents/src/main.zig')
| -rw-r--r-- | 9agents/src/main.zig | 43 |
1 files changed, 34 insertions, 9 deletions
diff --git a/9agents/src/main.zig b/9agents/src/main.zig index 2ac9078..3e7c290 100644 --- a/9agents/src/main.zig +++ b/9agents/src/main.zig @@ -43,6 +43,7 @@ const usage_text = \\ /omp /hermes /dsh full mirrors, raw \\ /skills/{claude,codex,omp} the union skills view \\ /active/<harness>/<pid>/ what is running right now + \\ /active/<harness>/<pid>/chat/<id>-<kind> its conversation (claude, codex) \\ \\By default the daemon posts itself under the name `agents`, so it is \\dialable at $XDG_RUNTIME_DIR/9p/agents and mountable by 9ns --mntgen @@ -71,10 +72,27 @@ const Runner = serve.Runner(tree.Harness, tree.opts, .{ .listeners = 2, // the posted name plus one of --unix/--tcp }); -// Static memory, the 9proc-demo way: the harness and the runner are large -// (the path table, the per-connection buffers) and live in .bss. +// Static memory, the 9proc-demo way: the harness (the path table) lives in +// .bss. The runner and the chat pool are larger — every connection's fid +// table, every transcript index — and each gets a mapping of its own +// (`mapped`), backed only as far as it is used. var harness_mem: tree.Harness = undefined; -var runner_mem: Runner = undefined; + +/// A `T` in its own anonymous `MAP_NORESERVE` mapping: address space until +/// written, and never written just to initialize it (a Debug build fills +/// an `undefined` global with 0xaa, which for these is hundreds of MB). +fn mapped(comptime T: type) error{OutOfMemory}!*T { + const rc = linux.mmap(null, @sizeOf(T), .{ .READ = true, .WRITE = true }, .{ + .TYPE = .PRIVATE, + .ANONYMOUS = true, + .NORESERVE = true, + }, -1, 0); + if (linux.errno(rc) != .SUCCESS) return error.OutOfMemory; + // Small pages: with transparent huge pages on, touching each + // connection's few header fields would back 2 MB apiece. + _ = linux.madvise(@ptrFromInt(rc), @sizeOf(T), linux.MADV.NOHUGEPAGE); + return @ptrFromInt(rc); +} var stop_requested = std.atomic.Value(bool).init(false); fn onSignal(sig: linux.SIG) callconv(.c) void { @@ -243,6 +261,9 @@ fn run(init: std.process.Init) !void { return error.Usage; } + // The chat indexes live in their own lazily backed mapping: ~100 MB of + // address space, none of it memory until a chat is read. + const chats = try tree.chat.Pool.create(); harness_mem.init(.{ .io = io, .pid = @intCast(linux.getpid()), @@ -253,6 +274,7 @@ fn run(init: std.process.Init) !void { .runtime = post.getenv(envp, "XDG_RUNTIME_DIR") orelse "", .allow_move = allow_move, .envp = envp, + .chats = chats, }); if (allow_move) { std.debug.print("9agents: moves allowed — a write to /active/<h>/<pid>/zmx re-execs that agent under {s}\n", .{zmx_abs}); @@ -273,12 +295,13 @@ fn run(init: std.process.Init) !void { .posted, .unix, .tcp => {}, } - runner_mem.init(.{ .io = io, .root = tree.root, .handler = .{ .ctx = &harness_mem, .serve = serveReq } }); - defer runner_mem.stop(); + const runner = try mapped(Runner); + runner.init(.{ .io = io, .root = tree.root, .handler = .{ .ctx = &harness_mem, .serve = serveReq } }); + defer runner.stop(); var posted_something = false; if (!no_post) { - runner_mem.listenPosted(envp, post_name, 16) catch |err| { + runner.listenPosted(envp, post_name, 16) catch |err| { if (err == error.AlreadyPosted) { std.debug.print("9agents: the name `{s}` is already posted by a live server\n", .{post_name}); return err; @@ -293,7 +316,7 @@ fn run(init: std.process.Init) !void { } switch (mode) { .unix => { - _ = try runner_mem.listen(.{ .unix = try arena.dupeZ(u8, unix_path) }, 16); + _ = try runner.listen(.{ .unix = try arena.dupeZ(u8, unix_path) }, 16); std.debug.print("9agents: listening on {s}\n", .{unix_path}); }, .tcp => { @@ -301,7 +324,7 @@ fn run(init: std.process.Init) !void { std.debug.print("9agents: bad --tcp address: {s}\n", .{tcp_addr}); return error.Usage; }; - const bound = try runner_mem.listen(.{ .tcp = addr }, 16); + const bound = try runner.listen(.{ .tcp = addr }, 16); std.debug.print("9agents: listening on tcp!{f}\n", .{bound}); }, else => {}, @@ -336,7 +359,9 @@ fn serveFd(io: Io, n: i32) !void { var rbuf: [tree.msize]u8 = undefined; var wbuf: [2 * tree.msize]u8 = undefined; var stage: [tree.msize]u8 = undefined; - var engine = Engine.init(.{ .in = &in, .out = &out, .root = tree.root }); + // Far too big for the stack at this fid capacity. + const engine = try mapped(Engine); + engine.initIn(.{ .in = &in, .out = &out, .root = tree.root }); var reader = stream.reader(io, &rbuf); var writer = stream.writer(io, &wbuf); while (true) { |
