Skip to content

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-report entry, so the rows built from it are fixture only (Sync, relay and origin).

Source reviews

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:

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

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

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_DIR or the sessionDir setting 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) and pi-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).

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: content string or [{type:"text",text}|{type:"image",data,mimeType}] (types.ts:542-546, 397-417). Images are inline base64.
  • assistant: content[] of text (text, textSignature), thinking (thinking, thinkingSignature, redacted), toolCall (id, name, arguments object, 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. edit diff/patch, bash fullOutputPath), 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 error toolResult whose 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 (v2 hookMessage renamed), 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: header cwd.
  • 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. label and context_edit point at targetId; compaction.firstKeptEntryId marks the cut.
  • Fork/branch: child header parentSession = parent file path. forkFrom copies 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: modelId and provider.
  • assistant message: responseModel, else model, and provider; when responseModel is set and differs from model, the row ends  requested=<model>. responseModel is the "concrete model reported by the provider when different from the requested model" (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: one system row debug 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 own timestamp, with an empty session_id and project_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: one system row 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's timestamp, with project_path the cwd and session_id the id in the sessionFile name; - 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.

  • /share uploads 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 trailing pi.share entry with the system prompt and tool schemas, to https://radius.pi.dev/v1/artifacts with visibility=organization (session-share.ts:24-47, 98-131, radius-config.ts:4); without them it renders HTML and runs gh gist create --public=false, a secret gist in the user's GitHub account viewed at https://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); the pi.share entry 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. Since 5c1736f the setting sessionShareDestination in settings.json (auto, gist, radius) chooses where /share uploads (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 a system row 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 uploads report.json, diagnostics.json and, when chosen, session.jsonl and a model-written summary.md to https://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 the pi.bug-report entry of Bug-report entry and empties .pi/agent/crashes.json when the report carried crashes (modes/interactive/bug-report.ts:226, crash-log.ts:21-23). The parser reads it, the one custom entry not skipped, and for delivery upload yields one system row session synced: bug report <id> uploaded to radius.pi.dev transcript=<sessionIncluded> summary=<summaryIncluded> (from session entry pi.bug-report, entry timestamp), at the entry's timestamp, with the session's id and cwd; a zip entry yields bug report exported: <id> path=<path> (from session entry pi.bug-report, entry timestamp) instead, since nothing left the host, and an absent value prints -. hint is not printed. Both rows are fixture only: no real install has shown a pi.bug-report entry. The server answers with its own bug_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 one system row session driven by little-coder (pi launcher): <why> when a custom_message has customType starting lc- (_shared/inject.ts:49-64; full list in little-coder.md), when an assistant provider is llamacpp (llama-cpp-provider/index.ts:172; pi's own built-in is llama.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 gives letta; inside, message.id starts letta-msg- and message.metadata.agent_id is set, fields pi never writes (local-store.ts:135-137, local-message.ts:26-46; manifest.json message_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 .pi contents.
  • 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.json holds literal tokens in practice (the documented form is ${ENV} interpolation).
  • Not determined: the durable SQLite record JSON shape (Experimental durable sessions).
  • Not determined: whether a rebranded pi build (piConfig.configDir) is in use anywhere, which would need its own catalog line.