From 9126dbe7a09d463c4eac99b8cc56b14a033b291a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 04:58:30 -0300 Subject: follow new: a log follower that starts at the end A follower read the ring's whole history before anything new, and there was no way to skip it. Writing follow new instead of follow skips what the open froze and waits for what comes after (tail -n0 -f); fs.md and the skill say that follow replays first. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/pardes-9p/SKILL.md | 3 ++- docs/fs.md | 5 ++++- src/ninep/events.zig | 22 +++++++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.agents/skills/pardes-9p/SKILL.md b/.agents/skills/pardes-9p/SKILL.md index e363b7cb..f0ae51e4 100644 --- a/.agents/skills/pardes-9p/SKILL.md +++ b/.agents/skills/pardes-9p/SKILL.md @@ -65,7 +65,8 @@ $m/pane//pty/run write one line, read `exit N` + its output, or `busy` / `e $m/log recent events, then EOF: new|del|rename|save , msg , run , exit , send , dump|restore , err : - (exec 3<>$m/log; echo follow >&3; cat <&3 waits for new ones; tail -f does not; + (exec 3<>$m/log; echo follow >&3; cat <&3 replays what is there, then waits for new + ones; `echo follow new` waits for new ones only; tail -f does not wait; while read -r line <&3; do ...; done loses nothing within a session: a Restore hangs every connection up, so dial again, and restart a 9ns mount) $m/screen the rendered screen as JSON, frozen per open diff --git a/docs/fs.md b/docs/fs.md index 04cef2f9..7c13d223 100644 --- a/docs/fs.md +++ b/docs/fs.md @@ -721,7 +721,10 @@ text, a cluster to a cell, is apart from this.) A click in a tag gives offsets into the whole tag as `tag` reads it, the path first.) An open freezes the ring's text the way `/screen` freezes a frame: reads walk it and end. Writing `follow` to that same open makes reads past it wait for the -next record, one per read; a follower the ring outran reads `lost N` first. +next record, one per read, after first reading all that the open froze (the +ring's whole history, up to 64 KiB); `follow new` skips that and waits for +what comes after, as `tail -n0 -f` does. A follower the ring outran reads +`lost N` first. Closing the open is the only way back, as with rio's `consctl`. A follower misses nothing within a session only: a Restore hangs its connection up (and a 9ns mount with it, which must be restarted), so it dials again and diff --git a/src/ninep/events.zig b/src/ninep/events.zig index a3f8c278..1067b60e 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -371,7 +371,12 @@ pub fn readLog(p: *Pardes, req: Req) Reply { /// records instead of ending; closing it is the way back. pub fn writeLog(p: *Pardes, req: Req) Reply { const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.log; - if (!std.mem.eql(u8, std.mem.trim(u8, req.data, " \t\r\n"), "follow")) return Reply.fail(req.tag, E.INVAL); + const word = std.mem.trim(u8, req.data, " \t\r\n"); + // `follow` reads what the open froze, then waits; `follow new` skips + // what was there and waits for what comes after, as tail -n0 -f does. + if (std.mem.eql(u8, word, "follow new")) { + slot.pos = slot.bytes.len; + } else if (!std.mem.eql(u8, word, "follow")) return Reply.fail(req.tag, E.INVAL); slot.follow = true; return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } @@ -1110,3 +1115,18 @@ test "a click in the body reports the offsets of the word it takes, so the recor _ = call(p, .{ .tag = 3, .op = .release, .node = event, .handle = h }); } } + +test "follow new skips what the log held and waits for what comes after" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + p.update(.tick); // the pane's `new` is logged first + while (p.nextEffect()) |_| {} + p.setMessage(0, "before the open"); + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = log, .handle = h, .data = "follow new\n" }).reply.status); + try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = log, .handle = h, .size = 4096 }).reply.status); + p.setMessage(0, "after it"); + try testing.expect(std.mem.endsWith(u8, call(p, .{ .tag = 4, .op = .read, .node = log, .handle = h, .size = 4096 }).bytes, " after it\n")); + _ = call(p, .{ .tag = 5, .op = .release, .node = log, .handle = h }); +} -- cgit v1.3