feat(conversation): CHAT-02 read-only Pi history reader and two board routes (#1507)

packages/conversation is a library with no server: safe-fs, the Pi session
parser, CHAT-01 pages, pinned snapshots, cursors and follow. The control
board adds GET /api/conversations and /api/conversation behind the Host
and Origin guard. Both are read-only, their queries are validated, and
each refusal code maps to a status.

Dewey authored it (packet 0cf177b1, revision 2). Filbert reviewed the code:
R1 revise (branch ids moving on append, the assumed-link bridge merging
branches, one unreadable seat directory turning the catalogue into a 500),
then R2 approve (3b14d66c). Darkwing reviewed the routes: R1 approve
(07b10ad1), R2 approve (b9d92003). The package lands with the routes,
because serve.mjs imports the reader at load.

On an index export: the eight suites 24/90/43/17/14/15/63/18,
conversation and control-board 153/153, webui 9/9.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-26 16:36:20 -05:00
co-authored by Claude Opus 5.5
parent c5db8c8819
commit a5beb6d97d
22 changed files with 3247 additions and 3 deletions
+773
View File
@@ -0,0 +1,773 @@
// packages/conversation (#1507, CHAT-02): fixtures F1–F15 and F17 from the
// brief (agents/dewey/work/chat-02/BRIEF.md §2.1). F16 is in the control-board
// suite, which serves these routes. Every reader call runs inside noWrites(),
// which fingerprints the fixture tree before and after (F17). Every page and
// cursor is checked against the CHAT-01 schema at the end.
import { test, after } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync, readFileSync, readdirSync, rmSync, chmodSync, symlinkSync, lstatSync, renameSync, openSync, writeSync, closeSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve, relative } from "node:path";
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import { rootsFromSpecs, createReader, conversationId, cwdInProject, UNSUPPORTED_HARNESS } from "../src/reader.mjs";
import { fragments, safeId, LIMITS } from "../src/parts.mjs";
const repoRoot = resolve(import.meta.dirname, "..", "..", "..");
const schemaPath = join(repoRoot, "docs", "plans", "chat-01", "contracts.schema.json");
const tmp = mkdtempSync(join(tmpdir(), "conversation-"));
const records = [];
after(() => {
// chmod 000 fixtures would block removal
spawnSync("chmod", ["-R", "u+rwx", tmp]);
rmSync(tmp, { recursive: true, force: true });
});
let fixtureCount = 0;
const time = (i) => new Date(Date.UTC(2026, 8, 26, 12, 0, 0) + i * 1000).toISOString();
// A project with one seat's sessions directory.
function project(seat = "pi-seat") {
const base = join(tmp, `f${++fixtureCount}`);
const proj = join(base, "proj");
const dir = join(proj, ".pi", "state", seat, "sessions");
mkdirSync(dir, { recursive: true });
return { base, proj, dir, seat, spec: { agent: seat, project: "proj", sessionsDir: dir } };
}
const header = (proj, extra = {}) => ({ type: "session", version: 3, id: "0f5e1c2a-1111-4222-8333-944455556666", timestamp: time(0), cwd: proj, ...extra });
const msg = (id, parentId, role, content, extra = {}, i = 1) => ({ type: "message", id, parentId, timestamp: time(i), message: { role, content, ...extra } });
const user = (id, parentId, text, i) => msg(id, parentId, "user", [{ type: "text", text }], {}, i);
const assistant = (id, parentId, text, i) => msg(id, parentId, "assistant", [{ type: "text", text }], { stopReason: "stop" }, i);
const lines = (...values) => values.map((v) => (typeof v === "string" ? v : JSON.stringify(v)) + "\n").join("");
function writeSession(dir, name, head, entries, trailing = "") {
const path = join(dir, name);
writeFileSync(path, lines(head, ...entries) + trailing);
return path;
}
// Fingerprint of every file and directory under `root`, symlinks not followed.
function fingerprint(root) {
const out = {};
const walk = (path) => {
const st = lstatSync(path, { bigint: true });
const rec = { dev: String(st.dev), ino: String(st.ino), mtime: String(st.mtimeNs), size: String(st.size), mode: String(st.mode) };
if (st.isDirectory()) {
rec.list = st.mode & 0o400n ? readdirSync(path).sort() : null;
for (const n of rec.list ?? []) walk(join(path, n));
} else if (st.isFile() && st.mode & 0o400n) {
rec.sha256 = createHash("sha256").update(readFileSync(path)).digest("hex");
}
out[path] = rec;
};
walk(root);
return out;
}
// F17: a reader operation leaves every file and listing in `root` unchanged.
function noWrites(root, fn) {
const before = fingerprint(root);
const result = fn();
assert.deepEqual(fingerprint(root), before, "a reader operation changed the fixture tree");
for (const key of ["page", "cursor", "follow"]) if (result?.[key]) records.push(result[key]);
return result;
}
function readerFor(fx, opts = {}) {
return createReader({ roots: rootsFromSpecs([fx.spec], opts.registrations ?? []), ...opts });
}
function openOne(fx, reader, name = "s.jsonl", args = {}) {
return noWrites(fx.base, () => reader.open({ conversation: conversationId(rootsFromSpecs([fx.spec])[0], name), ...args }));
}
// Every page from an open, following next cursors.
function readAll(fx, reader, first) {
const pages = [first];
let r = first;
while (r.ok && r.page.hasMore) {
const page = r.page;
r = noWrites(fx.base, () => reader.next({ cursor: page.nextCursor, conversation: page.conversation, branch: page.branch }));
assert.equal(r.ok, true, JSON.stringify(r.refusal));
pages.push(r);
}
return pages;
}
const texts = (page) => page.entries.map((e) => e.content.map((b) => b.text ?? b.summary ?? b.argumentsText ?? "").join(""));
const roles = (page) => page.entries.map((e) => e.role);
test("a plain conversation: catalogue row, one page, CHAT-01 records", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("a1", null, "hello", 1),
{ type: "session_info", id: "a2", parentId: "a1", timestamp: time(2), name: "Greeting" },
assistant("a3", "a2", "hi there", 3),
{ type: "model_change", id: "a4", parentId: "a3", timestamp: time(4), provider: "x", modelId: "y" },
]);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
assert.equal(cat.conversations.length, 1);
const row = cat.conversations[0];
assert.equal(row.title, "Greeting");
assert.equal(row.availability, "available");
assert.equal(row.readOnly, true);
assert.equal(row.conversationCreatedAt, time(0));
assert.equal(row.lastActivityAt, time(4));
assert.equal(row.engineStartedAt, null);
const r = openOne(fx, reader);
assert.equal(r.ok, true);
assert.deepEqual(roles(r.page), ["user", "assistant"]);
assert.deepEqual(texts(r.page), ["hello", "hi there"]);
assert.equal(r.page.hasMore, false);
assert.equal(r.page.nextCursor, null);
assert.equal(r.cursor, null);
assert.ok(r.follow, "the last page carries a follow cursor");
assert.equal(r.page.branch, "main");
assert.equal(r.view.defaultBranch, "main");
assert.equal(r.page.entries[1].parent, "a2");
assert.equal(r.page.readOnly, true);
});
test("native entries map to blocks: tools, thinking, bash, notices, ids that do not fit", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("b1", null, "run it", 1),
msg("b2", "b1", "assistant", [
{ type: "thinking", thinking: "plan" },
{ type: "thinking", thinking: "" },
{ type: "toolCall", id: "call_x|fc_y", name: "bash", arguments: { command: "ls" } },
{ type: "thinking", thinking: "[Reasoning redacted]", redacted: true, thinkingSignature: "opaque" },
], { stopReason: "toolUse" }, 2),
msg("b3", "b2", "toolResult", [{ type: "text", text: "a" }, { type: "image", data: "AAAA", mimeType: "image/png" }, { type: "text", text: "b" }], { toolCallId: "call_x|fc_y", toolName: "bash", isError: false }, 3),
{ type: "message", id: "b4", parentId: "b3", timestamp: time(4), message: { role: "bashExecution", command: "pwd", output: "/p", exitCode: 1, cancelled: false } },
{ type: "custom_message", id: "b5", parentId: "b4", timestamp: time(5), customType: "x", content: "shown", display: true },
{ type: "custom_message", id: "b6", parentId: "b5", timestamp: time(6), customType: "x", content: "hidden", display: false },
msg("b7", "b6", "assistant", [{ type: "text", text: "partial" }], { stopReason: "error", errorMessage: "overloaded" }, 7),
{ type: "future_kind", id: "b8", parentId: "b7", timestamp: time(8) },
]);
const r = openOne(fx, readerFor(fx));
assert.equal(r.ok, true);
assert.deepEqual(roles(r.page), ["user", "assistant", "tool", "tool", "notice", "assistant", "notice", "notice"]);
const [, a, tool, bash, shown, , end, unknown] = r.page.entries;
assert.deepEqual(a.content.map((b) => [b.type, b.visibility ?? null]), [["thinking", "permitted-visible"], ["thinking", "unavailable"], ["tool-call", null], ["thinking", "unavailable"]]);
assert.equal(a.content[3].text, "", "redacted reasoning shows no placeholder");
assert.equal(a.content[2].call, safeId("call_x|fc_y"));
assert.match(a.content[2].call, /^h-[0-9a-f]{40}$/);
assert.equal(a.content[2].argumentsText, '{"command":"ls"}');
assert.deepEqual(tool.content.map((b) => b.type), ["tool-result", "attachment"]);
assert.equal(tool.content[0].text, "a\nb");
assert.equal(tool.content[0].call, a.content[2].call);
assert.deepEqual(bash.content.map((b) => b.type), ["tool-call", "tool-result"]);
assert.equal(bash.content[1].isError, true);
assert.equal(shown.content[0].text, "shown");
assert.match(end.content[0].text, /overloaded/);
assert.match(unknown.content[0].text, /future_kind/);
assert.equal(new Set(r.page.entries.map((e) => e.id)).size, r.page.entries.length, "entry ids are unique");
});
test("F1: a malformed line is an unavailable part at its position, and reading continues", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("c1", null, "one", 1),
assistant("c2", "c1", "two", 2),
'{"type":"message","id":"c3", broken',
user("c4", "c2", "four", 4),
"[1,2,3]",
assistant("x.line-6", "c4", "five", 5), // a native id shaped like a notice id
]);
const r = openOne(fx, readerFor(fx));
assert.equal(r.ok, true);
assert.deepEqual(roles(r.page), ["user", "assistant", "notice", "user", "notice", "assistant"]);
assert.equal(new Set(r.page.entries.map((e) => e.id)).size, 6, "native ids never collide with notice ids");
assert.match(texts(r.page)[2], /^Line 4 could not be read/);
assert.match(texts(r.page)[4], /^Line 6 could not be read/);
assert.equal(r.view.unreadableLines, 2);
const cat = noWrites(fx.base, () => readerFor(fx).catalogue());
assert.equal(cat.conversations[0].availability, "available");
});
test("F1: a missing parent stops the history with a notice that names the unreadable lines", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("d1", null, "one", 1),
"garbage",
assistant("d3", "d2", "three", 3),
]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.deepEqual([roles(r.page), r.page.branch], [["notice", "assistant"], "b.d3"]);
assert.match(texts(r.page)[0], /missing from the file\. Line 3 could not be read and may have held it\.$/);
// The history before the gap is its own branch, and the line shows there too.
const main = openOne(fx, reader, "s.jsonl", { branch: "main" });
assert.deepEqual([roles(main.page), texts(main.page)[0]], [["user", "notice"], "one"]);
assert.match(texts(main.page)[1], /^Line 3 could not be read\. It may belong to this branch or another one\.$/);
const fx2 = project();
writeSession(fx2.dir, "s.jsonl", header(fx2.proj), [user("e1", null, "one", 1), assistant("e3", "e2", "three", 3)]);
const r2 = openOne(fx2, readerFor(fx2));
assert.deepEqual(roles(r2.page), ["notice", "assistant"], "no unreadable line: history stops with a notice");
assert.equal(texts(r2.page)[0], "History before this point is unavailable: an earlier entry is missing from the file.");
});
test("F1: an unreadable fork is never merged into another branch's history", () => {
// Line 4 held X, a fork from a; y is X's child.
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("a", null, "root", 1),
assistant("b", "a", "ON THE OTHER BRANCH", 2),
'{"type":"message","id":"X","parentId":"a",',
user("y", "X", "leaf", 4),
]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.deepEqual(r.view.branches.map((b) => [b.branch, b.isDefault]), [["main", false], ["b.y", true]]);
assert.deepEqual(roles(r.page), ["notice", "user"]);
assert.match(texts(r.page)[0], /Line 4 could not be read and may have held it/);
assert.ok(!texts(r.page).some((t) => /ON THE OTHER BRANCH|^root$/.test(t)), "no text from the other branch");
});
test("F1: a follow stays on its branch when the next entry's parent is unreadable", () => {
const fx = project();
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("t1", null, "one", 1), "garbage"]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.deepEqual([roles(r.page), r.page.branch], [["user", "notice"], "main"]);
// t3's parent was on the unreadable line, so t3 starts its own branch. The
// view keeps its branch and parts, and reports the new default.
appendFileSync(path, lines(assistant("t3", "t2", "three", 3)));
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.deepEqual([f.ok, f.page.entries.length, f.page.branch, f.view.defaultBranch], [true, 0, "main", "b.t3"]);
const again = openOne(fx, reader);
assert.deepEqual([roles(again.page), again.page.branch], [["notice", "assistant"], "b.t3"]);
assert.match(texts(again.page)[0], /Line 3 could not be read and may have held it/);
});
test("F1: a file whose entries are all unreadable shows a notice per line", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), ["garbage", "[1]", '{"type":"message"}']);
const r = openOne(fx, readerFor(fx));
assert.deepEqual([r.ok, r.page.branch, r.view.unreadableLines], [true, "main", 3]);
assert.deepEqual(texts(r.page).map((t) => t.match(/^Line (\d+)/)[1]), ["2", "3", "4"]);
});
test("F2: a truncated trailing line marks the view incomplete, not an error", () => {
const fx = project();
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("f1", null, "one", 1)], '{"type":"message","id":"f2","parentId":"f1"');
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.equal(r.ok, true);
assert.equal(r.view.incomplete, true);
assert.deepEqual(texts(r.page), ["one"]);
assert.equal(r.view.unreadableLines, 0);
appendFileSync(path, ',"timestamp":"' + time(2) + '","message":{"role":"assistant","content":[{"type":"text","text":"two"}]}}\n');
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.equal(f.ok, true);
assert.deepEqual(texts(f.page), ["two"]);
assert.equal(f.view.incomplete, false);
assert.equal(f.page.sourceEpoch, r.page.sourceEpoch);
});
// 150 one-part entries: two pages.
function longSession(fx) {
const entries = [];
for (let i = 0; i < 150; i++) entries.push(i % 2 ? assistant(`g${i}`, i ? `g${i - 1}` : null, `m${i}`, i + 1) : user(`g${i}`, i ? `g${i - 1}` : null, `m${i}`, i + 1));
return writeSession(fx.dir, "s.jsonl", header(fx.proj), entries);
}
test("pagination: 100 parts, then the rest; parts concatenate to the whole branch", () => {
const fx = project();
longSession(fx);
const reader = readerFor(fx);
const pages = readAll(fx, reader, openOne(fx, reader));
assert.deepEqual(pages.map((p) => p.page.entries.length), [100, 50]);
assert.deepEqual(pages.flatMap((p) => texts(p.page)), Array.from({ length: 150 }, (_, i) => `m${i}`));
assert.equal(pages[0].cursor.lastEntry, pages[0].page.entries[99].id);
assert.equal(pages[0].follow, null);
assert.ok(pages[1].follow);
});
test("F3: a replaced file (new inode) refuses old cursors with reconcile", () => {
const fx = project();
const path = longSession(fx);
const reader = readerFor(fx);
const r = openOne(fx, reader);
const content = readFileSync(path);
writeFileSync(path + ".new", content);
renameSync(path + ".new", path);
for (const cursor of [r.cursor.id]) {
const x = noWrites(fx.base, () => reader.next({ cursor, conversation: r.page.conversation, branch: r.page.branch }));
assert.deepEqual([x.ok, x.refusal.code, x.refusal.reconcile], [false, "source-replaced", true]);
}
const short = project();
writeSession(short.dir, "s.jsonl", header(short.proj), [user("h1", null, "one", 1)]);
const reader2 = readerFor(short);
const r2 = openOne(short, reader2);
const p2 = join(short.dir, "s.jsonl");
writeFileSync(p2 + ".new", readFileSync(p2));
renameSync(p2 + ".new", p2);
const f = noWrites(short.base, () => reader2.next({ cursor: r2.follow.id, conversation: r2.page.conversation, branch: r2.page.branch }));
assert.deepEqual([f.ok, f.refusal.code, f.refusal.reconcile], [false, "source-replaced", true], "follow cursor too");
const reopened = openOne(short, reader2);
assert.notEqual(reopened.page.sourceEpoch, r2.page.sourceEpoch, "a reopen is a new epoch");
});
test("F4: a same-inode rewrite of the prefix refuses old cursors with reconcile", () => {
const fx = project();
const path = longSession(fx);
const reader = readerFor(fx);
const r = openOne(fx, reader);
const ino = lstatSync(path).ino;
const text = readFileSync(path, "utf8").replace('"m3"', '"M3"');
const fd = openSync(path, "r+");
writeSync(fd, text, 0);
closeSync(fd);
assert.equal(lstatSync(path).ino, ino, "same inode");
const x = noWrites(fx.base, () => reader.next({ cursor: r.cursor.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.deepEqual([x.ok, x.refusal.code, x.refusal.reconcile], [false, "source-replaced", true]);
// Shorter than the snapshot.
const r2 = openOne(fx, reader);
writeFileSync(path, lines(header(fx.proj)));
const y = noWrites(fx.base, () => reader.next({ cursor: r2.cursor.id, conversation: r2.page.conversation, branch: r2.page.branch }));
assert.deepEqual([y.ok, y.refusal.code, y.refusal.reconcile], [false, "source-replaced", true]);
});
test("F5: growth between pages keeps the epoch and the page stops at the pinned length", () => {
const fx = project();
const path = longSession(fx);
const reader = readerFor(fx);
const r = openOne(fx, reader);
appendFileSync(path, lines(user("g150", "g149", "later", 200)) + '{"type":"mess');
const r2 = noWrites(fx.base, () => reader.next({ cursor: r.cursor.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.equal(r2.ok, true);
assert.equal(r2.page.entries.length, 50);
assert.equal(r2.page.snapshotDigest, r.page.snapshotDigest);
assert.equal(r2.page.sourceEpoch, r.page.sourceEpoch);
assert.equal(r2.view.incomplete, false, "the pinned snapshot ended on a complete line");
assert.ok(!texts(r2.page).includes("later"));
// Following picks up the growth, up to the last complete line.
const f = noWrites(fx.base, () => reader.next({ cursor: r2.follow.id, conversation: r2.page.conversation, branch: r2.page.branch }));
assert.equal(f.ok, true);
assert.deepEqual(texts(f.page), ["later"]);
assert.equal(f.page.sourceEpoch, r.page.sourceEpoch);
assert.notEqual(f.page.snapshotDigest, r.page.snapshotDigest);
assert.equal(f.page.branch, "main");
assert.equal(f.view.incomplete, true);
// A read of a file that grows by a partial line cuts at the last newline.
const again = openOne(fx, reader);
assert.equal(again.view.incomplete, true);
const all = readAll(fx, reader, again).flatMap((p) => texts(p.page));
assert.equal(all.length, 151);
});
test("F6: unknown, foreign and expired cursors refuse and leave the cursor usable", () => {
const fx = project();
longSession(fx);
let clock = Date.parse("2026-09-26T12:00:00Z");
const reader = readerFor(fx, { now: () => clock, ttlMs: 60_000 });
const r = openOne(fx, reader);
const good = { cursor: r.cursor.id, conversation: r.page.conversation, branch: r.page.branch };
const cases = [
[{ ...good, cursor: "c-unknown" }, "cursor-unknown"],
[{ ...good, actor: "someone-else" }, "cursor-foreign"],
[{ ...good, purpose: "drafts" }, "cursor-foreign"],
[{ ...good, conversation: "pi-" + "0".repeat(32) }, "cursor-foreign"],
[{ ...good, branch: "g1" }, "cursor-foreign"],
];
for (const [args, code] of cases) {
const x = noWrites(fx.base, () => reader.next(args));
assert.deepEqual([x.ok, x.refusal.code, x.refusal.reconcile], [false, code, true], code);
}
const ok = noWrites(fx.base, () => reader.next(good));
assert.equal(ok.ok, true, "the old view's cursor still works after the refusals");
clock += 60_000;
const expired = noWrites(fx.base, () => reader.next(good));
assert.deepEqual([expired.ok, expired.refusal.code, expired.refusal.reconcile], [false, "cursor-expired", true]);
const gone = noWrites(fx.base, () => reader.next(good));
assert.equal(gone.refusal.code, "cursor-unknown");
// Evicted cursors are unknown.
const small = readerFor(fx, { maxCursors: 2 });
const first = openOne(fx, small);
openOne(fx, small);
openOne(fx, small);
const evicted = noWrites(fx.base, () => small.next({ cursor: first.cursor.id, conversation: first.page.conversation, branch: first.page.branch }));
assert.equal(evicted.refusal.code, "cursor-unknown");
assert.ok(small.cursorCount() <= 2);
// open refuses other actors and purposes outright.
assert.equal(openOne(fx, reader, "s.jsonl", { actor: "x" }).refusal.code, "unknown-actor");
assert.equal(openOne(fx, reader, "s.jsonl", { purpose: "drafts" }).refusal.code, "unsupported-purpose");
});
// A file outside every root that no read may touch: unreadable, so an
// attempted open would throw EACCES rather than refuse.
function forbidden(base, name = "outside.jsonl") {
const dir = join(base, "outside");
mkdirSync(dir, { recursive: true });
const path = join(dir, name);
writeFileSync(path, lines(header("/elsewhere"), user("z1", null, "secret", 1)));
chmodSync(path, 0o000);
return path;
}
test("F7: a symlinked file and a symlinked directory component are refused, never opened", () => {
const fx = project();
const outside = forbidden(fx.base);
symlinkSync(outside, join(fx.dir, "link.jsonl"));
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("i1", null, "ok", 1)]);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
const link = cat.conversations.find((c) => c.availability === "denied");
assert.deepEqual([link.refusal, link.title], ["unsafe-path", null]);
const r = openOne(fx, reader, "link.jsonl");
assert.deepEqual([r.ok, r.refusal.code], [false, "unsafe-path"]);
assert.equal(cat.conversations.filter((c) => c.availability === "available").length, 1);
// .pi/state/<seat> is a symlink to a directory holding a sessions dir.
const fy = project("other");
const realSeat = join(fy.base, "real-seat");
mkdirSync(join(realSeat, "sessions"), { recursive: true });
writeFileSync(join(realSeat, "sessions", "s.jsonl"), lines(header(fy.proj), user("j1", null, "hidden", 1)));
chmodSync(join(realSeat, "sessions", "s.jsonl"), 0o000);
mkdirSync(join(fy.proj, ".pi", "state"), { recursive: true });
symlinkSync(realSeat, join(fy.proj, ".pi", "state", "linked"));
const spec = { agent: "linked", project: "proj", sessionsDir: join(fy.proj, ".pi", "state", "linked", "sessions") };
const reader2 = createReader({ roots: rootsFromSpecs([spec]) });
const cat2 = noWrites(fy.base, () => reader2.catalogue());
assert.deepEqual(cat2.conversations, []);
assert.deepEqual(cat2.refusedRoots, [{ seat: "linked", project: "proj", refusal: "unsafe-path" }]);
const root = rootsFromSpecs([spec])[0];
const r2 = noWrites(fy.base, () => reader2.open({ conversation: conversationId(root, "s.jsonl") }));
assert.deepEqual([r2.ok, r2.refusal.code], [false, "unknown-conversation"]);
});
test("F8: a file swapped for a symlink after the catalogue is refused", () => {
const fx = project();
const outside = forbidden(fx.base);
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("k1", null, "ok", 1)]);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
const r = openOne(fx, reader);
assert.equal(r.ok, true);
rmSync(path);
symlinkSync(outside, path);
const x = noWrites(fx.base, () => reader.open({ conversation: cat.conversations[0].conversation }));
assert.deepEqual([x.ok, x.refusal.code], [false, "unsafe-path"]);
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: r.page.branch }));
assert.deepEqual([f.ok, f.refusal.code], [false, "unsafe-path"]);
});
test("F9: registrations never add or redirect a root", () => {
const fx = project("seat-a");
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("l1", null, "ok", 1)]);
const outside = forbidden(fx.base);
const other = project("seat-a");
writeSession(other.dir, "o.jsonl", header(other.proj), [user("l2", null, "other project", 1)]);
const linkDir = join(fx.base, "link-to-other");
symlinkSync(other.dir, linkDir);
const reg = (extra) => ({ version: 1, seat: "seat-a", project: "proj", layout: "repo", harness: "claude-code", startedAt: time(0), ...extra });
const registrations = [
reg({ sessionsDir: join(fx.base, "outside") }), // outside every root
reg({ sessionsDir: linkDir }), // a symlink to another project's root
reg({ sessionsDir: other.dir }), // another project's directory
reg({ sessionsDir: fx.dir, project: "someone-else" }), // this directory, another project
reg({ seat: "seat-b", sessionsDir: join(fx.base, "outside") }), // a non-Pi placeholder outside the project
];
const roots = rootsFromSpecs([fx.spec], registrations);
assert.deepEqual(roots.map((r) => [r.seat, r.dir, r.harness, r.unsupportedReason]), [["seat-a", fx.dir, "pi", null]]);
const reader = createReader({ roots });
const cat = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual(cat.conversations.map((c) => [c.title, c.availability]), [["ok", "available"]]);
assert.ok(outside);
// A registration that matches the root narrows it and supplies launch time.
const matched = rootsFromSpecs([fx.spec], [reg({ harness: "pi", sessionsDir: fx.dir, startedAt: time(-5) })]);
assert.equal(matched[0].engineStartedAt, time(-5));
const cat2 = noWrites(fx.base, () => createReader({ roots: matched }).catalogue());
assert.equal(cat2.conversations[0].engineStartedAt, time(-5));
// Only repository specs are roots.
assert.deepEqual(rootsFromSpecs([
{ agent: "x", project: "fleet", sessionsDir: join(fx.base, "fleet", "x", ".pi", "agent", "sessions") },
{ agent: "seat-a", project: "wrong", sessionsDir: fx.dir },
{ agent: "seat-b", project: "proj", sessionsDir: fx.dir },
{ ...fx.spec, connector: "discord" },
]), []);
});
test("F10: a header cwd naming another project is refused", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(join(fx.base, "another-project")), [user("m1", null, "x", 1)]);
writeSession(fx.dir, "sub.jsonl", header(join(fx.proj, "packages", "x")), [user("m2", null, "sub", 1)]);
writeSession(fx.dir, "prefix.jsonl", header(fx.proj + "-evil"), [user("m3", null, "x", 1)]);
// Relative to the reader's own directory this names the project; it is still refused.
writeSession(fx.dir, "relative.jsonl", header(relative(process.cwd(), join(fx.proj, "packages")) || "."), [user("m5", null, "x", 1)]);
const linkRoot = join(fx.base, "compat");
symlinkSync(fx.proj, linkRoot);
writeSession(fx.dir, "link.jsonl", header(linkRoot), [user("m4", null, "via compat link", 1)]);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
const by = Object.fromEntries(cat.conversations.map((c) => [c.conversation, c]));
const root = rootsFromSpecs([fx.spec])[0];
for (const name of ["s.jsonl", "prefix.jsonl", "relative.jsonl"]) {
assert.deepEqual([by[conversationId(root, name)].availability, by[conversationId(root, name)].refusal], ["denied", "foreign-project"], name);
assert.deepEqual(openOne(fx, reader, name).refusal.code, "foreign-project", name);
}
assert.equal(openOne(fx, reader, "sub.jsonl").ok, true);
assert.equal(openOne(fx, reader, "link.jsonl").ok, true);
assert.equal(cwdInProject(undefined, fx.proj), false);
});
test("F11: parentSession renders with a marker and the parent is never opened", () => {
const fx = project();
const parent = forbidden(fx.base, "parent.jsonl");
writeSession(fx.dir, "s.jsonl", header(fx.proj, { parentSession: parent }), [user("n1", null, "forked", 1)]);
const r = openOne(fx, readerFor(fx));
assert.equal(r.ok, true);
assert.deepEqual(roles(r.page), ["notice", "user"]);
assert.match(texts(r.page)[0], /forked from an earlier session/);
assert.equal(r.view.forkedFromEarlierSession, true);
});
test("F12: two leaves: the default leaf is shown and the other branch reads alone", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("o1", null, "root", 1),
assistant("o2", "o1", "first answer", 2),
user("o3", "o2", "on branch one", 3),
assistant("o4", "o1", "second answer", 4),
]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
// o1's earliest child continues "main"; the later child o4 starts "b.o4".
assert.deepEqual(texts(r.page), ["root", "second answer"]);
assert.equal(r.page.branch, "b.o4");
assert.deepEqual(r.view.branches.map((b) => [b.branch, b.isDefault]), [["main", false], ["b.o4", true]]);
const other = openOne(fx, reader, "s.jsonl", { branch: "main" });
assert.deepEqual(texts(other.page), ["root", "first answer", "on branch one"]);
assert.equal(other.page.branch, "main");
assert.ok(other.page.entries.every((e) => e.branch === "main"));
for (const inner of ["o2", "b.o2", "o3", "b.o3", "o4"]) assert.equal(openOne(fx, reader, "s.jsonl", { branch: inner }).refusal.code, "unknown-branch", `${inner} is not a branch name`);
// Following a non-default branch after the default moves on adds nothing.
const path = join(fx.dir, "s.jsonl");
appendFileSync(path, lines(user("o5", "o4", "more on default", 5)));
const f = noWrites(fx.base, () => reader.next({ cursor: other.follow.id, conversation: other.page.conversation, branch: "main" }));
assert.deepEqual([f.ok, f.page.entries.length, f.page.branch, f.view.defaultBranch], [true, 0, "main", "b.o4"]);
// Following the default branch continues on it under the same name.
const g = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: "b.o4" }));
assert.deepEqual([texts(g.page), g.page.branch, g.view.defaultBranch], [["more on default"], "b.o4", "b.o4"]);
assert.ok([...r.page.entries, ...g.page.entries].every((e) => e.branch === "b.o4"), "one branch value across follows");
// The pre-growth name still opens, now with the growth.
assert.deepEqual(texts(openOne(fx, reader, "s.jsonl", { branch: "b.o4" }).page), ["root", "second answer", "more on default"]);
// A new default elsewhere: the old default view stays put.
appendFileSync(path, lines(assistant("o6", "o3", "back on branch one", 6)));
const h = noWrites(fx.base, () => reader.next({ cursor: g.follow.id, conversation: g.page.conversation, branch: "b.o4" }));
assert.deepEqual([h.page.entries.length, h.page.branch, h.view.defaultBranch], [0, "b.o4", "main"]);
// And the "main" view picks its growth up.
const k = noWrites(fx.base, () => reader.next({ cursor: f.follow.id, conversation: f.page.conversation, branch: "main" }));
assert.deepEqual([texts(k.page), k.page.branch], [["back on branch one"], "main"]);
});
test("F12: a follow refuses when an appended duplicate id changes the branch's earlier parts", () => {
// Where an id repeats the later entry wins, so an append can move an entry.
for (const [extra, why] of [
[[{ ...assistant("r2", "x1", "moved", 4) }], "r2 moves to the other root: main is shorter"],
[[{ ...assistant("r2", "x1", "moved", 4) }, assistant("r5", "r1", "in its place", 5)], "same length, another entry in r2's place"],
]) {
const fx = project();
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("r1", null, "one", 1), user("x1", null, "other root", 2), assistant("r2", "r1", "two", 3)]);
const reader = readerFor(fx);
const r = openOne(fx, reader, "s.jsonl", { branch: "main" });
assert.deepEqual(texts(r.page), ["one", "two"]);
appendFileSync(path, lines(...extra));
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: "main" }));
assert.deepEqual([f.ok, f.refusal?.code, f.refusal?.reconcile], [false, "source-replaced", true], why);
}
});
test("F12: a second root (Pi's resetLeaf) starts its own branch", () => {
const fx = project();
const path = writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("r1", null, "first root", 1),
assistant("r2", "r1", "first reply", 2),
user("r3", null, "second root", 3),
]);
const reader = readerFor(fx);
const r = openOne(fx, reader);
assert.deepEqual([texts(r.page), r.page.branch], [["second root"], "b.r3"]);
assert.deepEqual(r.view.branches.map((b) => [b.branch, b.isDefault]), [["main", false], ["b.r3", true]]);
assert.deepEqual(texts(openOne(fx, reader, "s.jsonl", { branch: "main" }).page), ["first root", "first reply"]);
appendFileSync(path, lines(assistant("r4", "r3", "second reply", 4)));
const f = noWrites(fx.base, () => reader.next({ cursor: r.follow.id, conversation: r.page.conversation, branch: "b.r3" }));
assert.deepEqual([texts(f.page), f.page.branch], [["second reply"], "b.r3"]);
});
test("F13: compaction is a marker in place, then the retained content", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [
user("p1", null, "early", 1),
assistant("p2", "p1", "early answer", 2),
{ type: "compaction", id: "p3", parentId: "p2", timestamp: time(3), summary: "they talked", firstKeptEntryId: "p2", tokensBefore: 1000 },
user("p4", "p3", "after", 4),
]);
const r = openOne(fx, readerFor(fx));
assert.deepEqual(roles(r.page), ["user", "assistant", "compaction", "user"]);
const marker = r.page.entries[2].content[0];
assert.deepEqual([marker.type, marker.summary, marker.nativeEntry], ["compaction", "they talked", "p3"]);
});
test("F14: long strings split into fragments and parts, reassemble exactly, and pages respect the byte cap", () => {
const fx = project();
const long = "x".repeat(LIMITS.chars + 1);
const emoji = "😀".repeat(LIMITS.chars + 3); // 4 bytes each: splits on bytes before characters
const blocks = Array.from({ length: 70 }, (_, i) => ({ type: "text", text: `block ${i}` }));
const wide = "€".repeat(200_000); // 3 bytes each, ~600 KB per part
const entries = [
user("q1", null, long, 1),
assistant("q2", "q1", emoji, 2),
msg("q3", "q2", "assistant", blocks, { stopReason: "stop" }, 3),
];
for (let i = 4; i < 44; i++) entries.push(user(`q${i}`, `q${i - 1}`, wide, i));
writeSession(fx.dir, "s.jsonl", header(fx.proj), entries);
const reader = readerFor(fx);
const pages = readAll(fx, reader, openOne(fx, reader));
for (const p of pages) {
assert.ok(p.page.entries.length <= LIMITS.parts);
assert.ok(Buffer.byteLength(JSON.stringify(p.page), "utf8") <= LIMITS.pageBytes, "page within 8 MiB");
for (const e of p.page.entries) {
assert.ok(e.content.length <= LIMITS.blocks);
for (const b of e.content) assert.ok([...(b.text ?? "")].length <= LIMITS.chars);
}
}
assert.ok(pages.length >= 3, "the multibyte parts fill pages on bytes before 100 parts");
assert.ok(pages[1].page.entries.length < LIMITS.parts);
// Reassemble per native entry: parts in order, fragments in order.
const all = pages.flatMap((p) => p.page.entries);
const rebuilt = new Map();
for (const e of all) {
const blocksOf = rebuilt.get(e.nativeEntry) ?? [];
for (const b of e.content) blocksOf[b.block] = (blocksOf[b.block] ?? "") + b.text;
rebuilt.set(e.nativeEntry, blocksOf);
}
assert.equal(rebuilt.get("q1")[0], long);
assert.equal(rebuilt.get("q2")[0], emoji);
assert.deepEqual(rebuilt.get("q3"), blocks.map((b) => b.text));
assert.equal(rebuilt.get("q20")[0], wide);
const q1 = all.filter((e) => e.nativeEntry === "q1");
assert.deepEqual(q1.flatMap((e) => e.content.map((b) => [b.fragment, b.lastFragment])), [[0, false], [1, true]]);
const q3 = all.filter((e) => e.nativeEntry === "q3");
assert.deepEqual(q3.map((e) => [e.part, e.lastPart, e.content.length]), [[0, false, 64], [1, true, 6]]);
assert.equal(new Set(all.map((e) => e.id)).size, all.length);
});
test("fragments never cut a surrogate pair and keep an empty string", () => {
assert.deepEqual(fragments(""), [""]);
const s = "a" + "😀".repeat(LIMITS.chars);
const parts = fragments(s);
assert.equal(parts.join(""), s);
for (const p of parts) {
assert.ok(!/^[\udc00-\udfff]/.test(p) && !/[\ud800-\udbff]$/.test(p));
assert.ok(Buffer.byteLength(JSON.stringify(p)) <= 1024 * 1024 + 2);
}
});
test("F15: a Claude seat is an unsupported-harness placeholder whose directory is never read", () => {
const fx = project("pi-seat");
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("r1", null, "ok", 1)]);
// Rocko's case: a claude-code registration and no Pi sessions directory.
const claudeDir = join(fx.proj, ".pi", "state", "claude-seat", "sessions");
const regs = [{ version: 1, seat: "claude-seat", project: "proj", layout: "repo", harness: "claude-code", sessionsDir: claudeDir, startedAt: time(-1) }];
// And a spec'd seat with sessions whose registration says claude-code.
const both = join(fx.proj, ".pi", "state", "mixed", "sessions");
mkdirSync(both, { recursive: true });
writeFileSync(join(both, "s.jsonl"), lines(header(fx.proj), user("r2", null, "never read", 1)));
chmodSync(join(both, "s.jsonl"), 0o000);
regs.push({ version: 1, seat: "mixed", project: "proj", layout: "repo", harness: "claude-code", sessionsDir: both, startedAt: time(-1) });
const roots = rootsFromSpecs([fx.spec, { agent: "mixed", project: "proj", sessionsDir: both }], regs);
const reader = createReader({ roots });
const cat = noWrites(fx.base, () => reader.catalogue());
const unsupported = cat.conversations.filter((c) => c.availability === "unsupported");
assert.deepEqual(unsupported.map((c) => [c.seat, c.harness, c.unsupportedReason, c.history]).sort(), [["claude-seat", "claude-code", UNSUPPORTED_HARNESS, null], ["mixed", "claude-code", UNSUPPORTED_HARNESS, null]]);
for (const c of unsupported) {
const r = noWrites(fx.base, () => reader.open({ conversation: c.conversation }));
assert.deepEqual([r.ok, r.refusal.code, r.refusal.reconcile], [false, UNSUPPORTED_HARNESS, false]);
}
});
test("unknown conversations, empty files and non-Pi files refuse", () => {
const fx = project();
writeFileSync(join(fx.dir, "empty.jsonl"), "");
writeFileSync(join(fx.dir, "notpi.jsonl"), lines({ hello: 1 }));
writeFileSync(join(fx.dir, "header-only.jsonl"), lines(header(fx.proj)));
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual(cat.conversations.map((c) => [c.availability, c.refusal]).sort(), [["available", null], ["unavailable", "incomplete-header"], ["unavailable", "not-a-pi-session"]]);
assert.equal(openOne(fx, reader, "empty.jsonl").refusal.code, "incomplete-header");
assert.equal(openOne(fx, reader, "notpi.jsonl").refusal.code, "not-a-pi-session");
const empty = openOne(fx, reader, "header-only.jsonl");
assert.deepEqual([empty.ok, empty.page.entries.length, empty.page.branch, empty.view.branches], [true, 0, "main", [{ branch: "main", isDefault: true, lastActivityAt: null }]]);
appendFileSync(join(fx.dir, "header-only.jsonl"), lines(user("s1", null, "first", 1)));
const f = noWrites(fx.base, () => reader.next({ cursor: empty.follow.id, conversation: empty.page.conversation, branch: "main" }));
assert.deepEqual([texts(f.page), f.page.branch], [["first"], "main"]);
const u = noWrites(fx.base, () => reader.open({ conversation: "pi-" + "f".repeat(32) }));
assert.deepEqual([u.refusal.code, u.refusal.reconcile], ["unknown-conversation", true]);
});
test("an unreadable file or root inside the roots is refused per row, not a failed catalogue", () => {
const fx = project();
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("u1", null, "ok", 1)]);
writeSession(fx.dir, "locked.jsonl", header(fx.proj), [user("u2", null, "locked", 1)]);
chmodSync(join(fx.dir, "locked.jsonl"), 0o000);
const reader = readerFor(fx);
const cat = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual(cat.conversations.map((c) => [c.availability, c.refusal]).sort(), [["available", null], ["denied", "unreadable"]]);
assert.equal(openOne(fx, reader, "locked.jsonl").refusal.code, "unreadable");
chmodSync(fx.dir, 0o300);
try {
const cat2 = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual([cat2.conversations, cat2.refusedRoots.map((r) => r.refusal)], [[], ["unreadable"]]);
} finally {
chmodSync(fx.dir, 0o700);
}
});
test("a seat directory without search permission refuses that root, not the catalogue", () => {
const fx = project("good");
writeSession(fx.dir, "s.jsonl", header(fx.proj), [user("p1", null, "readable", 1)]);
const badDir = join(fx.proj, ".pi", "state", "bad", "sessions");
mkdirSync(badDir, { recursive: true });
writeSession(badDir, "s.jsonl", header(fx.proj), [user("q1", null, "hidden", 1)]);
const bad = { agent: "bad", project: "proj", sessionsDir: badDir };
const goodId = conversationId(rootsFromSpecs([fx.spec])[0], "s.jsonl");
chmodSync(join(fx.proj, ".pi", "state", "bad"), 0o000);
try {
for (const specs of [[bad, fx.spec], [fx.spec, bad]]) {
const reader = createReader({ roots: rootsFromSpecs(specs) });
const cat = noWrites(fx.base, () => reader.catalogue());
assert.deepEqual([cat.conversations.map((c) => [c.seat, c.availability]), cat.refusedRoots], [[["good", "available"]], [{ seat: "bad", project: "proj", refusal: "unreadable" }]]);
const r = noWrites(fx.base, () => reader.open({ conversation: goodId }));
assert.deepEqual(texts(r.page), ["readable"]);
const u = noWrites(fx.base, () => reader.open({ conversation: "pi-" + "e".repeat(32) }));
assert.equal(u.refusal.code, "unknown-conversation");
}
} finally {
chmodSync(join(fx.proj, ".pi", "state", "bad"), 0o700);
}
});
test("every page and cursor is a valid CHAT-01 record", () => {
assert.ok(records.length > 50);
const script = `
import json, sys
from jsonschema import Draft202012Validator, FormatChecker
s = json.load(open(sys.argv[1]))
bad = []
for i, r in enumerate(json.load(sys.stdin)):
v = Draft202012Validator({"$defs": s["$defs"], "$ref": "#/$defs/" + r["kind"]}, format_checker=FormatChecker())
for e in v.iter_errors(r):
bad.append(f"{i} {r['kind']}: {e.message[:200]}")
break
print(json.dumps(bad))
`;
const run = spawnSync("python3", ["-c", script, schemaPath], { input: JSON.stringify(records), encoding: "utf8", maxBuffer: 1 << 30 });
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout), []);
});