bough
What is in a Pi session file All documents

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.