What is actually in a Pi session file
Pi records every session as a JSONL file under ~/.pi/agent/sessions, and
unlike Claude Code and Codex it tells you what goes in it: Pi is open source and
documents its own session format. These are notes from reading that code and
my own files while teaching bough to read them, and from the places where
the documentation was right and a reader could still go wrong.
The corpus behind them is small: a handful of sessions on one machine, the largest 70 lines, across three providers. That is fine here in a way it would not be for the other two, because every shape below was checked against the code that writes it rather than inferred from a sample.
If you have read the same notes for Claude Code or Codex, the headline is familiar: Pi writes the same work more than once too, just in a different place.
Where the files live
~/.pi/agent/sessions/--<cwd>--/<timestamp>_<session id>.jsonl
One folder per working directory, with the path flattened into its name:
every /, \ and : becomes -. That cannot be undone, since a dash in the
name could have been any of the four, so the real path comes from the header on
the first line of the file. PI_CODING_AGENT_DIR and
PI_CODING_AGENT_SESSION_DIR move all of it somewhere else.
A session is a tree
Every entry has an id and a parentId. Most of the time they form a straight
line. Go back to an earlier point with /tree and carry on, and the next entry
becomes a second child of that point, in the same file.
Pi works out what the model sees by walking back from the newest entry, so the branch you left behind drops out of its context. It does not drop out of what happened. Those prompts were typed and answered, and they were paid for. Read the file in order and count everything, and treat an entry whose parent is not the one before it as the moment somebody jumped.
Forks copy everything
/fork starts a new session file holding the whole conversation so far, every
entry under its original id and timestamp, and the new header names the file it
came from. /clone does the same with just the path to where you are.
Read both files as they stand and everything before the fork is counted twice:
prompts, tokens, cost. The fix is to leave copied entries to the file they came
from. Match on id and timestamp together, because ids are only eight hex
characters and unique within one file. And keep the copy when the original has
been deleted, since it is then the only record left.
This is Pi's version of the problem the other two have. Claude Code replays records inside one file, Codex replays them into a new file when a session resumes, and Pi copies them into a new file when you fork.
A skill prompt is not what you typed
Run /skill:review this file and the message saved as your prompt holds the
skill's entire text wrapped in a <skill> tag, followed by "this file". Show
that as the prompt and a page of instructions you never wrote becomes the label
on your work. The command you typed is recoverable from the tag, and that is the
honest thing to show.
The cost is written down, mostly
Every reply carries its usage and, unlike the other two, a dollar figure Pi worked out itself:
{"input":6108,"output":68,"cacheRead":0,"cacheWrite":0,"reasoning":21,"totalTokens":6176,"cost":{"total":0.0013032}}
The four counts do not overlap, so they add up as they are, and reasoning is
already inside output. That makes Pi the easiest of the three to price, with
two things to watch.
Work done by a sub-agent is not in Pi's totals. Pi has no sub-agents of its own; an extension adds them, runs each as a separate process that saves no session, and hands back everything it did inside the tool result. Pi does not add that to the session's usage. Count it from the result or a delegated task looks free.
And price by the model you asked for, not the one that answered. A reply records
both, and they differ for dull reasons: OpenRouter reports its free models
without the :free on the end, so pricing by the answering name bills free work
at the paid rate.
On the session I checked it against, across three providers, bough's figure priced from its own table came out the same as the one Pi recorded, to the cent.
Failed requests are there too
A request the provider refused is saved as an assistant message with empty
content, all-zero usage and an errorMessage. It costs nothing and opens
nothing, but it is in the file, so a reader counting replies has to know to
skip it. Two of the replies in my own session were exactly this: one provider
out of extra usage, and another at its limit.
Local models look like everyone else
A model running on your own machine turns up under whatever provider name you
gave it in Pi's models.json, ollama for instance. The session records the
provider and the model and nothing about where the server was. models.json
is the one place that says a model was local, and even there a local address
is not proof: a proxy on localhost forwarding to a hosted model looks exactly
the same.
The fuller version
These notes are kept next to the code that depends on them, at the Pi session file format, along with the entry types, the tool arguments and where the tokens hide. The Claude Code and Codex formats are written up the same way.