Read a session file and its tree
See a session as JSONL: a header, entries linked by id and parentId, a computed directory name, and a context_edit that hides without deleting.
A persistent Pi session is a plain JSONL file you can open in any editor. The
first line is a header, every later line is a tree entry with an id and a
parentId, and the model only ever sees the active branch. This is an
illustrative trace plus a directory-name calculation, grounded in the
Chapter 5 tests; it is a drawing of the shape, not a transcript from your machine.
The first line is a header
Entries form a tree. For a linear conversation each entry’s parent is the entry before it, and the header is not part of the tree:
{"type":"session","version":3,"id":"<session-id>"}
{"type":"message","id":"a1b2c3d4","parentId":null,"timestamp":"2026-10-01T09:00:00.000Z","message":{"role":"user","content":"question A"}}
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2026-10-01T09:00:01.000Z","message":{"role":"assistant","content":"answer A"}}
{"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2026-10-01T09:00:02.000Z","message":{"role":"user","content":"question B"}}The test asserts this on the real file: header first, then one entry per message
in order, ids unique, and the first entry’s parentId is null. It also shows
stored is not sent: model_change, thinking_level_change and
appendCustomEntry entries sit in the file and never reach the provider.
The directory name is a calculation
The default location is
~/.pi/agent/sessions/--<path>--/<timestamp>_<session-id>.jsonl. Pi drops the
leading separator of the working directory, then replaces /, \ and : with
-:
| Working directory | Directory component |
|---|---|
C:\Projects\pi |
--C--Projects-pi-- |
/home/u/app |
--home-u-app-- |
The drive letter stays; no separator and no colon survives. The session id is a UUID by default, which the filename pattern in the test confirms.
A context edit hides without deleting
Appending a context_edit with replacement: null omits the target from future
requests and leaves the entry in the file:
{"type":"context_edit","id":"g6h7i8j9","parentId":"f6g7h8i9","timestamp":"2026-10-01T09:00:03.000Z","targetId":"a1b2c3d4","replacement":null}The edit is branch-relative: navigating to a point before it (the assistant’s
answer to A) reveals the original contribution again. A replacement object such
as { "content": "REDACTED" } rewrites the message for the model and leaves the
stored entry untouched; the replacement must be an object with content, not a
bare string.
Mechanism and limitations
Documented: the session-file format and location formula in the pinned
session-format.md, and the base entry fields (type, id, parentId,
timestamp) declared in examples/ch27-session-entries/declared.ts. That
declaration is Chapter 27’s subject and is referenced here, not reproduced.
Observed: disk.test.ts reads the real file written by the shared history and
asserts the header order, the tree links, the stored-not-sent entries, the
branch-relative edit, and the directory name on the shipped binary.
The trace is illustrative and its ids are shortened; it shows the entry shapes, not a complete file. Stored is not sent in either direction: an entry in the file is not proof the model saw it, and the active branch — after compaction and edits — is what a request contains. Chapter 24 covers branching and Chapter 27 the full format.
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.