summaryrefslogtreecommitdiff
path: root/9agents/src/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-25 16:40:49 -0300
committerGabriel Schneider <[email protected]>2026-09-25 17:52:33 -0300
commit1c1b192d4ef59199a7196229d56901b1f6512678 (patch)
tree9f6e4c1efcf877d1231aa58975ccd923ad4f2f2e /9agents/src/main.zig
parent73602127d15d10a1932b6fe916bd18a608054980 (diff)
downloadcloud9-main.tar.gz
cloud9-main.zip
9agents: /active/<h>/<pid>/chat, the conversation as one file per messageHEADmain
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.zig43
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) {