Example

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.

Full source and test .

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.