summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 04:58:30 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commit9126dbe7a09d463c4eac99b8cc56b14a033b291a (patch)
tree3266afcdd6515536335f597d67f5252eef00412b
parent7f29aaf32835547ec58fc3fe0c00376f00e9eab9 (diff)
downloadpardes-9126dbe7a09d463c4eac99b8cc56b14a033b291a.tar.gz
pardes-9126dbe7a09d463c4eac99b8cc56b14a033b291a.zip
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 <[email protected]>
-rw-r--r--.agents/skills/pardes-9p/SKILL.md3
-rw-r--r--docs/fs.md5
-rw-r--r--src/ninep/events.zig22
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/<n>/pty/run write one line, read `exit N` + its output, or `busy` / `e
$m/log recent events, then EOF: new|del|rename|save <serial> <name>, msg <serial|-> <text>,
run <serial> <line>, exit <serial> <N|?>, send <from> <to> <repl-id>,
dump|restore <path>, err <serial|-> <file>: <why>
- (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 });
+}