Read a session file with the chapter's program
Run the chapter's session reader against a real JSONL file and see the header, the id/parentId tree, and the two timestamp formats.
A Pi session is a JSONL file: one JSON object per line, a session header first,
then entries linked by id and parentId. This format is the shared source
for the session claims in Chapters 5, 6, 23 and 24 — they reference its
declaration rather than redefining it — so a working reader is the most direct way
to see what they all mean. Below is Chapter 27’s own reading program on 1.0.4,
reproduced exactly.
Run the chapter’s reader
read-session.ts walks the file, not the branch, printing one line per known entry
type. It is the chapter’s example:, in full:
import { readFileSync } from "fs";
const lines = readFileSync(process.argv[2] ?? "session.jsonl", "utf8").trim().split("\n");
for (const line of lines) {
const entry = JSON.parse(line);
switch (entry.type) {
case "session":
console.log(`Session v${entry.version ?? 1}: ${entry.id}`);
break;
case "message":
console.log(`[${entry.id}] ${entry.message.role}: ${JSON.stringify(entry.message.content)}`);
break;
case "compaction":
console.log(`[${entry.id}] Compaction: ${entry.tokensBefore} tokens summarized`);
break;
case "branch_summary":
console.log(`[${entry.id}] Branch from ${entry.fromId}`);
break;
case "usage":
console.log(`[${entry.id}] Usage (${entry.kind}): ${entry.usage.totalTokens} tokens`);
break;
case "custom":
console.log(`[${entry.id}] Custom (${entry.customType}): ${JSON.stringify(entry.data)}`);
break;
case "custom_message":
console.log(`[${entry.id}] Extension message (${entry.customType}): ${entry.content}`);
break;
case "label":
console.log(`[${entry.id}] Label "${entry.label}" on ${entry.targetId}`);
break;
case "model_change":
console.log(`[${entry.id}] Model: ${entry.provider}/${entry.modelId}`);
break;
case "thinking_level_change":
console.log(`[${entry.id}] Thinking: ${entry.thinkingLevel}`);
break;
}
}Run node read-session.ts <session.jsonl> (a bare session.jsonl is the default)
against a file that follows the format below.
How the file is shaped
Two facts the reader depends on, asserted on the real file. The first line is
a session header with "version": 3 and no parentId — metadata, not part of
the tree. And there are two timestamp formats in one file: the entry’s
timestamp is ISO ("2026-10-01T09:00:01.000Z"), while the nested
message.timestamp is Unix milliseconds (1733234401000); same instant, two
formats.
The declaration it reads against
Every non-header entry carries the base fields; this declaration is what Chapters 5, 6, 23 and 24 cross-reference:
export interface SessionEntryBase {
type: string;
id: string; // Usually an 8-char hex ID; may fall back to a full UUID
parentId: string | null; // null for a root entry
timestamp: string; // ISO timestamp
}Mechanism and limitations
Documented: the JSONL container, the header-first rule, and the base entry
fields come from the pinned session-format.md and this declaration. Observed:
entries.test.ts builds a persisted session with a faux provider, then runs this
exact reader with Node against the real file and asserts the header-first shape,
the v3 version, the tree links, the two timestamp formats, and the reader’s own
output lines (Session v3:, user:, assistant:, Model: faux/).
The reader shows stored entries, not what a request contained: compaction
selects a range and context_edit rewrites entries — Chapters 23 and 5 own those
mechanisms; this page does not re-derive them. The example never runs a model:
its subject is the file format that records a run.
Understand this example
Copy this prompt into your AI tool. No code runs here.
Apply this example
Copy this prompt into your AI tool. No code runs here.