pi¶
pi is the pi coding agent CLI from Earendil Works (@earendil-works/pi-coding-agent), a
terminal agent with one per-user directory, ~/.pi/agent, on every OS, where it writes one
append-only JSONL file per session, a tree of entries linked by id/parentId. Two other
catalog agents reuse its storage and format: little-coder, a launcher that runs pi itself and
writes into the same ~/.pi/agent (see little-coder.md), and Letta Code's
local backend, which writes pi's session format with its own writer into its own directory (see
letta.md); Wrapper attribution says how to tell them apart.
Catalog agent: pi. Parsed: the pi parser reads the session files, trust.json, the
defaultProjectTrust setting, the debug and crash logs and the experimental session metadata.
Source and evidence¶
earendil-works/pi (the monorepo for the pi coding agent
CLI, @earendil-works/pi-coding-agent 1.0.2), MIT licence
(LICENSE:1),
open source, TypeScript. Reviewed commit
200387122ca450d6387f033949423114a270b96c
(2026-10-04). Where the layout or the schema differs, tag v0.83.0
(845d6ff1f6643aba440341cce877ce1c43ebbc39),
the version little-coder pins, is cited. All claims from source.
Observed¶
- No real install: nothing was installed or run. No real install has shown a
pi.bug-reportentry, so the rows built from it are fixture only (Sync, relay and origin).
Source reviews¶
- 2026-10-21: earendil-works/pi at
5c1736f(coding-agent 1.3.0) and tagv0.83.0: the parser now readsdefaultProjectTrust,pi-debug.log,crashes.jsonand little-coder's permission-gate refusals, so Permission rows and Past-use rows and secondary sources say what it writes;pi-debug.logturned out to exist at v0.83.0 too, and Source host rows was added. - 2026-10-19: earendil-works/pi at
5c1736f, the head ofmain(coding-agent 1.3.0, packages/coding-agent/package.json:3), compared file by file with2003871. The review addeddefaultProjectTrustand--approveto Project trust,sessionShareDestinationto Sync, relay and origin rows, the current contents of the debug and crash logs to Other stores,durationMsandargumentsErrorto Format versions, and the experimental server's removal to Experimental durable sessions.
Collection¶
Per-user storage¶
One layout on every OS. The agent directory is $PI_CODING_AGENT_DIR (with ~ expanded) or
os.homedir()/.pi/agent
(packages/coding-agent/src/config.ts:566-572;
tilde expansion
utils/paths.ts:88-93).
No XDG, no AppData, no Library: Windows is C:\Users\<u>\.pi\agent. The .pi name and the
env-var prefix come from piConfig in the package's package.json
(config.ts:538-547,
package.json:6-8),
so a rebranded build (the source comment's example is TAU_CODING_AGENT_DIR) uses another directory
and is not covered by the catalog lines.
Under ~/.pi/agent:
sessions/--<cwd>--/<ISO timestamp>_<session id>.jsonl: one JSONL transcript per session (config.ts:610-612;core/session-manager.ts:589-594,1079-1080). The directory name is the cwd with a leading/or\dropped and every/,\,:replaced by-:/home/alice/proj→--home-alice-proj--,C:\Users\alice\proj→--C--Users-alice-proj--. The file name is the ISO time with:and.replaced by-. A file is only created once a user or assistant message exists (1161-1170). The directory can be moved with--session-dir,$PI_CODING_AGENT_SESSION_DIRor thesessionDirsetting, in that order (main.ts:688-692); a custom directory is flat, with no per-cwd subdirectory (session-manager.ts:1755-1757). Sessions written by v0.30.0 sat directly in~/.pi/agent/*.jsonluntil a migration moved them (migrations.ts:76-84).settings.json(global settings;packages,extensions,skills,sessionDir,httpProxy, default provider and model) (config.ts:590-592;core/settings-manager.ts:133-138,159-180),keybindings.json(core/keybindings.ts:379-380). Since5c1736fsettings.jsonalso holdsdefaultProjectTrust(Project trust) andsessionShareDestination, where/shareuploads (Sync, relay and origin rows).auth.json,models.json,models-store.json(dynamically refreshed provider model catalogs) (config.ts:580-587;core/models-store.ts:52).trust.json: project trust decisions keyed by absolute path (Project trust).mcp.json(MCP servers),mcp-auth.json(MCP OAuth state),mcp.log(extensions/mcp/config.ts:146-148;extensions/mcp/oauth.ts:137-145;extensions/mcp/index.ts:351). Not present at v0.83.0.crashes.json, the recent crashes (not present at v0.83.0), andpi-debug.log, written by/debug(present at v0.83.0 as well) (Debug log and crash log).- User resources:
extensions/,skills/,prompts/(formerlycommands/),themes/,AGENTS.md/CLAUDE.mdglobal context,SYSTEM.md,APPEND_SYSTEM.md(core/resource-loader.ts:978-982,1207,1221,242-246;migrations.ts:137-143). Skills are also read from~/.agents/skills(core/trust-manager.ts:187-188), already in the catalog asshared|.agents. - Installed packages (
pi install):npm/(an npm root withpackage.jsonandnode_modules/<name>) andgit/<host>/<path>checkouts (core/package-manager.ts:2134,2154-2174); temporary checkouts intmp/extensions/(231-236). bin/(downloadedfdandrg), legacytools/(config.ts:595-602;migrations.ts:176-185).
Scratch directories¶
None known: pi creates none under the home; it runs in the user's cwd. At
5c1736f
its output files go to the system temporary directory through one helper, now mode 0600
(utils/output-files.ts:14-34),
outside every catalog line.
Truncated bash output is spilled to <tmpdir>/pi-bash-<16 hex>.log
(core/bash-executor.ts:68-69),
MCP and codemode output to pi-mcp-* and pi-codemode-*.txt
(extensions/mcp/tools.ts:72;
extensions/codemode/execute.ts:263);
the session records the path in fullOutputPath.
Experimental stores¶
Experimental (PI_EXPERIMENTAL=1,
core/experimental.ts:1-3;
experimental/commands.ts:86),
not at v0.83.0: experimental/sessions/<id>/{meta.json,session.sqlite}
(experimental/server.ts:69-71;
experimental/session-catalog.ts:5-16),
experimental/vacation-sessions/<sha256(cwd)[:24]>/<ms>-<uuid>/session.sqlite
(experimental/vacation/sessions.ts:18-55),
and a server directory $PI_SERVER_DIR or ~/.pi/server holding default-server-id, lock
directories and Unix sockets
(experimental/server.ts:51-56,
85-91).
At 5c1736f these stores are no longer written
(Experimental durable sessions); they remain only where an
older build ran.
Rewrites on open¶
pi rewrites its own evidence. Opening an older-format session rewrites the whole file in the current
format
(session-manager.ts:1088-1094),
and loading a file whose last line has no newline appends one
(668).
Collect before anyone resumes a session on the host. What the session picker's delete removes, and
what survives it, is in What survives a delete.
Credentials¶
~/.pi/agent/auth.json, created mode 0600 in a 0700 directory: one object per provider,{type:"api_key", key, env}or{type:"oauth", refresh, access, expires, ...}(core/auth-storage.ts:24-25,52-60;packages/ai/src/auth/types.ts:17-37). No keychain. The code read writes no backup or second copy of the file; the catalog's.pi/agent/auth.json.*secret glob guards only against an editor's copy.~/.pi/agent/mcp-auth.json: MCP client registrations, tokens and pending PKCE verifiers (extensions/mcp/oauth.ts:137-145).~/.pi/agent/models.json: custom providers may carryapiKeyandheaders(core/model-config.ts:242-251); a value is a literal, an env reference, or!commandwhose stdout is used (core/resolve-config-value.ts:140-145). Flag it.~/.pi/agent/mcp.jsonand project.pi/mcp.json:headerssuch asAuthorization: Bearer ...(extensions/mcp/config.ts:14-21). Flag it.- Legacy:
oauth.jsonandsettings.jsonapiKeyswere folded intoauth.json; the old file is renamedoauth.json.migrated, not deleted (migrations.ts:20-60). - pi also reads, but does not write,
~/.config/gcloud/application_default_credentials.json(packages/ai/src/env-api-keys.ts:60-67) and the Hugging Face token (extensions/llama/huggingface.ts:51-55); they belong to those tools.
Exclusions¶
bin/ holds third-party binaries. npm/node_modules, the git/ checkouts and tmp/ are installed
extension packages; the package sources stay recorded in settings.json packages and
npm/package.json
(package-manager.ts:2068),
so exclude only their dependency trees and .git. Project .pi/npm and .pi/git are the same at
project scope
(2130-2132,
2169-2172).
Sockets in ~/.pi/server cannot be copied. Sessions themselves stay: images are inline base64 and
can be large, but they are the transcript.
Project-local files¶
<cwd>/.pi/ holds settings.json
(settings-manager.ts:301),
mcp.json, extensions/, skills/, prompts/, themes/, SYSTEM.md, APPEND_SYSTEM.md,
npm/, git/
(trust-manager.ts:30-39;
resource-loader.ts:984-988).
Project extensions run with the user's rights once the folder is trusted, so they are evidence.
little-coder adds .pi/approved-plan.md
(Project-local files in little-coder.md).
Context files read from the cwd up: AGENTS.override.md, AGENTS.md, AGENTS.MD, CLAUDE.md,
CLAUDE.MD
(resource-loader.ts:183-185),
and .agents/skills up to the git root
(package-manager.ts:467-480).
Exports land in the cwd by default: /export HTML as pi-session-<session file basename>.html
(core/export-html/index.ts:274-277)
and JSONL as session-<ISO time>.jsonl
(core/session-export.ts:37-43).
No per-project database.
Where the project path is recorded¶
- Session header (line 1 of each file), key
cwd(session-manager.ts:43-50); the directory name is a lossy copy. trust.json: every key is an absolute project path (Project trust).crashes.json:cwdandsessionFileper record (Debug log and crash log).experimental/sessions/*/meta.json:cwd(session-catalog.ts:6-13).
Parsing¶
Transcript stores¶
.pi/agent/sessions/--<encoded cwd>--/<ISO time, : and . as ->_<session id>.jsonl, e.g.2026-10-01T09-00-00-000Z_0199a1b2-....jsonl, named as in Per-user storage. One file per session; forks and branches get new files (session-manager.ts:1077-1080).- With
--session-dir,PI_CODING_AGENT_SESSION_DIRor thesessionDirsetting the files sit flat in that directory, anywhere on disk (Per-user storage). - Legacy:
.pi/agent/*.jsonl(v0.30.0 bug, moved on next start). - Exports in project directories:
session-<time>.jsonl(same format, one branch) andpi-session-*.html(Project-local files). - Experimental durable sessions are SQLite, Other stores.
What survives a delete¶
The session picker's delete runs the trash command and falls back to unlinking the session file
(session-selector.ts:650-680;
unchanged at 5c1736f,
session-selector.ts:650-680),
so the file is gone or sits in the user's trash, with any pi.bug-report entry inside it; no index
or log repeats that entry, and pi offers no way to delete the remote copy that /share or a bug
report uploaded. pi keeps no per-prompt history file (none in pi at 5c1736f: a grep of
packages/coding-agent/src for history.jsonl and promptHistory finds none).
What remains is pi-debug.log, whose last /debug snapshot can hold one session's messages, and
crashes.json, with each recent crash's sessionFile and cwd
(Debug log and crash log); both survive the delete.
Whether the delete touches the exports a user wrote into project
directories (session-<time>.jsonl, pi-session-*.html) is not
stated. pi writes no deletion marker. The analyzer reads the session
files, trust.json, pi-debug.log as a secondary source, whose messages it keeps when no session
file holds them, and crashes.json, whose rows name a deleted session's file and directory (see
Secondary sources).
Record schema¶
Every line is one JSON object. Malformed lines are skipped by pi (session-manager.ts:616-624).
Header¶
Line 1: type:"session", version (3; absent in v1), id (session id), timestamp (ISO 8601 UTC
with ms and Z, from toISOString), cwd, optional parentSession (absolute path of the session
file this one was forked or branched from)
(43-50,
1061-1069).
The session id is a UUIDv7
(264-266)
unless given with --session-id
(main.ts:448,
session-manager.ts:268-274).
There is no producer name or pi version in the header.
Entries¶
type, id (8 hex characters,
276-284),
parentId (null at a root), timestamp (ISO, written at append time)
(57-62,
1204-1213).
Types
(64-195):
type |
Fields |
|---|---|
message |
message: one of the roles below |
thinking_level_change |
thinkingLevel |
model_change |
provider, modelId |
usage |
kind (e.g. cache_warm), provider, model, usage, note; HEAD only |
compaction |
summary, firstKeptEntryId, tokensBefore, details, usage, fromHook, systemMessage (HEAD only) |
branch_summary |
fromId, summary, details, usage, fromHook |
custom |
customType, data: extension state, not in model context |
custom_message |
customType, content (string or text/image parts), display, details: extension-injected context |
context_edit |
targetId, replacement ({content} or null = omitted from context); HEAD only |
label |
targetId, label (undefined clears) |
session_info |
name (user-set session title) |
Bug-report entry¶
/bug (HEAD only; absent at v0.83.0) appends a custom entry with customType:"pi.bug-report" to
the session file once a report is uploaded or exported
(modes/interactive/bug-report.ts:213-227,
core/bug-report.ts:17).
Its data is id (the report id, a UUIDv7 made locally,
core/bug-report.ts:159-160),
createdAt (ISO), hint (the user's description or null), sessionIncluded and summaryIncluded
(booleans), delivery (upload or zip) and, for zip, path, the archive
pi-bug-report-<id>.zip in the working directory
(core/bug-report.ts:235-243,
278-280).
The entry's own timestamp is the append time
(session-manager.ts:1290-1301).
A session never reported has no such entry.
Message roles¶
message.message.role, every one with timestamp
in Unix milliseconds:
user:contentstring or[{type:"text",text}|{type:"image",data,mimeType}](types.ts:542-546, 397-417). Images are inline base64.assistant:content[]oftext(text,textSignature),thinking(thinking,thinkingSignature,redacted),toolCall(id,name,argumentsobject,thoughtSignature,namespace);api,provider,model,responseModel,responseId,thinkingLevel,diagnostics,usage(input,output,cacheRead,cacheWrite,totalTokens,cost{...}),stopReason(pending|stop|length|toolUse|error|aborted|deferred),errorMessage(types.ts:403-452, 548-572).toolResult:toolCallId,toolName,content[](text/image),details(tool-specific, e.g. editdiff/patch, bashfullOutputPath),usage,nestedCalls(calls a codemode script made),isError(596-610; tools/edit.ts:70-77, tools/bash.ts:66-69). An extension that blocks a call makes the agent loop record an errortoolResultwhose text is the extension's reason (packages/agent/src/agent-loop.ts:745-753, 912-917); little-coder's permission gate is such an extension.system:content(the system prompt on the first one, added instructions later),sections,toolsAdded,toolsRemoved(types.ts:524-540). Persisted since the system-message change (agent-session.ts:1126-1133); v0.83.0 persisted only user, assistant and toolResult (0.83.0 agent-session.ts:635-641).bashExecution: a user!command:command,output,exitCode,cancelled,truncated,fullOutputPath,excludeFromContext(messages.ts:29-40; agent-session.ts:3848).custom(v2hookMessagerenamed),branchSummary,compactionSummary(messages.ts:46-67).
Tool arguments¶
Built-in tool arguments: bash command, timeout
(tools/bash.ts:40-43);
read path, offset, limit
(read.ts:14-18);
write path, content
(write.ts:11-14);
edit path, edits[{oldText,newText}]
(edit.ts:21-37);
grep pattern, path, glob
(grep.ts:21-24);
find pattern, path
(find.ts:26-30);
ls path
(ls.ts:11-12);
codemode code
(extensions/codemode/tool.ts:87-90).
Joins and timestamps¶
- Session: header
id, also the file-name suffix. Project: headercwd. - Tool use to result:
toolCall.id=toolResult.toolCallId. - Tree:
parentId→id; the file holds every branch; the active path is the walk up from the last entry.labelandcontext_editpoint attargetId;compaction.firstKeptEntryIdmarks the cut. - Fork/branch: child header
parentSession= parent file path.forkFromcopies every parent entry verbatim, ids and timestamps included (session-manager.ts:1844-1863); branching copies the path to the chosen entry (1632-1681). The same entry therefore appears in several files. - little-coder checkpoints: a directory named after the session file
(Checkpoints in
little-coder.md). - Sub-agents: pi has none built in; little-coder's leave no session file
(Transcript stores in
little-coder.md).
Other stores¶
The main format has no SQLite or binary store.
Experimental durable sessions¶
The experimental server and vacation mode (PI_EXPERIMENTAL=1, HEAD only) keep session.sqlite
in WAL mode
(packages/durable/src/storage/sqlite/node.ts:181-193)
with tables conversations, entries(conversation_id, head, commit_seq, record JSON), tasks,
submissions, documents, document_revisions
(migrations.ts:10-82).
Not parsed in this plan; open through sqlite_util.py with sidecars when it is.
At 5c1736f
the CLI no longer has the experimental server: packages/server, packages/client and
packages/coding-agent/src/experimental/server.ts are gone, leaving only
src/experimental/source-resolver.ts in the CLI, coding-agent no longer depends on
pi-durable (grepped its package.json and src for durable, and the tree for session-relays
and radius-relay), and PI_EXPERIMENTAL
(core/experimental.ts:1-3)
is read only by the footer and the startup screen
(footer.ts:213,
cli/startup-ui.ts:141).
experimental/sessions/, experimental/vacation-sessions/ and ~/.pi/server exist only where a
build from before the removal ran.
Debug log and crash log¶
pi-debug.log, written by /debug
(config.ts:615-617;
at 5c1736f
config.ts:653-656),
is rewritten by each /debug: a first line Debug output at <ISO time>, the terminal size, the
rendered screen lines, then === Agent messages (JSONL) === and the open session's messages, one
JSON object per line, with no session id
(modes/interactive/interactive-mode.ts:6959-6982),
so it can hold a deleted session's turns. v0.83.0 writes the same file the same way
(config.ts:563-565,
interactive-mode.ts:5886-5910).
Each line is the inner object of a session message entry (Message roles), with
its own timestamp in Unix milliseconds and no entry id or timestamp. An assistant message's
timestamp is set when its stream starts
(packages/ai/src/api/anthropic-messages.ts:585-600),
while the session file appends the entry, with the append time, at message_end
(core/agent-session.ts:1148-1166),
so the two times of one message differ by the length of the reply.
crashes.json is a JSON array of the last five crashes (a crash older than seven days is not
announced), each with its own timestamp, message, stack, sessionFile and cwd
(core/crash-log.ts:6-23;
at 5c1736f
core/crash-log.ts:7-19),
where a record also has version, kind (uncaught_exception or fatal_error) and notified,
and stack and sessionFile may be null
(crash-log.ts:124-144).
Project trust¶
~/.pi/agent/trust.json is an object whose keys are absolute project paths and whose values are
true or false; a null decision deletes the key
(trust-manager.ts:28,
214,
232-241);
v0.83.0, which little-coder pins, writes the same file
(trust-manager.ts:212).
Since 5c1736f the global setting defaultProjectTrust in settings.json (ask, the default,
always or never; global only) decides an untrusted project's fate when trust.json has no
entry
(settings-schema.ts:266-271,
settings-defaults.ts:30,
project-trust.ts:78).
pi has no per-call tool approval of its own: the only --approve is the per-run project trust
override
(cli/args.ts:241-244,
341-342),
which writes nothing.
Format versions¶
Header version: 1 (no field, no ids), 2 (id/parentId added), 3 (hookMessage role renamed
custom)
(session-manager.ts:41,
286-345).
pi rewrites old files in place on open, so a collected v1/v2 file means nobody resumed it. Within v3
fields were added without a bump: usage and context_edit entries, system role,
compaction.systemMessage, stopReason:"deferred" (absent at v0.83.0,
0.83.0 types.ts:391).
A parser must ignore unknown types and fields.
At 5c1736f (coding-agent 1.3.0) the session writer (core/session-manager.ts),
core/trust-manager.ts, core/bug-report.ts, core/crash-log.ts and the session picker are
byte for byte the same as at 2003871, so the record shapes the parser reads are unchanged and no
model field changed; assistant messages gained durationMs and tool calls argumentsError
(packages/ai/src/types.ts:448,
600),
again without a version bump. The meta.json reader serves only installs from before the
experimental server's removal (Experimental durable sessions).
Secrets¶
Tool arguments and outputs are stored verbatim (cat .env, write
contents, bash output). system messages carry the full system prompt,
including AGENTS.md and skill text. thinkingSignature,
thoughtSignature, textSignature are opaque provider blobs (redacted
thinking is encrypted there). Credentials are in auth.json and
mcp-auth.json, not in sessions.
Parser plan¶
The parser reads .pi/agent/sessions/*/*.jsonl and .pi/agent/*.jsonl, takes line 1 as the
header and skips files whose first record is not type:"session". Files in a custom session
directory elsewhere on disk are not read; they would be detected by that header shape.
| Column | Source |
|---|---|
| timestamp_utc | entry timestamp; fall back to message.timestamp (ms) |
| session_id | header id |
| project_path | header cwd |
| git_branch | empty (not recorded) |
| turn_type | user; assistant text parts; toolCall → tool_use; toolResult → tool_result; thinking → thinking; bashExecution → tool_use + tool_result; system role, custom role, custom_message, model_change, thinking_level_change, compaction, branch_summary, context_edit, session_info → system; skip custom entries, label, usage |
| model | assistant responseModel, else model; model_change.modelId |
| tool_name | toolCall.name, toolResult.toolName; bash for bashExecution |
| tool_use_id | toolCall.id / toolResult.toolCallId; entry id for bashExecution |
| text | text parts joined; tool_use: command, path, pattern or code, else JSON; tool_result: text parts; compaction/branch: summary |
Permission rows¶
No entry type or message role in Record schema records a permission decision. The
standing state is ~/.pi/agent/trust.json (Project trust). One system row per
key, permission setting: <path>=<value> (from .pi/agent/trust.json, file mtime), with
project_path the key, session_id empty, source_line 1 and timestamp_utc the file's
modification time. A file that is not a JSON object is one system row
parser: .pi/agent/trust.json is not a JSON object.
The other standing setting, defaultProjectTrust in ~/.pi/agent/settings.json
(Project trust), is one system row permission setting:
defaultProjectTrust=<value> (from .pi/agent/settings.json, file mtime) at the file's modification
time, with an empty session_id; no row when the key is absent, and no other key of the file is
read. pi records no per-call decision; an extension's block is an error toolResult with the
extension's reason as its text (Message roles). little-coder's permission gate is
the one such extension whose texts are known
(Permission gate in little-coder.md): an error toolResult
whose text is edit cancelled by user or command cancelled by user or starts with
shell whitelist: is followed by permission decision: denied (<the text>), with the call's
tool_name and tool_use_id and the result's time. Both rows are source-derived; not observed on
any real install.
Model rows¶
system row model: <id> provider=<provider> at the
first entry of a session that names a model, and model: <old> -> <new>
provider=<provider> on each later entry whose model differs, both with the
model column set to the new id:
model_change:modelIdandprovider.- assistant message:
responseModel, elsemodel, andprovider; whenresponseModelis set and differs frommodel, the row endsrequested=<model>.responseModelis the "concrete model reported by the provider when different from the requestedmodel" (types.ts:553-554).
The row precedes the entry's own rows and carries its timestamp. A
model_change entry is the user's own switch, so one that names the
model already in force still yields the plain model: <id>
provider=<provider> row; an assistant message that repeats the current
model yields none.
Past-use rows and secondary sources¶
Both files of Debug log and crash log are read on every run; source-derived; not observed on any real install.
pi-debug.log, secondary: onesystemrowdebug snapshot: /debug wrote <N> message(s) of the open session, which the log does not name (from .pi/agent/pi-debug.log, Debug output at line)at the time on its first line, then the lines after the marker through the session reader, each at the message's owntimestamp, with an emptysession_idandproject_path. A row the session file also holds is dropped; the window is one hour, because an assistant message's own time is its stream start and the entry's is its end. The screen lines before the marker are not read.crashes.json, primary, since it repeats no session event: onesystemrow per record,crash: <kind> <message> version=<version> sessionFile=<sessionFile> (from .pi/agent/crashes.json, record timestamp)followed by the stack on the next lines, at the record'stimestamp, withproject_paththecwdandsession_idthe id in thesessionFilename;-for an absent value.
pi keeps no prompt history file (What survives a delete).
When a file's parentSession parent is also in the collection, drop
child entries whose id and timestamp match a parent entry, so a fork
does not double the timeline.
Sync, relay and origin rows¶
pi keeps no session in a vendor account, but two commands send a session out, and only one leaves a mark.
/shareuploads the current branch on request (slash-commands.ts:27, interactive-mode.ts:3190-3194). With Radius credentials (Radius is Earendil Works' hosted model gateway for pi, signed in with/login radius, docs/providers.md:104-112) it posts the branch as JSONL, plus a trailingpi.shareentry with the system prompt and tool schemas, tohttps://radius.pi.dev/v1/artifactswithvisibility=organization(session-share.ts:24-47, 98-131, radius-config.ts:4); without them it renders HTML and runsgh gist create --public=false, a secret gist in the user's GitHub account viewed athttps://pi.dev/session/#<gist id>(session-share.ts:71-88, 159-203, config.ts:553-559). Both write only to a temporary directory that is removed afterwards, and the share URL is only shown on screen (session-share.ts:58-60, 89-95, 146); thepi.shareentry goes into the uploaded copy, never the session file (session-export.ts:9-29). v0.83.0, which little-coder pins, has the gist path only, the same way (0.83.0 interactive-mode.ts:5523-5608). So a shared session leaves no local marker; the gist is found from the GitHub account, the Radius artifact from the organization. Since5c1736fthe settingsessionShareDestinationinsettings.json(auto,gist,radius) chooses where/shareuploads (settings-schema.ts:224-230, session-share.ts:69-76), still with no local record of a share; it names the likely destination and could be asystemrow beside the trust rows, but a share leaves no mark of its own. The experimental Radius relay below is gone at that commit (Experimental durable sessions)./bug(HEAD only) asks whether to include the transcript, then uploadsreport.json,diagnostics.jsonand, when chosen,session.jsonland a model-writtensummary.mdtohttps://radius.pi.dev/v1/bug-reports, with no login needed, or writes them to a zip (modes/interactive/bug-report.ts:113-120, core/bug-report.ts:252-272, bug-report-upload.ts:12-37). Either way it appends thepi.bug-reportentry of Bug-report entry and empties.pi/agent/crashes.jsonwhen the report carried crashes (modes/interactive/bug-report.ts:226, crash-log.ts:21-23). The parser reads it, the onecustomentry not skipped, and fordeliveryuploadyields onesystemrowsession synced: bug report <id> uploaded to radius.pi.dev transcript=<sessionIncluded> summary=<summaryIncluded> (from session entry pi.bug-report, entry timestamp), at the entry'stimestamp, with the session's id andcwd; azipentry yieldsbug report exported: <id> path=<path> (from session entry pi.bug-report, entry timestamp)instead, since nothing left the host, and an absent value prints-.hintis not printed. Both rows are fixture only: no real install has shown api.bug-reportentry. The server answers with its ownbug_report.id, which pi shows and does not store (modes/interactive/bug-report.ts:186-189).
The experimental server (PI_EXPERIMENTAL=1, HEAD only,
experimental.ts:2)
also opens a live relay through Radius, /v1/session-relays/<server id>/connect, when Radius
credentials exist
(server.ts:609-615,
radius-relay.ts:619-623,
radius-auth.ts:31-55);
it carries traffic to remote clients rather than storing a copy, and its only file is the server id,
.pi/server/default-server-id
(server.ts:54-56,
76-112),
which says the host could be reached that way, not that a session was. No telemetry leaves pi: the
telemetry package has no exporter
(telemetry/README.md:11).
Scratch directory rows¶
None in the home. Spilled tool output goes to the system temporary directory and is named in a
toolResult's details.fullOutputPath (Scratch directories).
Deletion rows¶
pi writes no deletion marker; the picker's delete removes the session file with its
pi.bug-report entry (What survives a delete). Covered.
Bad-line rows¶
pi itself skips malformed lines (Record schema); the parser does not. Each line
that does not decode is its own system row at the end of the session's rows,
parser: unparseable line (<decoder reason>): <the line in full>, with the
session's id and cwd; a file with no header line gives those rows alone,
with an empty session id. Every line-by-line reader reports bad lines.
Fixtures¶
Fixture, one session file:
.pi/agent/sessions/--home-alice-proj--/2026-10-01T09-00-00-000Z_0199a1b2-7c3d-7e4f-8a5b-6c7d8e9f0a1b.jsonl
{"type":"session","version":3,"id":"0199a1b2-7c3d-7e4f-8a5b-6c7d8e9f0a1b","timestamp":"2026-10-01T09:00:00.000Z","cwd":"/home/alice/proj"}
{"type":"model_change","id":"1a2b3c4d","parentId":null,"timestamp":"2026-10-01T09:00:00.100Z","provider":"anthropic","modelId":"claude-sonnet-4-5"}
{"type":"message","id":"2b3c4d5e","parentId":"1a2b3c4d","timestamp":"2026-10-01T09:00:01.000Z","message":{"role":"user","content":"fix the failing test","timestamp":1759309201000}}
{"type":"message","id":"3c4d5e6f","parentId":"2b3c4d5e","timestamp":"2026-10-01T09:00:04.000Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"run the tests first"},{"type":"toolCall","id":"toolu_01","name":"bash","arguments":{"command":"npm test"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":900,"output":40,"cacheRead":0,"cacheWrite":0,"totalTokens":940,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1759309204000}}
{"type":"message","id":"4d5e6f70","parentId":"3c4d5e6f","timestamp":"2026-10-01T09:00:07.000Z","message":{"role":"toolResult","toolCallId":"toolu_01","toolName":"bash","content":[{"type":"text","text":"1 failing"}],"isError":false,"timestamp":1759309207000}}
{"type":"message","id":"5e6f7081","parentId":"4d5e6f70","timestamp":"2026-10-01T09:00:09.000Z","message":{"role":"bashExecution","command":"git status","output":"M src/a.ts","exitCode":0,"cancelled":false,"truncated":false,"timestamp":1759309209000}}
{"type":"session_info","id":"6f708192","parentId":"5e6f7081","timestamp":"2026-10-01T09:00:10.000Z","name":"test fix"}
this line is cut mid-wri
The last line exercises the bad-line system row (Bad lines).
pi-debug.log, the screen lines shortened:
Debug output at 2026-10-01T09:02:00.000Z
Terminal: 120x40
Total lines: 2
=== All rendered lines with visible widths ===
[0] (w=20) "fix the failing test"
=== Agent messages (JSONL) ===
{"role": "user", "content": "fix the failing test", "timestamp": 1790845201000}
{"role": "user", "content": "a prompt whose session file was deleted", "timestamp": 1790845300000}
crashes.json:
[{"timestamp": "2026-10-01T09:05:00.000Z", "version": "1.3.0", "kind": "uncaught_exception", "message": "Cannot read properties of undefined", "stack": "TypeError: ...", "sessionFile": "/home/alice/.pi/agent/sessions/--srv-proj--/2026-10-01T09-00-00-000Z_0199a1b2-7c3d-7e4f-8a5b-6c7d8e9f0a1b.jsonl", "cwd": "/srv/proj", "notified": true}]
Wrapper attribution¶
The header names no producer, so attribution rests on what the wrapper leaves:
- little-coder writes into pi's own directory. The rows stay agent
pi, as the manifest says, and the session gets onesystemrowsession driven by little-coder (pi launcher): <why>when acustom_messagehascustomTypestartinglc-(_shared/inject.ts:49-64; full list in little-coder.md), when an assistantproviderisllamacpp(llama-cpp-provider/index.ts:172; pi's own built-in isllama.cpp, llama/provider.ts:22), or when.little-coder/checkpoints/<this file name>/exists in the same home. - Letta Code writes the format under
.letta/lc-local-backend/conversations/*/messages.jsonl, so the path already givesletta; inside,message.idstartsletta-msg-andmessage.metadata.agent_idis set, fields pi never writes (local-store.ts:135-137, local-message.ts:26-46;manifest.jsonmessage_format:"pi-session-entry-jsonl", local-transcript.ts:24-46). Share the pi parser, keep the agent from the catalog. - Otherwise
pi. An SDK embedder or rebranded build is not distinguishable from the file alone.
Source host rows¶
The agent records no source-host integration. /share without Radius runs the user's own gh
CLI, after gh auth status, to create a secret gist
(session-share.ts:79-83,
182),
so the GitHub token is gh's and pi stores no account or token for it
(Sync, relay and origin rows). The GitHub Copilot model provider's
OAuth grant, scope read:user, is a model credential kept in auth.json as {type, refresh,
access, expires, enterpriseUrl} with no login
(packages/ai/src/auth/oauth/github-copilot.ts:215-218,
341-347), and the
git/<host>/<path> package checkouts are installs (Per-user storage). A
grep of packages/coding-agent/src and packages/ai/src at
5c1736f
for github, gitlab, bitbucket, oauth, repositor, pull request and clone finds no
repository list and no repository-bound conversation.
Confidence¶
- High (source): the agent directory, its override, every file name in
Per-user storage, the session directory encoding and file naming, credential
file shapes and project
.picontents. - High (source): the header, entry types, roles, field names and timestamp units of Record schema.
- High: that the layout is the same on Windows (plain
homedir()join). - Medium:
system-role persistence on versions between 0.83.0 and HEAD (taken from the ai changelog's 0.86.0 note, not bisected). - Medium: whether
.pi/mcp.jsonholds literal tokens in practice (the documented form is${ENV}interpolation). - Not determined: the durable SQLite
recordJSON shape (Experimental durable sessions). - Not determined: whether a rebranded pi build (
piConfig.configDir) is in use anywhere, which would need its own catalog line.