summaryrefslogtreecommitdiff
path: root/9agents/src/chat.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/chat.zig
parent73602127d15d10a1932b6fe916bd18a608054980 (diff)
downloadcloud9-1c1b192d4ef59199a7196229d56901b1f6512678.tar.gz
cloud9-1c1b192d4ef59199a7196229d56901b1f6512678.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/chat.zig')
-rw-r--r--9agents/src/chat.zig1626
1 files changed, 1626 insertions, 0 deletions
diff --git a/9agents/src/chat.zig b/9agents/src/chat.zig
new file mode 100644
index 0000000..d6b4773
--- /dev/null
+++ b/9agents/src/chat.zig
@@ -0,0 +1,1626 @@
+//! The chat behind `/active/<harness>/<pid>/chat/`: a harness transcript
+//! read as numbered messages, one file each.
+//!
+//! chat/00000000-user 00000001-assistant 00000002-bash 00000003-bash-result ...
+//!
+//! The name is `<id>-<kind>`. The id is the message's position in the
+//! transcript, counted from 0 and written as 8 digits so the names sort
+//! in order; transcripts are append-only, so an id never moves and a new
+//! message takes the next one. The kind says who spoke, normalized across
+//! harnesses (`Kind`); a tool call is named after its tool and its result
+//! after the call (`Chat.label`). The file holds the message rendered as
+//! text: JSON strings unescaped, and each part on its own line — a tool
+//! call is its name, then its input.
+//!
+//! A transcript is JSONL, and finding message N means knowing where every
+//! message before it starts, so each transcript gets an index: per message,
+//! the spans of the file that render it (`Span`). The index is not a copy
+//! of anything. It holds offsets, never bytes, and every read still reads
+//! the file. It is keyed by the file's (dev, ino) and extended from where it
+//! stopped when the file grows; a file that shrinks or is replaced starts
+//! over. Only complete lines are indexed, so a record the harness is
+//! half-way through writing waits for its newline.
+//!
+//! The caps are loud, as everywhere else in 9agents: a transcript with more
+//! messages than an index holds is marked `full`, and the listing refuses
+//! rather than end early.
+const std = @import("std");
+const Io = std.Io;
+const linux = std.os.linux;
+
+// ---- comptime bounds ---------------------------------------------------------
+
+/// Messages one index holds. The longest codex rollout on this machine
+/// (49k records) is 32k messages; Claude Code sessions stay near 3k. The
+/// pool is mapped `MAP_NORESERVE` (`Pool.create`), so a generous cap costs
+/// address space, not memory: a page is only backed once an index uses it.
+pub const max_msgs: usize = 1 << 18;
+/// Spans one index holds: most messages are one span, a tool call two.
+pub const max_spans: usize = 2 * max_msgs;
+/// Digits in a message id: every id has the same width, so a plain sort
+/// of the names is the order the messages were said in.
+pub const id_digits: usize = 8;
+comptime {
+ std.debug.assert(max_msgs <= 100_000_000); // every id fits the width
+}
+/// Transcripts indexed at once, reused least-recently-used first.
+pub const chat_slots: usize = 8;
+/// The read window over a transcript.
+pub const window: usize = 64 * 1024;
+/// The window always shows at least this much past the position asked for,
+/// unless the file ends first: enough for any key compared and for the
+/// longest JSON escape (a surrogate pair, 12 bytes).
+const lookahead: usize = 64;
+
+/// The transcript formats this module reads.
+pub const Format = enum(u8) { claude, codex };
+
+/// Who spoke. The name of a message file ends in `text()`.
+pub const Kind = enum(u8) {
+ /// A person's prompt.
+ user,
+ /// The model's reply text.
+ assistant,
+ /// The model's reasoning, where the harness writes it in the clear.
+ thinking,
+ /// A tool the model called: its name, then its input. Named after
+ /// the tool when it has a usable name; `tool-call` when not.
+ tool_call,
+ /// What the tool answered. Named `<tool>-result` when its call is
+ /// known; `tool-result` when not.
+ tool_result,
+ /// The harness speaking: developer instructions, slash-command
+ /// output, injected context, compaction summaries.
+ system,
+ /// Another agent of a team (codex): its name, then what it said.
+ agent,
+
+ pub fn text(k: Kind) []const u8 {
+ return switch (k) {
+ .tool_call => "tool-call",
+ .tool_result => "tool-result",
+ else => @tagName(k),
+ };
+ }
+
+ pub fn fromText(s: []const u8) ?Kind {
+ for (std.enums.values(Kind)) |k| if (std.mem.eql(u8, k.text(), s)) return k;
+ return null;
+ }
+};
+
+/// How a span's bytes become text.
+pub const Enc = enum(u2) {
+ /// The bytes as they are (a JSON value served verbatim).
+ raw,
+ /// The body of a JSON string, unescaped.
+ str,
+ /// A fixed text from `literals`; `off` is its index.
+ lit,
+};
+
+/// Texts for what a transcript holds that is not text.
+const literals = [_][]const u8{ "[image]", "" };
+const lit_image: u64 = 0;
+const lit_empty: u64 = 1;
+
+/// One stretch of the transcript that renders part of a message.
+/// Packed to 16 bytes: an index is mostly these.
+pub const Span = packed struct(u128) {
+ /// File offset of the first byte; for `lit`, an index into `literals`.
+ off: u48,
+ /// Bytes in the file.
+ len: u32,
+ /// Bytes it renders to, not counting `nl`.
+ out: u32,
+ enc: Enc,
+ /// A newline follows, because the text does not end in one.
+ nl: bool,
+ _: u13 = 0,
+
+ fn size(s: Span) u64 {
+ return @as(u64, s.out) + @intFromBool(s.nl);
+ }
+};
+
+pub const Msg = struct {
+ /// Index of the first span in `Chat.spans`.
+ first: u32,
+ count: u16,
+ kind: Kind,
+ /// Unix seconds of the record, 0 when it carries no timestamp.
+ time: u32,
+ /// Rendered size: the sum of the spans'.
+ size: u32,
+ /// For a tool call or result, the tool, as an index into the chat's
+ /// tool names plus one; 0 when unknown.
+ tool: u8 = 0,
+};
+
+/// Distinct tool names one chat names its calls by. Past this, a call is
+/// named `tool-call`, as one whose tool has no usable name is.
+pub const max_tools: usize = 255;
+/// Longest tool name kept for a file name.
+pub const tool_name_cap: usize = 48;
+/// Calls remembered for matching their results: results follow their
+/// calls closely, a few dozen at most when calls run in parallel.
+const call_ring: usize = 256;
+
+const Call = struct {
+ /// Hash of the call's id (never 0 when set).
+ id: u64 = 0,
+ tool: u8 = 0,
+};
+
+/// The index of one transcript.
+pub const Chat = struct {
+ used: bool = false,
+ format: Format = .claude,
+ dev: u64 = 0,
+ ino: u64 = 0,
+ /// The file's size when last synced; reads never go past it.
+ size: u64 = 0,
+ /// Offset of the first line not yet indexed. Always a line start.
+ scanned: u64 = 0,
+ /// Least-recently-used ordering.
+ stamp: u64 = 0,
+ /// A cap was reached. Messages past `n` exist but are not indexed.
+ full: bool = false,
+ /// When the file was created, where the filesystem says: a new file
+ /// that reuses a deleted one's inode is born later.
+ btime: i128 = 0,
+ /// Hashes of the first and last bytes indexed (up to `probe_len` each,
+ /// the last ending at `scanned`). A transcript rewritten in place to
+ /// the same size or longer keeps its inode and grows past `scanned`;
+ /// only its bytes can say it is no longer the file that was indexed.
+ head: u64 = 0,
+ tail: u64 = 0,
+ /// Bumped by every reset, so a read cursor cannot outlive its index.
+ epoch: u32 = 0,
+ /// Offset up to which the unindexed tail is known to hold no newline.
+ unterminated: u64 = 0,
+ n: u32 = 0,
+ nspans: u32 = 0,
+ ntools: u8 = 0,
+ call_next: u16 = 0,
+ tool_lens: [max_tools]u8,
+ tool_names: [max_tools][tool_name_cap]u8,
+ calls: [call_ring]Call,
+ msgs: [max_msgs]Msg,
+ spans: [max_spans]Span,
+
+ fn reset(c: *Chat, format: Format, dev: u64, ino: u64) void {
+ c.used = true;
+ c.epoch +%= 1;
+ c.btime = 0;
+ c.head = 0;
+ c.tail = 0;
+ c.format = format;
+ c.dev = dev;
+ c.ino = ino;
+ c.size = 0;
+ c.scanned = 0;
+ c.full = false;
+ c.unterminated = 0;
+ c.n = 0;
+ c.nspans = 0;
+ c.ntools = 0;
+ c.call_next = 0;
+ c.calls = @splat(.{});
+ }
+
+ pub fn msg(c: *const Chat, i: u32) ?Msg {
+ return if (i < c.n) c.msgs[i] else null;
+ }
+
+ /// What a message's name says after its id: its kind — or, for a
+ /// tool call, the tool, and for its result the tool and `-result`.
+ fn label(c: *const Chat, m: Msg) struct { []const u8, []const u8 } {
+ if (m.tool == 0) return .{ m.kind.text(), "" };
+ const t = c.tool_names[m.tool - 1][0..c.tool_lens[m.tool - 1]];
+ return .{ t, if (m.kind == .tool_result) "-result" else "" };
+ }
+
+ /// `<id>-<label>`, the id as `id_digits` digits, written into `buf`.
+ pub fn name(c: *const Chat, i: u32, buf: []u8) ?[]const u8 {
+ const m = c.msg(i) orelse return null;
+ const l = c.label(m);
+ return std.fmt.bufPrint(buf, "{d:0>8}-{s}{s}", .{ i, l[0], l[1] }) catch null;
+ }
+
+ /// The message a file name names: `<id>-<label>` with the id in its
+ /// one spelling (exactly `id_digits` digits) and the label it really
+ /// has, so every message answers to exactly one name.
+ pub fn lookup(c: *const Chat, file_name: []const u8) ?u32 {
+ if (file_name.len <= id_digits or file_name[id_digits] != '-') return null;
+ const digits = file_name[0..id_digits];
+ for (digits) |d| if (!std.ascii.isDigit(d)) return null;
+ const i = std.fmt.parseInt(u32, digits, 10) catch return null;
+ const m = c.msg(i) orelse return null;
+ const rest = file_name[id_digits + 1 ..];
+ const l = c.label(m);
+ if (rest.len != l[0].len + l[1].len) return null;
+ if (!std.mem.startsWith(u8, rest, l[0]) or !std.mem.endsWith(u8, rest, l[1])) return null;
+ return i;
+ }
+
+ /// The index (plus one) a tool name is kept under, adding it if new;
+ /// 0 when it has no usable name. Lowercased, and anything but letters,
+ /// digits, `_`, `.` and `-` becomes `-`, so it is safe in a file name.
+ /// A name that would read as another kind, or as a result, is not used.
+ fn internTool(c: *Chat, raw: []const u8) u8 {
+ var buf: [tool_name_cap]u8 = undefined;
+ const n = @min(raw.len, buf.len);
+ if (n == 0) return 0;
+ for (raw[0..n], buf[0..n]) |ch, *o| o.* = switch (ch) {
+ 'A'...'Z' => ch + ('a' - 'A'),
+ 'a'...'z', '0'...'9', '_', '.', '-' => ch,
+ else => '-',
+ };
+ const t = buf[0..n];
+ if (Kind.fromText(t) != null or std.mem.endsWith(u8, t, "-result")) return 0;
+ for (0..c.ntools) |i| {
+ if (std.mem.eql(u8, c.tool_names[i][0..c.tool_lens[i]], t)) return @intCast(i + 1);
+ }
+ if (c.ntools == max_tools) return 0;
+ @memcpy(c.tool_names[c.ntools][0..n], t);
+ c.tool_lens[c.ntools] = @intCast(n);
+ c.ntools += 1;
+ return c.ntools;
+ }
+
+ /// Remembers which tool a call went to, for its result to find.
+ fn rememberCall(c: *Chat, id: u64, tool: u8) void {
+ if (id == 0 or tool == 0) return;
+ c.calls[c.call_next] = .{ .id = id, .tool = tool };
+ c.call_next = @intCast((c.call_next + 1) % call_ring);
+ }
+
+ /// The tool a result answers, from its call's id; 0 when not known.
+ fn toolOfCall(c: *const Chat, id: u64) u8 {
+ if (id == 0) return 0;
+ for (c.calls) |k| if (k.id == id) return k.tool;
+ return 0;
+ }
+
+ // -- building --
+
+ fn open(c: *Chat, kind: Kind, time: u32) bool {
+ if (c.n == max_msgs) return false;
+ c.msgs[c.n] = .{ .first = c.nspans, .count = 0, .kind = kind, .time = time, .size = 0 };
+ return true;
+ }
+
+ fn add(c: *Chat, s: Span) bool {
+ const m = &c.msgs[c.n];
+ if (c.nspans == max_spans or m.count == std.math.maxInt(u16)) return false;
+ const size = std.math.add(u32, m.size, @intCast(s.size())) catch return false;
+ c.spans[c.nspans] = s;
+ c.nspans += 1;
+ m.count += 1;
+ m.size = size;
+ return true;
+ }
+
+ /// Keeps the message being built if it got a span, drops it if not.
+ fn close(c: *Chat) void {
+ const m = &c.msgs[c.n];
+ if (m.count > 0) c.n += 1 else c.nspans = m.first;
+ }
+};
+
+/// The indexes, and the window they read through. One instance serves the
+/// daemon; the caller serializes access (the harness mutex).
+pub const Pool = struct {
+ chats: [chat_slots]Chat,
+ clock: u64,
+ /// Where the last read of a long string stopped (see `render`).
+ cursor: Cursor,
+ win: [window]u8,
+
+ /// A pool in its own anonymous mapping, `MAP_NORESERVE`: the ~100 MB
+ /// of tables are address space until an index writes them, and no
+ /// part of it is ever written just to initialize it.
+ pub fn create() error{OutOfMemory}!*Pool {
+ const rc = linux.mmap(null, @sizeOf(Pool), .{ .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, the first write to
+ // each chat's header would back a whole 2 MB page of its tables.
+ _ = linux.madvise(@ptrFromInt(rc), @sizeOf(Pool), linux.MADV.NOHUGEPAGE);
+ const p: *Pool = @ptrFromInt(rc);
+ p.init();
+ return p;
+ }
+
+ pub fn destroy(p: *Pool) void {
+ _ = linux.munmap(@ptrCast(p), @sizeOf(Pool));
+ }
+
+ /// Resets the headers only: the tables are written as they fill.
+ pub fn init(p: *Pool) void {
+ p.clock = 0;
+ p.cursor = .{};
+ for (&p.chats) |*c| {
+ c.used = false;
+ c.n = 0;
+ c.nspans = 0;
+ c.epoch = 0;
+ }
+ }
+
+ /// The index of the transcript open as `file`, brought up to date with
+ /// it. Null when the file is not a regular file.
+ pub fn sync(p: *Pool, io: Io, file: Io.File, format: Format) ?*Chat {
+ var sx: linux.Statx = undefined;
+ const want: linux.STATX = .{ .TYPE = true, .INO = true, .SIZE = true, .BTIME = true };
+ const rc = linux.statx(file.handle, "", linux.AT.EMPTY_PATH, want, &sx);
+ if (linux.errno(rc) != .SUCCESS) return null;
+ if (sx.mode & linux.S.IFMT != linux.S.IFREG) return null;
+ const dev = (@as(u64, sx.dev_major) << 32) | sx.dev_minor;
+ const btime: i128 = if (sx.mask.BTIME) @as(i128, sx.btime.sec) * std.time.ns_per_s + sx.btime.nsec else 0;
+
+ p.clock += 1;
+ const c = p.find(format, dev, sx.ino);
+ // A transcript only grows, and keeps the bytes it had. Anything
+ // else — shorter, born again under a reused inode, or rewritten in
+ // place — is another file, and the offsets mean nothing in it.
+ if (sx.size < c.scanned or c.btime != btime or !p.unchanged(io, file, c)) {
+ c.reset(format, dev, sx.ino);
+ }
+ c.btime = btime;
+ c.stamp = p.clock;
+ c.size = sx.size;
+ if (c.scanned < sx.size and !c.full) {
+ p.catchUp(io, file, c);
+ const pr = p.probe(io, file, c.scanned);
+ c.head = pr.head;
+ c.tail = pr.tail;
+ }
+ return c;
+ }
+
+ const probe_len: usize = 64;
+
+ /// Hashes of the first and last `probe_len` bytes before `end`.
+ fn probe(p: *Pool, io: Io, file: Io.File, end: u64) struct { head: u64, tail: u64 } {
+ _ = p;
+ if (end == 0) return .{ .head = 0, .tail = 0 };
+ var buf: [probe_len]u8 = undefined;
+ const k: usize = @intCast(@min(end, probe_len));
+ const hn = file.readPositionalAll(io, buf[0..k], 0) catch 0;
+ const head = std.hash.Wyhash.hash(0, buf[0..hn]);
+ const tn = file.readPositionalAll(io, buf[0..k], end - k) catch 0;
+ const tail = std.hash.Wyhash.hash(1, buf[0..tn]);
+ return .{ .head = head, .tail = tail };
+ }
+
+ fn unchanged(p: *Pool, io: Io, file: Io.File, c: *const Chat) bool {
+ if (c.scanned == 0) return true;
+ const pr = p.probe(io, file, c.scanned);
+ return pr.head == c.head and pr.tail == c.tail;
+ }
+
+ fn find(p: *Pool, format: Format, dev: u64, ino: u64) *Chat {
+ var victim = &p.chats[0];
+ for (&p.chats) |*c| {
+ if (c.used and c.format == format and c.dev == dev and c.ino == ino) return c;
+ if (!c.used) {
+ if (victim.used) victim = c;
+ } else if (victim.used and c.stamp < victim.stamp) victim = c;
+ }
+ victim.reset(format, dev, ino);
+ return victim;
+ }
+
+ fn catchUp(p: *Pool, io: Io, file: Io.File, c: *Chat) void {
+ var src: Src = .{ .io = io, .file = file, .end = c.size, .buf = &p.win };
+ var pos = c.scanned;
+ while (pos < c.size) {
+ // A line without its newline is still being written.
+ // A long unterminated tail is searched once, not per request.
+ const eol = src.find(@max(pos, c.unterminated), '\n') orelse {
+ c.unterminated = c.size;
+ break;
+ };
+ const mark_n = c.n;
+ const mark_s = c.nspans;
+ var ps: P = .{ .src = &src, .lim = eol };
+ const ok = switch (c.format) {
+ .claude => claudeLine(c, &ps, pos),
+ .codex => codexLine(c, &ps, pos),
+ };
+ if (!ok) {
+ // A cap: this line is left whole for nobody, and the
+ // listing says so instead of ending early.
+ c.n = mark_n;
+ c.nspans = mark_s;
+ c.full = true;
+ break;
+ }
+ pos = eol + 1;
+ }
+ c.scanned = pos;
+ }
+
+ /// Renders message `i` from byte `off` into `out`, reading `file`.
+ /// Returns the bytes written; fewer than asked only at the end.
+ ///
+ /// A string's output offset does not map onto its input offset, so a
+ /// read deep into a long one would decode everything before it, and a
+ /// client reading a message front to back would pay for it again on
+ /// every read. So the pool remembers where the last read stopped — the
+ /// input offset of the last escape or run it started and the output
+ /// offset that produced — and a read continuing the same message
+ /// starts from there.
+ pub fn render(p: *Pool, io: Io, file: Io.File, c: *const Chat, i: u32, off: u64, out: []u8) usize {
+ const m = c.msg(i) orelse return 0;
+ var src: Src = .{ .io = io, .file = file, .end = c.size, .buf = &p.win };
+ var at: u64 = 0;
+ var n: usize = 0;
+ for (c.spans[m.first..][0..m.count], m.first..) |s, k| {
+ if (n == out.len) break;
+ const size = s.size();
+ defer at += size;
+ if (at + size <= off) continue;
+ var sink: Sink = .{ .skip = off -| at, .out = out[n..] };
+ switch (s.enc) {
+ .lit => sink.put(literals[@intCast(s.off)]),
+ .raw => {
+ // One byte in, one byte out: start at the offset asked.
+ const from = @min(sink.skip, s.len);
+ sink.skip -= from;
+ copy(&src, @as(u64, s.off) + from, s.len - from, &sink);
+ },
+ .str => {
+ const cur = p.cursor;
+ var from: u64 = s.off;
+ var base: u64 = 0; // output offset `from` produces
+ if (cur.chat == c and cur.epoch == c.epoch and cur.span == k and cur.out <= sink.skip) {
+ from = cur.in;
+ base = cur.out;
+ sink.skip -= cur.out;
+ }
+ sink.mark_in = from;
+ decodeSpan(&src, from, @as(u64, s.off) + s.len - from, &sink);
+ if (sink.done()) p.cursor = .{
+ .chat = c,
+ .epoch = c.epoch,
+ .span = @intCast(k),
+ .in = sink.mark_in,
+ .out = base + sink.mark_out,
+ };
+ },
+ }
+ if (s.nl) sink.put("\n");
+ n += sink.n;
+ }
+ return n;
+ }
+};
+
+// ---- reading the file through a window ----------------------------------------
+
+const Src = struct {
+ io: Io,
+ file: Io.File,
+ /// Nothing at or past this offset is read.
+ end: u64,
+ buf: []u8,
+ base: u64 = 0,
+ len: usize = 0,
+
+ /// The bytes from `pos` on, at least `lookahead` of them unless the
+ /// file ends first. Empty at the end, or when the file was cut short
+ /// under the read.
+ fn at(s: *Src, pos: u64) []const u8 {
+ if (pos >= s.end) return &.{};
+ const have_end = s.base + s.len;
+ const inside = pos >= s.base and pos < have_end;
+ if (!inside or (have_end - pos < lookahead and have_end < s.end)) s.load(pos);
+ if (pos < s.base or pos >= s.base + s.len) return &.{};
+ return s.buf[@intCast(pos - s.base)..s.len];
+ }
+
+ fn load(s: *Src, pos: u64) void {
+ const want: usize = @intCast(@min(s.buf.len, s.end - pos));
+ s.base = pos;
+ s.len = s.file.readPositionalAll(s.io, s.buf[0..want], pos) catch 0;
+ }
+
+ /// The offset of the next `byte` at or after `pos`.
+ fn find(s: *Src, pos: u64, byte: u8) ?u64 {
+ var q = pos;
+ while (true) {
+ const b = s.at(q);
+ if (b.len == 0) return null;
+ if (std.mem.indexOfScalar(u8, b, byte)) |i| return q + i;
+ q += b.len;
+ }
+ }
+};
+
+/// Where the last read of a string stopped: decoding span `span` from
+/// file offset `in` produces its output from offset `out` on.
+const Cursor = struct {
+ chat: ?*const Chat = null,
+ epoch: u32 = 0,
+ span: u32 = 0,
+ in: u64 = 0,
+ out: u64 = 0,
+};
+
+/// Where rendered bytes go: `skip` of them are dropped, then `out` fills.
+/// `total` and `last` see every byte, which is how a span is measured.
+const Sink = struct {
+ skip: u64 = 0,
+ out: []u8 = &.{},
+ n: usize = 0,
+ total: u64 = 0,
+ last: u8 = '\n',
+ /// Measuring: nothing is written, everything is counted.
+ measuring: bool = false,
+ /// The input offset of the last run or escape begun before `out`
+ /// filled, and the `total` it began at: where a later read resumes.
+ mark_in: u64 = 0,
+ mark_out: u64 = 0,
+
+ fn mark(s: *Sink, pos: u64) void {
+ if (s.done()) return;
+ s.mark_in = pos;
+ s.mark_out = s.total;
+ }
+
+ fn put(s: *Sink, bytes: []const u8) void {
+ if (bytes.len == 0) return;
+ s.total += bytes.len;
+ s.last = bytes[bytes.len - 1];
+ var b = bytes;
+ if (s.skip > 0) {
+ const d: usize = @intCast(@min(s.skip, b.len));
+ s.skip -= d;
+ b = b[d..];
+ }
+ const k = @min(s.out.len - s.n, b.len);
+ @memcpy(s.out[s.n..][0..k], b[0..k]);
+ s.n += k;
+ }
+
+ fn done(s: *const Sink) bool {
+ return !s.measuring and s.n == s.out.len;
+ }
+};
+
+fn copy(src: *Src, off: u64, len: u64, sink: *Sink) void {
+ var pos = off;
+ const end = off + len;
+ while (pos < end and !sink.done()) {
+ const b = src.at(pos);
+ if (b.len == 0) return;
+ const chunk = b[0..@intCast(@min(b.len, end - pos))];
+ sink.put(chunk);
+ pos += chunk.len;
+ }
+}
+
+// ---- JSON strings -----------------------------------------------------------
+
+/// Unescapes the body of a JSON string held in `[off, off+len)`.
+fn decodeSpan(src: *Src, off: u64, len: u64, sink: *Sink) void {
+ var pos = off;
+ const end = off + len;
+ while (pos < end and !sink.done()) {
+ const b = src.at(pos);
+ if (b.len == 0) return;
+ const chunk = b[0..@intCast(@min(b.len, end - pos))];
+ const k = decode(chunk, pos, pos + chunk.len == end, sink);
+ if (k == 0) return; // cannot happen with `lookahead`; never spin
+ pos += k;
+ }
+}
+
+/// Unescapes as much of `chunk` (at file offset `at`) as it can,
+/// returning the bytes consumed. An escape cut by the chunk's end is left
+/// for the next call unless `final` says there is no more.
+fn decode(chunk: []const u8, at: u64, final: bool, sink: *Sink) usize {
+ var i: usize = 0;
+ while (i < chunk.len) {
+ sink.mark(at + i);
+ const j = std.mem.indexOfScalarPos(u8, chunk, i, '\\') orelse {
+ sink.put(chunk[i..]);
+ return chunk.len;
+ };
+ sink.put(chunk[i..j]);
+ i = j;
+ if (!final and chunk.len - i < 12) return i;
+ sink.mark(at + i);
+ if (i + 1 == chunk.len) {
+ sink.put("\\"); // a lone backslash at the very end
+ return chunk.len;
+ }
+ const e = chunk[i + 1];
+ const simple: ?u8 = switch (e) {
+ 'n' => '\n',
+ 't' => '\t',
+ 'r' => '\r',
+ 'b' => 0x08,
+ 'f' => 0x0c,
+ '"', '\\', '/' => e,
+ else => null,
+ };
+ if (simple) |c| {
+ sink.put(&.{c});
+ i += 2;
+ continue;
+ }
+ if (e != 'u') {
+ sink.put(chunk[i .. i + 2]); // not an escape JSON has: as written
+ i += 2;
+ continue;
+ }
+ const hi = hex4(chunk[i + 2 ..]) orelse {
+ sink.put(chunk[i .. i + 2]);
+ i += 2;
+ continue;
+ };
+ i += 6;
+ var cp: u21 = hi;
+ if (hi >= 0xd800 and hi < 0xdc00) {
+ // A high surrogate wants its low half next, as `\uDCxx`.
+ const rest = chunk[i..];
+ const lo: ?u16 = if (rest.len >= 6 and rest[0] == '\\' and rest[1] == 'u') hex4(rest[2..]) else null;
+ if (lo) |l| if (l >= 0xdc00 and l < 0xe000) {
+ cp = 0x10000 + ((@as(u21, hi) - 0xd800) << 10) + (l - 0xdc00);
+ i += 6;
+ };
+ if (cp == hi) cp = 0xfffd;
+ } else if (hi >= 0xdc00 and hi < 0xe000) cp = 0xfffd;
+ var utf: [4]u8 = undefined;
+ const n = std.unicode.utf8Encode(cp, &utf) catch 0;
+ sink.put(utf[0..n]);
+ }
+ return i;
+}
+
+/// Four hex digits exactly: `parseInt` would also take a sign or `_`.
+fn hex4(b: []const u8) ?u16 {
+ if (b.len < 4) return null;
+ var v: u16 = 0;
+ for (b[0..4]) |c| v = (v << 4) | (std.fmt.charToDigit(c, 16) catch return null);
+ return v;
+}
+
+// ---- a JSON walker over the window ------------------------------------------
+
+/// Enough of a JSON parser to find values by key and know where each one
+/// starts and ends. Positions are file offsets; `lim` is the line's end.
+/// It checks nesting, not grammar: a line it cannot walk yields nothing.
+const P = struct {
+ src: *Src,
+ lim: u64,
+
+ fn peek(p: *P, pos: u64) ?u8 {
+ if (pos >= p.lim) return null;
+ const b = p.src.at(pos);
+ return if (b.len == 0) null else b[0];
+ }
+
+ /// The window from `pos`, cut at the line's end.
+ fn bytes(p: *P, pos: u64) []const u8 {
+ if (pos >= p.lim) return &.{};
+ const b = p.src.at(pos);
+ return b[0..@intCast(@min(b.len, p.lim - pos))];
+ }
+
+ fn ws(p: *P, pos: u64) u64 {
+ var q = pos;
+ while (p.peek(q)) |c| switch (c) {
+ ' ', '\t', '\r', '\n' => q += 1,
+ else => break,
+ };
+ return q;
+ }
+
+ /// `pos` at an opening quote; the offset just past the closing one.
+ /// Most of a transcript's bytes are inside strings (tool output,
+ /// encrypted reasoning), so this is the hot loop: it jumps from quote
+ /// to quote, and a quote closes the string when the run of
+ /// backslashes before it is even.
+ fn strEnd(p: *P, pos: u64) ?u64 {
+ var q = pos + 1;
+ // Backslashes ending the bytes before `q`, carried across windows.
+ var run: usize = 0;
+ while (true) {
+ const b = p.bytes(q);
+ if (b.len == 0) return null;
+ var from: usize = 0;
+ while (std.mem.indexOfScalarPos(u8, b, from, '"')) |i| {
+ var k = i;
+ while (k > from and b[k - 1] == '\\') k -= 1;
+ const slashes = (i - k) + (if (k == 0) run else 0);
+ if (slashes % 2 == 0) return q + i + 1;
+ from = i + 1;
+ run = 0;
+ }
+ var t = b.len;
+ while (t > from and b[t - 1] == '\\') t -= 1;
+ run = (b.len - t) + (if (t == 0) run else 0);
+ q += b.len;
+ }
+ }
+
+ /// The offset just past the value at `pos`.
+ fn skip(p: *P, pos: u64) ?u64 {
+ const c = p.peek(pos) orelse return null;
+ switch (c) {
+ '"' => return p.strEnd(pos),
+ '{', '[' => {
+ var depth: usize = 0;
+ var q = pos;
+ while (true) {
+ const b = p.bytes(q);
+ if (b.len == 0) return null;
+ const i = std.mem.indexOfAny(u8, b, "\"{}[]") orelse {
+ q += b.len;
+ continue;
+ };
+ q += i;
+ switch (b[i]) {
+ '"' => q = p.strEnd(q) orelse return null,
+ '{', '[' => {
+ depth += 1;
+ q += 1;
+ },
+ else => {
+ if (depth == 0) return null;
+ depth -= 1;
+ q += 1;
+ if (depth == 0) return q;
+ },
+ }
+ }
+ },
+ else => {
+ var q = pos;
+ while (p.peek(q)) |d| switch (d) {
+ ',', '}', ']', ' ', '\t', '\r', '\n' => break,
+ else => q += 1,
+ };
+ return if (q == pos) null else q;
+ },
+ }
+ }
+
+ fn isString(p: *P, pos: u64) bool {
+ return p.peek(pos) == '"';
+ }
+
+ /// A short string value without escapes, copied into `buf`: the
+ /// `type`s, roles and timestamps the walk steers by.
+ fn word(p: *P, pos: ?u64, buf: []u8) []const u8 {
+ const at = pos orelse return "";
+ if (!p.isString(at)) return "";
+ const end = p.strEnd(at) orelse return "";
+ const len = end - at - 2;
+ if (len > buf.len or len > lookahead) return "";
+ const b = p.bytes(at + 1);
+ if (b.len < len) return "";
+ const s = b[0..@intCast(len)];
+ if (std.mem.indexOfScalar(u8, s, '\\') != null) return "";
+ @memcpy(buf[0..s.len], s);
+ return buf[0..s.len];
+ }
+
+ fn isTrue(p: *P, pos: ?u64) bool {
+ const at = pos orelse return false;
+ const b = p.bytes(at);
+ if (!std.mem.startsWith(u8, b, "true")) return false;
+ return b.len == 4 or !std.ascii.isAlphanumeric(b[4]);
+ }
+
+ /// Elements of the array at `pos` (or members of an object), in order.
+ fn items(p: *P, pos: ?u64) Items {
+ const at = pos orelse return .{ .p = p, .pos = 0, .done = true };
+ const open = p.peek(at) orelse return .{ .p = p, .pos = 0, .done = true };
+ if (open != '[' and open != '{') return .{ .p = p, .pos = 0, .done = true };
+ return .{ .p = p, .pos = at + 1, .object = open == '{' };
+ }
+
+ /// The keys this module ever asks a record for.
+ const Key = enum {
+ type,
+ role,
+ text,
+ content,
+ name,
+ input,
+ thinking,
+ arguments,
+ output,
+ action,
+ summary,
+ author,
+ message,
+ payload,
+ timestamp,
+ isMeta,
+ isCompactSummary,
+ isSidechain,
+ id,
+ tool_use_id,
+ call_id,
+ attachment,
+ prompt,
+ origin,
+ kind,
+ };
+
+ const Fields = struct {
+ at: [std.enums.values(Key).len]?u64 = @splat(null),
+ bad: bool = false,
+
+ fn get(f: *const Fields, k: Key) ?u64 {
+ return f.at[@intFromEnum(k)];
+ }
+ };
+
+ /// Where each known key's value starts in the object at `pos`. The
+ /// first occurrence wins. `bad` when the object does not walk.
+ fn fields(p: *P, pos: ?u64) Fields {
+ var f: Fields = .{};
+ const at = pos orelse {
+ f.bad = true;
+ return f;
+ };
+ if (p.peek(at) != '{') {
+ f.bad = true;
+ return f;
+ }
+ var it = p.items(at);
+ while (it.next()) |m| {
+ const key = p.bytes(m.key);
+ if (key.len < m.key_len) continue;
+ inline for (comptime std.enums.values(Key)) |k| {
+ if (f.at[@intFromEnum(k)] == null and std.mem.eql(u8, key[0..m.key_len], @tagName(k))) {
+ f.at[@intFromEnum(k)] = m.val;
+ }
+ }
+ }
+ f.bad = it.bad;
+ return f;
+ }
+};
+
+const Item = struct {
+ /// First byte of the key inside its quotes (objects only).
+ key: u64 = 0,
+ key_len: usize = 0,
+ val: u64,
+};
+
+const Items = struct {
+ p: *P,
+ pos: u64,
+ object: bool = false,
+ first: bool = true,
+ done: bool = false,
+ bad: bool = false,
+
+ fn next(it: *Items) ?Item {
+ if (it.done) return null;
+ const p = it.p;
+ var q = p.ws(it.pos);
+ const c = p.peek(q) orelse return it.fail();
+ if (c == (if (it.object) @as(u8, '}') else ']')) {
+ it.done = true;
+ return null;
+ }
+ if (!it.first) {
+ if (c != ',') return it.fail();
+ q = p.ws(q + 1);
+ }
+ it.first = false;
+ var item: Item = .{ .val = q };
+ if (it.object) {
+ if (!p.isString(q)) return it.fail();
+ const kend = p.strEnd(q) orelse return it.fail();
+ item.key = q + 1;
+ item.key_len = @intCast(@min(kend - q - 2, lookahead));
+ q = p.ws(kend);
+ if (p.peek(q) != ':') return it.fail();
+ q = p.ws(q + 1);
+ item.val = q;
+ }
+ it.pos = p.skip(q) orelse return it.fail();
+ return item;
+ }
+
+ fn fail(it: *Items) ?Item {
+ it.done = true;
+ it.bad = true;
+ return null;
+ }
+};
+
+// ---- spans ------------------------------------------------------------------
+
+/// The body of the string at `pos`, measured.
+fn strSpan(p: *P, pos: u64) ?Span {
+ const end = p.strEnd(pos) orelse return null;
+ const len = end - pos - 2;
+ if (len >= std.math.maxInt(u32)) return null;
+ var sink: Sink = .{ .measuring = true };
+ decodeSpan(p.src, pos + 1, len, &sink);
+ if (sink.total >= std.math.maxInt(u32)) return null;
+ return .{
+ .off = @intCast(pos + 1),
+ .len = @intCast(len),
+ .out = @intCast(sink.total),
+ .enc = .str,
+ .nl = sink.total > 0 and sink.last != '\n',
+ };
+}
+
+/// The value at `pos` as it is written.
+fn rawSpan(p: *P, pos: u64) ?Span {
+ const end = p.skip(pos) orelse return null;
+ const len = end - pos;
+ if (len == 0 or len >= std.math.maxInt(u32)) return null;
+ const last = p.src.at(end - 1);
+ return .{
+ .off = @intCast(pos),
+ .len = @intCast(len),
+ .out = @intCast(len),
+ .enc = .raw,
+ .nl = last.len == 0 or last[0] != '\n',
+ };
+}
+
+/// A string as its text, anything else as its JSON.
+fn valueSpan(p: *P, pos: u64) ?Span {
+ return if (p.isString(pos)) strSpan(p, pos) else rawSpan(p, pos);
+}
+
+fn litSpan(which: u64) Span {
+ const t = literals[@intCast(which)];
+ return .{ .off = @intCast(which), .len = 0, .out = @intCast(t.len), .enc = .lit, .nl = t.len > 0 };
+}
+
+/// Adds a span if there is one; false only when a cap is reached.
+fn addSpan(c: *Chat, s: ?Span) bool {
+ const span = s orelse return true;
+ return c.add(span);
+}
+
+/// One message of `kind` holding the value at `pos` (a string as text).
+fn one(c: *Chat, p: *P, kind: Kind, time: u32, pos: ?u64) bool {
+ const at = pos orelse return true;
+ if (!c.open(kind, time)) return false;
+ if (!addSpan(c, valueSpan(p, at))) return false;
+ c.close();
+ return true;
+}
+
+/// A call's or result's id, hashed; 0 when there is none.
+fn callId(p: *P, pos: ?u64) u64 {
+ var buf: [lookahead]u8 = undefined;
+ const w = p.word(pos, &buf);
+ if (w.len == 0) return 0;
+ return std.hash.Wyhash.hash(0x63616c6c, w) | 1;
+}
+
+/// A tool call, named after its tool: the name on one line, then its
+/// input. `tool` is the name the file takes; `name` the value shown.
+fn call(c: *Chat, p: *P, time: u32, tool: []const u8, id: ?u64, name: ?u64, input: ?u64) bool {
+ if (!c.open(.tool_call, time)) return false;
+ const t = c.internTool(tool);
+ c.msgs[c.n].tool = t;
+ if (name) |n| if (!addSpan(c, valueSpan(p, n))) return false;
+ if (input) |i| if (!addSpan(c, valueSpan(p, i))) return false;
+ const kept = c.msgs[c.n].count > 0;
+ c.close();
+ if (kept) c.rememberCall(callId(p, id), t);
+ return true;
+}
+
+/// A tool's answer, named after the call it answers when that is known.
+fn result(c: *Chat, p: *P, time: u32, id: ?u64, content: ?u64) bool {
+ return blocks(c, p, .tool_result, time, c.toolOfCall(callId(p, id)), null, content);
+}
+
+/// Content that is a string, or an array of blocks whose text is shown,
+/// images named, and anything else served as its JSON — one message.
+fn blocks(c: *Chat, p: *P, kind: Kind, time: u32, tool: u8, head: ?u64, pos: ?u64) bool {
+ if (!c.open(kind, time)) return false;
+ c.msgs[c.n].tool = tool;
+ if (head) |h| if (!addSpan(c, valueSpan(p, h))) return false;
+ if (pos) |at| {
+ if (p.isString(at)) {
+ if (!addSpan(c, strSpan(p, at))) return false;
+ } else if (p.peek(at) == '[') {
+ var it = p.items(at);
+ while (it.next()) |el| {
+ const f = p.fields(el.val);
+ var tb: [32]u8 = undefined;
+ const ty = p.word(f.get(.type), &tb);
+ const s: ?Span = if (f.get(.text)) |t|
+ (if (p.isString(t)) strSpan(p, t) else null)
+ else if (std.mem.endsWith(u8, ty, "image"))
+ litSpan(lit_image)
+ else if (std.mem.eql(u8, ty, "encrypted_content"))
+ null // nothing a reader could use
+ else
+ rawSpan(p, el.val);
+ if (!addSpan(c, s)) return false;
+ }
+ } else if (!addSpan(c, rawSpan(p, at))) return false;
+ }
+ // A result that says nothing is still an answer to its call.
+ if (c.msgs[c.n].count == 0 and kind == .tool_result) {
+ if (!c.add(litSpan(lit_empty))) return false;
+ }
+ c.close();
+ return true;
+}
+
+// ---- Claude Code --------------------------------------------------------------
+
+/// One line of a Claude Code transcript. `user`, `assistant` and `system`
+/// records carry the conversation; the rest (attachments, mode changes,
+/// titles, snapshots) are the harness's bookkeeping and are not messages.
+/// Each content block is its own message, as Claude Code itself writes
+/// them. False only when a cap is reached.
+fn claudeLine(c: *Chat, p: *P, start: u64) bool {
+ const top = p.fields(p.ws(start));
+ if (top.bad) return true;
+ if (p.isTrue(top.get(.isSidechain))) return true; // a subagent's thread
+ var tb: [32]u8 = undefined;
+ var ts: [40]u8 = undefined;
+ const ty = p.word(top.get(.type), &tb);
+ const time = parseTime(p.word(top.get(.timestamp), &ts));
+ // Text the harness put in the user's mouth.
+ const injected = p.isTrue(top.get(.isMeta)) or p.isTrue(top.get(.isCompactSummary));
+ const said: Kind = if (injected) .system else .user;
+
+ if (std.mem.eql(u8, ty, "attachment")) {
+ // What a person typed while the model was working is queued, and
+ // lands here instead of in a `user` record. Queued prompts from
+ // anyone else (a peer, a task finishing) are the harness talking.
+ const a = p.fields(top.get(.attachment));
+ if (a.bad) return true;
+ var ab: [32]u8 = undefined;
+ if (!std.mem.eql(u8, p.word(a.get(.type), &ab), "queued_command")) return true;
+ const prompt = a.get(.prompt) orelse return true;
+ if (!p.isString(prompt)) return true;
+ const o = p.fields(a.get(.origin));
+ var ob: [32]u8 = undefined;
+ const human = !o.bad and std.mem.eql(u8, p.word(o.get(.kind), &ob), "human");
+ return one(c, p, if (human and !injected) .user else .system, time, prompt);
+ }
+ if (std.mem.eql(u8, ty, "system")) {
+ const content = top.get(.content) orelse return true;
+ if (!p.isString(content)) return true;
+ return one(c, p, .system, time, content);
+ }
+ const is_user = std.mem.eql(u8, ty, "user");
+ if (!is_user and !std.mem.eql(u8, ty, "assistant")) return true;
+
+ const m = p.fields(top.get(.message));
+ if (m.bad) return true;
+ const content = m.get(.content) orelse return true;
+ if (p.isString(content)) return one(c, p, if (is_user) said else .assistant, time, content);
+
+ var nm: [lookahead]u8 = undefined;
+ var it = p.items(content);
+ while (it.next()) |el| {
+ const b = p.fields(el.val);
+ if (b.bad) continue;
+ var bt: [32]u8 = undefined;
+ const bty = p.word(b.get(.type), &bt);
+ const ok = if (std.mem.eql(u8, bty, "text"))
+ one(c, p, if (is_user) said else .assistant, time, b.get(.text))
+ else if (std.mem.eql(u8, bty, "thinking")) blk: {
+ // Claude Code keeps only the signature of redacted thinking:
+ // an empty string is nothing to show.
+ const t = b.get(.thinking) orelse break :blk true;
+ if (std.mem.startsWith(u8, p.bytes(t), "\"\"")) break :blk true;
+ break :blk one(c, p, .thinking, time, t);
+ } else if (std.mem.endsWith(u8, bty, "tool_use"))
+ call(c, p, time, p.word(b.get(.name), &nm), b.get(.id), b.get(.name), b.get(.input))
+ else if (std.mem.endsWith(u8, bty, "tool_result"))
+ result(c, p, time, b.get(.tool_use_id), b.get(.content))
+ else if (is_user and std.mem.eql(u8, bty, "image")) blk: {
+ if (!c.open(said, time)) break :blk false;
+ if (!c.add(litSpan(lit_image))) break :blk false;
+ c.close();
+ break :blk true;
+ } else true;
+ if (!ok) return false;
+ }
+ return true;
+}
+
+// ---- Codex --------------------------------------------------------------------
+
+/// One line of a codex rollout. The conversation is the `response_item`
+/// records; `event_msg` repeats parts of it for the UI and is skipped, as
+/// are the session and turn metadata. False only when a cap is reached.
+fn codexLine(c: *Chat, p: *P, start: u64) bool {
+ const top = p.fields(p.ws(start));
+ if (top.bad) return true;
+ var tb: [32]u8 = undefined;
+ var ts: [40]u8 = undefined;
+ if (!std.mem.eql(u8, p.word(top.get(.type), &tb), "response_item")) return true;
+ const time = parseTime(p.word(top.get(.timestamp), &ts));
+ const f = p.fields(top.get(.payload));
+ if (f.bad) return true;
+ var pb: [40]u8 = undefined;
+ const ty = p.word(f.get(.type), &pb);
+
+ if (std.mem.eql(u8, ty, "message")) {
+ var rb: [32]u8 = undefined;
+ const role = p.word(f.get(.role), &rb);
+ const kind: Kind = if (std.mem.eql(u8, role, "user"))
+ (if (codexInjected(p, f.get(.content))) .system else .user)
+ else if (std.mem.eql(u8, role, "assistant"))
+ .assistant
+ else
+ .system; // developer, system
+ if (!c.open(kind, time)) return false;
+ var it = p.items(f.get(.content));
+ while (it.next()) |el| {
+ const b = p.fields(el.val);
+ var bt: [32]u8 = undefined;
+ const bty = p.word(b.get(.type), &bt);
+ const s: ?Span = if (b.get(.text)) |t|
+ (if (p.isString(t)) strSpan(p, t) else null)
+ else if (std.mem.endsWith(u8, bty, "image")) litSpan(lit_image) else null;
+ if (!addSpan(c, s)) return false;
+ }
+ c.close();
+ return true;
+ }
+ if (std.mem.eql(u8, ty, "reasoning")) {
+ // The summary, where codex wrote one; the reasoning itself is
+ // encrypted and never shown. Nothing readable, no message.
+ if (!c.open(.thinking, time)) return false;
+ for ([_]P.Key{ .summary, .content }) |k| {
+ var it = p.items(f.get(k));
+ while (it.next()) |el| {
+ const b = p.fields(el.val);
+ const t = b.get(.text) orelse continue;
+ if (!p.isString(t)) continue;
+ if (!addSpan(c, strSpan(p, t))) return false;
+ }
+ }
+ c.close();
+ return true;
+ }
+ if (std.mem.eql(u8, ty, "agent_message")) {
+ return blocks(c, p, .agent, time, 0, f.get(.author), f.get(.content));
+ }
+ var nm: [lookahead]u8 = undefined;
+ if (std.mem.eql(u8, ty, "function_call")) {
+ return call(c, p, time, p.word(f.get(.name), &nm), f.get(.call_id), f.get(.name), f.get(.arguments));
+ }
+ if (std.mem.eql(u8, ty, "custom_tool_call")) {
+ return call(c, p, time, p.word(f.get(.name), &nm), f.get(.call_id), f.get(.name), f.get(.input));
+ }
+ if (std.mem.endsWith(u8, ty, "_output")) {
+ return result(c, p, time, f.get(.call_id), f.get(.output));
+ }
+ if (std.mem.endsWith(u8, ty, "_call")) {
+ // local_shell_call, web_search_call, ...: no name of their own, so
+ // the record's type names the tool (less its `_call`) and its
+ // action is the input.
+ return call(c, p, time, ty[0 .. ty.len - "_call".len], f.get(.call_id), f.get(.type), f.get(.action));
+ }
+ return true;
+}
+
+/// The openings codex gives the context it writes in the user's role:
+/// the AGENTS.md it loaded, the environment, goals, plugin lists.
+const codex_injected = [_][]const u8{
+ "# AGENTS.md instructions",
+ "<environment_context>",
+ "<user_instructions>",
+ "<codex_internal_context",
+ "<recommended_plugins>",
+};
+
+/// Whether a user-role codex message is codex speaking: its first text
+/// opens with one of codex's own wrappers.
+fn codexInjected(p: *P, content: ?u64) bool {
+ var it = p.items(content);
+ const el = it.next() orelse return false;
+ const b = p.fields(el.val);
+ const t = b.get(.text) orelse return false;
+ if (!p.isString(t)) return false;
+ const head = p.bytes(t + 1);
+ for (codex_injected) |m| if (std.mem.startsWith(u8, head, m)) return true;
+ return false;
+}
+
+// ---- timestamps ---------------------------------------------------------------
+
+/// `2026-09-24T12:51:26.691Z` as Unix seconds; 0 for anything else.
+pub fn parseTime(s: []const u8) u32 {
+ if (s.len < 19 or s[4] != '-' or s[7] != '-' or s[10] != 'T' or s[13] != ':' or s[16] != ':') return 0;
+ const y = std.fmt.parseInt(i64, s[0..4], 10) catch return 0;
+ const mo = std.fmt.parseInt(i64, s[5..7], 10) catch return 0;
+ const d = std.fmt.parseInt(i64, s[8..10], 10) catch return 0;
+ const hh = std.fmt.parseInt(i64, s[11..13], 10) catch return 0;
+ const mi = std.fmt.parseInt(i64, s[14..16], 10) catch return 0;
+ const ss = std.fmt.parseInt(i64, s[17..19], 10) catch return 0;
+ if (mo < 1 or mo > 12 or d < 1 or d > 31 or hh > 23 or mi > 59 or ss > 60) return 0;
+ // Days from the civil date (Howard Hinnant's algorithm).
+ const yy = if (mo <= 2) y - 1 else y;
+ const era = @divFloor(yy, 400);
+ const yoe = yy - era * 400;
+ const mp = @mod(mo + 9, 12);
+ const doy = @divFloor(153 * mp + 2, 5) + d - 1;
+ const doe = yoe * 365 + @divFloor(yoe, 4) - @divFloor(yoe, 100) + doy;
+ const days = era * 146097 + doe - 719468;
+ const t = days * 86400 + hh * 3600 + mi * 60 + ss;
+ return std.math.cast(u32, t) orelse 0;
+}
+
+// ---- unit tests ---------------------------------------------------------------
+
+const testing = std.testing;
+
+const Fixture = struct {
+ dir: testing.TmpDir,
+ pool: *Pool,
+ file: Io.File = undefined,
+
+ fn init() !Fixture {
+ const pool = try Pool.create();
+ return .{ .dir = testing.tmpDir(.{}), .pool = pool };
+ }
+
+ fn deinit(f: *Fixture) void {
+ f.file.close(testing.io);
+ f.dir.cleanup();
+ f.pool.destroy();
+ }
+
+ /// Writes the transcript (replacing it) and opens it for reading.
+ fn write(f: *Fixture, bytes: []const u8) !void {
+ var w = try f.dir.dir.createFile(testing.io, "t.jsonl", .{});
+ defer w.close(testing.io);
+ try w.writeStreamingAll(testing.io, bytes);
+ f.file = try f.dir.dir.openFile(testing.io, "t.jsonl", .{});
+ }
+
+ fn append(f: *Fixture, bytes: []const u8) !void {
+ var w = try f.dir.dir.openFile(testing.io, "t.jsonl", .{ .mode = .read_write });
+ defer w.close(testing.io);
+ const st = try w.stat(testing.io);
+ try w.writePositionalAll(testing.io, bytes, st.size);
+ }
+
+ fn sync(f: *Fixture, format: Format) !*Chat {
+ return f.pool.sync(testing.io, f.file, format) orelse error.NotIndexed;
+ }
+
+ /// The names of every message, space-separated.
+ fn names(f: *Fixture, c: *const Chat, buf: []u8) []const u8 {
+ _ = f;
+ var w = Io.Writer.fixed(buf);
+ var nb: [96]u8 = undefined;
+ for (0..c.n) |i| {
+ if (i > 0) w.writeAll(" ") catch {};
+ w.writeAll(c.name(@intCast(i), &nb).?) catch {};
+ }
+ return w.buffered();
+ }
+
+ fn text(f: *Fixture, c: *const Chat, i: u32, buf: []u8) []const u8 {
+ const n = f.pool.render(testing.io, f.file, c, i, 0, buf);
+ return buf[0..n];
+ }
+};
+
+const claude_fixture =
+ \\{"type":"permission-mode","permissionMode":"default"}
+ \\{"type":"user","message":{"role":"user","content":"fix the \"race\"\nplease"},"timestamp":"2026-09-24T12:51:26.691Z","isMeta":false}
+ \\{"type":"assistant","message":{"role":"assistant","content":[{"type":"thinking","thinking":"","signature":"abc"}]}}
+ \\{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"caf\u00e9 \ud83d\ude00 done"}]}}
+ \\{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"ls -la"}}],"role":"assistant"}}
+ \\{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_1","type":"tool_result","content":"a\nb\n","is_error":false}]}}
+ \\{"type":"attachment","attachment":{"type":"total_tokens_reminder"}}
+ \\{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t2","content":[{"type":"text","text":"shot"},{"type":"image","source":{"data":"AAAA"}}]}]}}
+ \\{"type":"user","isMeta":true,"message":{"role":"user","content":"Base directory for this skill"}}
+ \\{"type":"user","isSidechain":true,"message":{"role":"user","content":"not ours"}}
+ \\{"type":"system","subtype":"local_command","content":"<local-command-stdout>ok</local-command-stdout>"}
+ \\{"type":"user","message":{"content":"broken"
+ \\{"type":"user","message":{"role":"user","content":[{"type":"text","text":"thanks"}]}}
+ \\{"type":"attachment","attachment":{"type":"queued_command","prompt":"also restart it","commandMode":"prompt","origin":{"kind":"human"},"humanTurn":true}}
+ \\{"type":"attachment","attachment":{"type":"queued_command","prompt":"<task-notification>done</task-notification>","commandMode":"task-notification"}}
+ \\
+;
+
+test "chat: a Claude Code transcript reads as numbered messages" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ try f.write(claude_fixture);
+ const c = try f.sync(.claude);
+ var nb: [512]u8 = undefined;
+ try testing.expectEqualStrings(
+ "00000000-user 00000001-assistant 00000002-bash 00000003-bash-result 00000004-tool-result 00000005-system 00000006-system 00000007-user 00000008-user 00000009-system",
+ f.names(c, &nb),
+ );
+ var tb: [256]u8 = undefined;
+ try testing.expectEqualStrings("fix the \"race\"\nplease\n", f.text(c, 0, &tb));
+ try testing.expectEqualStrings("café 😀 done\n", f.text(c, 1, &tb));
+ try testing.expectEqualStrings("Bash\n{\"command\":\"ls -la\"}\n", f.text(c, 2, &tb));
+ try testing.expectEqualStrings("a\nb\n", f.text(c, 3, &tb));
+ try testing.expectEqualStrings("shot\n[image]\n", f.text(c, 4, &tb));
+ try testing.expectEqualStrings("Base directory for this skill\n", f.text(c, 5, &tb));
+ try testing.expectEqualStrings("<local-command-stdout>ok</local-command-stdout>\n", f.text(c, 6, &tb));
+ try testing.expectEqualStrings("thanks\n", f.text(c, 7, &tb));
+ try testing.expectEqualStrings("also restart it\n", f.text(c, 8, &tb));
+ try testing.expectEqualStrings("<task-notification>done</task-notification>\n", f.text(c, 9, &tb));
+ // The size a stat reports is exactly what a read returns.
+ for (0..c.n) |i| {
+ try testing.expectEqual(@as(usize, c.msgs[i].size), f.text(c, @intCast(i), &tb).len);
+ }
+ try testing.expectEqual(@as(u32, 1790254286), c.msgs[0].time);
+ try testing.expectEqual(@as(u32, 0), c.msgs[1].time);
+}
+
+test "chat: a codex rollout reads as numbered messages" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ try f.write(
+ \\{"timestamp":"2026-09-24T12:51:26.691Z","type":"session_meta","payload":{"id":"x","cwd":"/"}}
+ \\{"timestamp":"2026-09-24T12:51:27.000Z","type":"response_item","payload":{"type":"message","role":"developer","content":[{"type":"input_text","text":"be careful"}]}}
+ \\{"timestamp":"2026-09-24T12:51:28.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"one"},{"type":"input_text","text":"two\n"}]}}
+ \\{"timestamp":"2026-09-24T12:51:28.000Z","type":"event_msg","payload":{"type":"user_message","message":"one"}}
+ \\{"timestamp":"2026-09-24T12:51:29.000Z","type":"response_item","payload":{"type":"reasoning","summary":[],"content":null,"encrypted_content":"gAAA"}}
+ \\{"timestamp":"2026-09-24T12:51:29.000Z","type":"response_item","payload":{"type":"reasoning","summary":[{"type":"summary_text","text":"**plan**"}]}}
+ \\{"timestamp":"2026-09-24T12:51:30.000Z","type":"response_item","payload":{"type":"function_call","name":"shell","arguments":"{\"command\":[\"ls\"]}","call_id":"c1"}}
+ \\{"timestamp":"2026-09-24T12:51:31.000Z","type":"response_item","payload":{"type":"function_call_output","call_id":"c1","output":"file\n"}}
+ \\{"timestamp":"2026-09-24T12:51:32.000Z","type":"response_item","payload":{"type":"custom_tool_call","name":"apply_patch","input":"*** Begin Patch","call_id":"c2"}}
+ \\{"timestamp":"2026-09-24T12:51:33.000Z","type":"response_item","payload":{"type":"custom_tool_call_output","call_id":"c2","output":[{"type":"input_text","text":"Done"},{"type":"input_text","text":"!"}]}}
+ \\{"timestamp":"2026-09-24T12:51:34.000Z","type":"response_item","payload":{"type":"web_search_call","status":"completed","action":{"type":"search","query":"zig"}}}
+ \\{"timestamp":"2026-09-24T12:51:35.000Z","type":"response_item","payload":{"type":"agent_message","author":"/root/w1","recipient":"/root","content":[{"type":"input_text","text":"hi"},{"type":"encrypted_content","encrypted_content":"gAAA"}]}}
+ \\{"timestamp":"2026-09-24T12:51:36.000Z","type":"compacted","payload":{"message":"","replacement_history":[{"type":"message"}]}}
+ \\{"timestamp":"2026-09-24T12:51:37.000Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"all done"}]}}
+ \\{"timestamp":"2026-09-24T12:51:38.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"<environment_context>\\n <cwd>/x</cwd>"}]}}
+ \\{"timestamp":"2026-09-24T12:51:39.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"# AGENTS.md instructions for /x"}]}}
+ \\
+ );
+ const c = try f.sync(.codex);
+ var nb: [512]u8 = undefined;
+ try testing.expectEqualStrings(
+ "00000000-system 00000001-user 00000002-thinking 00000003-shell 00000004-shell-result 00000005-apply_patch 00000006-apply_patch-result 00000007-web_search 00000008-agent 00000009-assistant 00000010-system 00000011-system",
+ f.names(c, &nb),
+ );
+ var tb: [256]u8 = undefined;
+ try testing.expectEqualStrings("be careful\n", f.text(c, 0, &tb));
+ try testing.expectEqualStrings("one\ntwo\n", f.text(c, 1, &tb));
+ try testing.expectEqualStrings("**plan**\n", f.text(c, 2, &tb));
+ try testing.expectEqualStrings("shell\n{\"command\":[\"ls\"]}\n", f.text(c, 3, &tb));
+ try testing.expectEqualStrings("file\n", f.text(c, 4, &tb));
+ try testing.expectEqualStrings("apply_patch\n*** Begin Patch\n", f.text(c, 5, &tb));
+ try testing.expectEqualStrings("Done\n!\n", f.text(c, 6, &tb));
+ try testing.expectEqualStrings("web_search_call\n{\"type\":\"search\",\"query\":\"zig\"}\n", f.text(c, 7, &tb));
+ try testing.expectEqualStrings("/root/w1\nhi\n", f.text(c, 8, &tb));
+ try testing.expectEqualStrings("all done\n", f.text(c, 9, &tb));
+ try testing.expectEqual(@as(u32, 1790254287), c.msgs[0].time);
+}
+
+test "chat: a tool call is named after its tool, and its result after the call" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ try f.write(
+ \\{"type":"assistant","message":{"content":[{"type":"tool_use","id":"a","name":"Read","input":{}}]}}
+ \\{"type":"assistant","message":{"content":[{"type":"tool_use","id":"b","name":"mcp__x__Send Mail/now","input":{}}]}}
+ \\{"type":"assistant","message":{"content":[{"type":"tool_use","id":"c","name":"User","input":{}}]}}
+ \\{"type":"assistant","message":{"content":[{"type":"tool_use","id":"d","name":"read","input":{}}]}}
+ \\{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"b","content":"sent"}]}}
+ \\{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"a","content":"text"}]}}
+ \\{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"c","content":"?"}]}}
+ \\{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"zz","content":"?"}]}}
+ \\
+ );
+ const c = try f.sync(.claude);
+ var nb: [512]u8 = undefined;
+ // Parallel calls answered out of order still find their tools; a name
+ // is made safe for a file; one that would read as another kind is not
+ // used, and neither is a result whose call is unknown.
+ try testing.expectEqualStrings(
+ "00000000-read 00000001-mcp__x__send-mail-now 00000002-tool-call 00000003-read " ++
+ "00000004-mcp__x__send-mail-now-result 00000005-read-result 00000006-tool-result 00000007-tool-result",
+ f.names(c, &nb),
+ );
+ try testing.expectEqual(@as(?u32, 4), c.lookup("00000004-mcp__x__send-mail-now-result"));
+ try testing.expect(c.lookup("00000004-tool-result") == null);
+ try testing.expect(c.lookup("00000000-tool-call") == null);
+ try testing.expect(c.lookup("00000000-read-result") == null);
+ var tb: [64]u8 = undefined;
+ try testing.expectEqualStrings("Read\n{}\n", f.text(c, 0, &tb));
+}
+
+test "chat: ids hold as the transcript grows; a half-written line waits" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ try f.write(
+ \\{"type":"user","message":{"content":"first"}}
+ \\{"type":"assistant","message":{"content":[{"type":"text","text":"sec
+ );
+ var c = try f.sync(.claude);
+ try testing.expectEqual(@as(u32, 1), c.n); // the second line has no newline yet
+ try f.append("ond\"}]}}\n{\"type\":\"user\",\"message\":{\"content\":\"third\"}}\n");
+ c = try f.sync(.claude);
+ var nb: [128]u8 = undefined;
+ try testing.expectEqualStrings("00000000-user 00000001-assistant 00000002-user", f.names(c, &nb));
+ var tb: [64]u8 = undefined;
+ try testing.expectEqualStrings("second\n", f.text(c, 1, &tb));
+
+ // One name per message: the kind must match, the id has one spelling.
+ try testing.expectEqual(@as(?u32, 1), c.lookup("00000001-assistant"));
+ try testing.expect(c.lookup("00000001-user") == null);
+ try testing.expect(c.lookup("1-assistant") == null); // unpadded
+ try testing.expect(c.lookup("000000001-assistant") == null); // too wide
+ try testing.expect(c.lookup("+0000001-assistant") == null);
+ try testing.expect(c.lookup("0000000x-assistant") == null);
+ try testing.expect(c.lookup("00000003-user") == null);
+ try testing.expect(c.lookup("1") == null);
+ try testing.expect(c.lookup("-assistant") == null);
+
+ // A transcript rewritten shorter is a different transcript.
+ f.file.close(testing.io);
+ try f.write("{\"type\":\"assistant\",\"message\":{\"content\":\"new\"}}\n");
+ c = try f.sync(.claude);
+ try testing.expectEqualStrings("00000000-assistant", f.names(c, &nb));
+}
+
+test "chat: a transcript rewritten in place is indexed again" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ try f.write("{\"type\":\"user\",\"message\":{\"content\":\"first\"}}\n");
+ var c = try f.sync(.claude);
+ var nb: [128]u8 = undefined;
+ try testing.expectEqualStrings("00000000-user", f.names(c, &nb));
+ // The same inode, truncated and rewritten longer: only the bytes can
+ // tell. (`cp backup live.jsonl` does exactly this.)
+ f.file.close(testing.io);
+ try f.write(
+ \\{"type":"assistant","message":{"content":[{"type":"text","text":"a different start"}]}}
+ \\{"type":"user","message":{"content":"then this"}}
+ \\
+ );
+ c = try f.sync(.claude);
+ try testing.expectEqualStrings("00000000-assistant 00000001-user", f.names(c, &nb));
+ var tb: [64]u8 = undefined;
+ try testing.expectEqualStrings("a different start\n", f.text(c, 0, &tb));
+}
+
+test "chat: reads at any offset match the whole, across the window's edge" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ // A string far longer than the window, dense with escapes, so some
+ // escape straddles every window boundary the render crosses.
+ const unit = "ab\\u00e9\\n\\ud83d\\ude00\\\"x";
+ const plain = "abé\n😀\"x";
+ const reps = 12000;
+ var bytes: std.ArrayList(u8) = .empty;
+ defer bytes.deinit(testing.allocator);
+ try bytes.appendSlice(testing.allocator, "{\"type\":\"user\",\"message\":{\"content\":\"");
+ for (0..reps) |_| try bytes.appendSlice(testing.allocator, unit);
+ try bytes.appendSlice(testing.allocator, "\"}}\n");
+ try f.write(bytes.items);
+ const c = try f.sync(.claude);
+ try testing.expectEqual(@as(u32, 1), c.n);
+ const want_len = plain.len * reps + 1;
+ try testing.expectEqual(@as(u64, want_len), c.msgs[0].size);
+
+ const whole = try testing.allocator.alloc(u8, want_len);
+ defer testing.allocator.free(whole);
+ try testing.expectEqual(want_len, f.pool.render(testing.io, f.file, c, 0, 0, whole));
+ for (0..reps) |i| try testing.expectEqualStrings(plain, whole[i * plain.len ..][0..plain.len]);
+ try testing.expectEqual(@as(u8, '\n'), whole[want_len - 1]);
+
+ // Piecewise, at odd sizes, the way a client's reads arrive: each read
+ // resumes where the last stopped.
+ var off: usize = 0;
+ var piece: [4093]u8 = undefined;
+ while (off < want_len) {
+ const n = f.pool.render(testing.io, f.file, c, 0, off, &piece);
+ try testing.expect(n > 0);
+ try testing.expectEqualSlices(u8, whole[off..][0..n], piece[0..n]);
+ off += n;
+ }
+ try testing.expectEqual(@as(usize, 0), f.pool.render(testing.io, f.file, c, 0, want_len, &piece));
+ // Out of order — backwards, and repeating a read — must not trust the
+ // cursor a later read left behind.
+ var back: usize = want_len;
+ while (back > 0) {
+ const at = back -| 3001;
+ const n = f.pool.render(testing.io, f.file, c, 0, at, piece[0..@min(piece.len, back - at)]);
+ try testing.expectEqualSlices(u8, whole[at..][0..n], piece[0..n]);
+ const again = f.pool.render(testing.io, f.file, c, 0, at, piece[0..@min(piece.len, back - at)]);
+ try testing.expectEqual(n, again);
+ try testing.expectEqualSlices(u8, whole[at..][0..n], piece[0..n]);
+ back = at;
+ }
+}
+
+test "chat: a string ends at the quote an even run of backslashes leaves bare" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ // Runs of escaped backslashes long enough to straddle the window's
+ // edge, each ending in an escaped quote that must not close the string.
+ var bytes: std.ArrayList(u8) = .empty;
+ defer bytes.deinit(testing.allocator);
+ const a = testing.allocator;
+ try bytes.appendSlice(a, "{\"type\":\"user\",\"message\":{\"content\":\"x");
+ for (0..3) |_| {
+ for (0..40000) |_| try bytes.appendSlice(a, "\\\\");
+ try bytes.appendSlice(a, "\\\"");
+ }
+ try bytes.appendSlice(a, "end\"}}\n{\"type\":\"assistant\",\"message\":{\"content\":\"after\"}}\n");
+ try f.write(bytes.items);
+ const c = try f.sync(.claude);
+ var nb: [64]u8 = undefined;
+ try testing.expectEqualStrings("00000000-user 00000001-assistant", f.names(c, &nb));
+ const want = 1 + 3 * (40000 + 1) + "end\n".len;
+ try testing.expectEqual(@as(u32, want), c.msgs[0].size);
+ const out = try a.alloc(u8, want);
+ defer a.free(out);
+ try testing.expectEqual(@as(usize, want), f.pool.render(testing.io, f.file, c, 0, 0, out));
+ try testing.expectEqualStrings("\\\"end\n", out[want - 6 ..]);
+ try testing.expectEqual(@as(usize, 3), std.mem.count(u8, out, "\""));
+ var tb: [16]u8 = undefined;
+ try testing.expectEqualStrings("after\n", f.text(c, 1, &tb));
+}
+
+test "chat: a transcript past the cap says so instead of ending early" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ var bytes: std.ArrayList(u8) = .empty;
+ defer bytes.deinit(testing.allocator);
+ for (0..max_msgs + 1) |_| try bytes.appendSlice(testing.allocator, "{\"type\":\"user\",\"message\":{\"content\":\"\"}}\n");
+ try f.write(bytes.items);
+ const c = try f.sync(.claude);
+ try testing.expect(c.full);
+ try testing.expectEqual(@as(u32, max_msgs), c.n);
+}
+
+test "chat: malformed JSON never escapes the line it is on" {
+ var f = try Fixture.init();
+ defer f.deinit();
+ try f.write(
+ \\{"type":"user","message":{"content":"a"}}
+ \\]]]}}}{{{["""\\"
+ \\{"type":"user","message":{"content":[{"type":"text","text":"b"},]}}
+ \\{"type":"assistant","message":{"content":[{"type":"tool_use","name":"X","input":{"u":"\u"}}]}}
+ \\{"type":"user","message":{"content":"c"}}
+ \\
+ );
+ const c = try f.sync(.claude);
+ var nb: [128]u8 = undefined;
+ // The trailing comma ends that walk after "b"; the stray `\u` is not
+ // an escape, so it is served as written.
+ try testing.expectEqualStrings("00000000-user 00000001-user 00000002-x 00000003-user", f.names(c, &nb));
+ var tb: [64]u8 = undefined;
+ try testing.expectEqualStrings("X\n{\"u\":\"\\u\"}\n", f.text(c, 2, &tb));
+ // A sign or an underscore is not a hex digit.
+ var sink: Sink = .{ .out = &tb };
+ _ = decode("\\u+0e9\\u0_e9\\u00e9", 0, true, &sink);
+ try testing.expectEqualStrings("\\u+0e9\\u0_e9é", tb[0..sink.n]);
+ try testing.expectEqualStrings("c\n", f.text(c, 3, &tb));
+}
+
+test "chat: timestamps" {
+ try testing.expectEqual(@as(u32, 0), parseTime("2026-09-24T12:51:26"[0..10]));
+ try testing.expectEqual(@as(u32, 86400), parseTime("1970-01-02T00:00:00Z"));
+ try testing.expectEqual(@as(u32, 1790254286), parseTime("2026-09-24T12:51:26.691Z"));
+ try testing.expectEqual(@as(u32, 0), parseTime("not a time at all, no"));
+}