pi-export-my-chat
pi extension that exports a running chat to one complete, lossless, revivable JSON — the full session tree, every provider request observed (captured to a compact content-addressed journal, deduplicated by content hash), usage/cost/context stats, and timi
Package details
Install pi-export-my-chat from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-export-my-chat- Package
pi-export-my-chat- Version
1.1.0- Published
- Sep 13, 2026
- Downloads
- 337/mo · 337/wk
- Author
- prawnbear
- License
- MIT
- Types
- extension
- Size
- 108.1 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
export-my-chat
A pi extension that turns a running chat into one complete, lossless, revivable JSON file — and turns that file back into a living pi session on any machine.
Two commands, one contract:
/export-my-chat— writes a single self-contained.json: the durable session tree (header, every entry, active branch, active context), every provider request observed during the session (deduplicated by content hash, never dropped), usage/cost/context stats, and operation timings. Refuses to run while the agent is mid-turn, never overwrites, writes0600./export-my-chat:revive <path> [--force]— validates that export, rebuilds a real pi session file from it, saves it into pi's session directory for the project you're standing in, and switches the current pi window onto it. The chat comes back — named, branchable, listed in/resume— on an entirely different machine.
/export-my-chat # -> ./pi-my-chat-export-<UTC>.json
/export-my-chat /abs/path/chat.json # -> exact file; must NOT exist
/export-my-chat:revive ~/backups/chat.json # -> rebuilds + switches this window
/export-my-chat:revive chat.json --force # relative paths ok for reads; --force skips the prompt
Why this exists
pi already persists sessions as JSONL under ~/.pi/agent/sessions/ — so why an
export tool? Because the session file is pi's internal logical record. It
answers "what did pi store." It cannot answer:
- "What did the model actually receive?" — the provider-facing request bodies (system prompt rendering, tool schemas, cache markers, the exact context slice the provider saw at every turn) exist only at request time.
- "What did the model see over time?" — compaction and branching change the context slice on every request; only a per-request record shows that evolution.
- "Can I pick this chat up, move it, and keep working?" — a session file is
machine-local by convention (keyed by its original
cwd, unlisted in other projects), and carries no usage stats, timings, or request history.
This extension makes the portable artifact the unit of work: one JSON that is simultaneously a forensic record of the session and a seed from which the session can be reborn.
Design philosophy — every choice, first principles
- Lossless in content AND count. The default is all requests, not the
latest one. Tool-heavy sessions re-send nearly identical context every turn,
so literal duplication is factored out by content hash (byte-identical
payloads are stored once, referenced by
sha256) — but no request is ever dropped, and ordering is preserved. "Lossless" is a property of the set of observed requests, not of its most recent member. - Losslessness must be provable, not claimed. The revivable core (header
- ordered entries) is serialized by one canonical function —
buildSessionJsonl— used by both export (to compute the checksum) and revive (to rebuild the file). Revive recomputes and refuses on mismatch. Corrupt or edited exports fail loudly, before any state is touched.
- ordered entries) is serialized by one canonical function —
- The at-rest contract. Both commands refuse while the agent is mid-turn.
A snapshot taken mid-flight describes a moving target; a refused command is
cheaper than a subtly-wrong export. (No silent
waitForIdle— you asked for idle, you get a refusal until idle.) - Strict writes, forgiving reads. Export destinations must be absolute
.jsonpaths that don't exist yet, with an existing parent directory — exclusive0600creation, no overwrite, no symlink following, partial files cleaned up on failure. Revive inputs are read-only, so relative paths and quoting are accepted there. Asymmetry is deliberate: writing is dangerous, reading is not. - The session file IS the wire format. Revival doesn't need a bespoke
importer: pi sessions are header line + ordered entry lines of JSONL, and
ctx.switchSession(path)loads any session file. Revive reconstructs exactly that shape, then hands it to pi's own machinery. No parallel schema to rot. - Portability with identity, not anonymity. A revived session keeps its
original UUID on a new machine (so it's the same chat, not a clone) and
mints a fresh one only where the original still exists (same machine —
avoiding ambiguous
pi --session <id>matches). The headercwdis re-rooted to the reviving project: the session becomes native to where you are, which is what puts it in the right/resumebucket. The original cwd is never lost — it's in the export and in the revive notice. - Versioned on three axes. Every export records the export format
schemaVersion, pi'sCURRENT_SESSION_VERSION, and the extension version. Revive refuses (rather than half-loads) anything newer than what the local machine understands. Older session formats migrate up automatically — pi migrates v1/v2→v3 on load, so older is always fine. - The name is data, not metadata. pi stores the session name as a
session_infoentry inside the entry tree, so a faithful rebuild carries the name for free — revived sessions show up in/resumewith their names. The export also surfacessession.namefor humans; if the entries somehow lost the name, revive re-attaches it as a trailingsession_infoentry — the same mechanism/nameuses. No name → nothing appended → pi's default naming applies. - Capture to disk, not RAM — and store content, not copies. Every request is
journaled to a per-session scratch file in the agent cache dir the moment it's
observed, but split into content units — the request params, each individual
message, the tools array — each stored once per session, keyed by sha256 of
its canonical JSON and referenced by every request that contains it (the
codec lives in
helpers.mjs, unit-tested like everything else). Consecutive requests share nearly all of their context, so this keeps "all requests" on disk and bounded in RAM while turning journal growth from quadratic in stored bytes to linear — typically 10–17× smaller journals. The journal is reset onsession_start(new session, resume, or/reload), is written0600like the exports themselves, and journals untouched for 30+ days are pruned automatically. Every unit is verified against its hash when the journal is read; damaged lines and the requests that depended on them are reported in the export, never silently dropped. - Destructive acts ask. Reviving replaces the current window's context. A
non-empty current session gets a confirmation prompt. Headless callers have
no dialog UI, so pi's confirm would silently answer "no" — revive therefore
requires
--forcewhen there is no UI. The old session always stays on disk. - Honest capture metadata.
requests.noterecords the exactness boundaries ofbefore_provider_request: auth headers are never in a payload; a response is not part of the request that produced it; later-loaded extensions may mutate the payload after this hook sees it; capture resets when the runtime resets. The export says what it knows and what it can't.
What the JSON contains that a pi session file doesn't
| In the export, not in the session file | Why |
|---|---|
| Every provider request body | The session stores logical messages; the export stores the provider-shaped JSON the model received — system prompt rendering, tool schemas, thinking config |
| The full request sequence | Shows how compaction/branching changed the context slice on every turn |
| Per-request live context snapshots | used / window / remaining / percentUsed at the moment of each request |
| Per-request model metadata | provider / model / api as of each call, plus a request counter |
| Whole-session + active-branch usage & cost | Cache-token breakdowns, nested tool usage, compaction and branch-summary costs — aggregated, NaN-guarded, timing records excluded to avoid double-counting |
| Operation timing summaries | Per-kind min/max/avg/status, with overlap notes |
| The revivable core + checksum | The session tree re-serialized canonically, with a sha256 that proves the rebuild is byte-faithful |
The genuinely unique content is the request payloads and per-request context snapshots; the stats and timings are derived from records that do live in the session file, re-organized for analysis.
Quick start
- The extension is auto-discovered at
~/.pi/agent/extensions/export-my-chat/(or installed as an npm package — see below). Restart pi or run/reload. - Chat. Say things, run tools, branch, compact — live your best life.
- When the agent is idle, run
/export-my-chat. Note the reported entry and request counts. - Carry
pi-my-chat-export-….jsonwherever it needs to go. - On any machine with this extension: open pi in the project directory,
run
/export-my-chat:revive <path/to/export.json>, confirm, and continue the chat. The revived session is saved in this machine's~/.pi/agent/sessions/and listed in/resume.
How the pieces work
Request capture
before_provider_request fires before every HTTP call with the complete
provider-specific payload. The handler serializes it, takes the sha256 of the
full wire bytes (the request's identity in the export), and splits the
wire-parsed payload into content units — the request params (payload minus
messages/tools), each individual message, and the tools array. It appends
to ~/.pi/agent/cache/export-my-chat/<sessionId>.jsonl (mode 0600) one
line per new unit plus one reference line for the request itself: metadata
(n, timestamp, provider/model/api), a live getContextUsage() snapshot
(unknown token counts are recorded as null, never as 0 — pi cannot
estimate right after compaction), the wire sha256, and the unit references.
Because a session's requests differ mostly by their newest turn, each unit is
typically written once per session — the journal grows with the conversation,
not with the square of it. Each append starts on a fresh line, so a torn
mid-write append costs at most one request, never the ones after it.
session_start truncates the journal (the capture window is per-runtime, so
resuming or /reload resets it) and prunes journals untouched for 30+ days.
At export time the journal is read and verified — every unit's hash is
recomputed — and payloads are rebuilt value-exactly; byte-identical wire
payloads collapse into the export's payloads map keyed by the request-time
sha256. Anything unreadable is counted in the export
(corruptJournalLines, unknownJournalLines, droppedJournalRequests), so
the export always says exactly what survived.
The revivable core
session.header, session.entries (insertion order — the last entry is the
leaf, i.e. the branch position), and session.leafId. The checksum is sha256
over the canonical JSONL built from exactly these. session.revive also
records matchesOriginalFile: whether the canonical rebuild is byte-identical
to pi's own on-disk session file. It's a diagnostic, not a gate — the checksum
covers what revive writes, which is what matters for fidelity.
Revive, step by step
- Guards — refuses mid-turn (checked first, before any reading); confirms
before replacing a non-empty current session (
--forceskips; headless requires it). - Parse + validate — path (relative ok, quotes ok), format, schema
version, session header version vs this pi's
CURRENT_SESSION_VERSION, structural integrity (unique ids, resolvable parents, leaf = last entry, declared entry count), and the sha256 checksum. Any failure refuses before touching session state. - Rebuild — header re-rooted to
ctx.cwd, original UUID kept unless it collides locally, name re-attached if the entries lost it, written exclusively0600intoctx.sessionManager.getSessionDir()using pi's own filename convention (<timestamp>_<uuid>.jsonl, same timestamp format pi itself mints). - Switch —
ctx.switchSession(path)makes the revived session the current chat; further messages append to it like any native session. If another extension cancels the switch, the rebuilt file is still safe on disk and the notice points at it. The success notice reports entry count, name changes, and the original cwd.
Stats
Usage and cost are collected from persisted records — assistant messages,
nested model usage on tool results, compaction, and branch summaries — with
NaN/Infinity clamps, provider-reported totals preferred over component sums,
and timing records excluded (an agent-total timing overlaps its child
records; counting both would double-count). Stats are reported for both the
active branch and the whole session, plus a live context snapshot. Timing
records from the timings extension (if installed) are exported raw and
summarized per kind, with an explicit note that kinds overlap in wall time.
Security
Treat every export as maximally sensitive. It contains your system prompt,
context files, full conversations, tool output, source code, base64 images,
absolute paths, and anything secret that ever entered the chat or a request.
The export and every revived session file are written 0600; the scratch
journals that hold the same request payloads are written 0600 under a 0700
cache directory and are pruned automatically after 30 days of inactivity.
Export refuses existing destinations (including symlinks) and cleans up partial
files. Revive refuses exports that fail validation. Never commit an export;
leftover scratch journals can be deleted by hand as well.
Caveats & limits
- Requests are only what this runtime observed. Requests made before the
extension loaded (or before a
/reload) are not in the journal. The export reportscaptureStartedAtandobservedRequestCountso the window is always visible. - Auth headers are never captured (they aren't part of the payload), and responses are not requests — final assistant replies live in the session tree, not in the payload that produced them.
- Later-loaded extensions may mutate payloads after this hook observes them. The record is exact for what this hook saw.
- The journal stores content units, not wire copies. The export's
payloadsare JSON objects value-exactly equal to the wire request the provider received, andpayloadSha256identifies the original wire bytes captured at request time. (The export format has never embedded raw wire strings — payloads were always stored as parsed JSON — but pre-1.1 journals kept a full copy of every request on disk, growing quadratically with turn count; the content-addressed journal grows linearly instead.) - Version skew: an export made by a newer pi (higher session header version) or newer extension schema is refused on older machines with a clear message. Older exports revive on newer pi via pi's own session migrations.
- The working tree doesn't travel. A revived session remembers files from the original machine; the new machine's tree is whatever it is. Revive's notice names the original cwd for exactly this reason.
- The
withSessionrebind: afterctx.switchSession, the old extension instance's contexts are stale; all post-switch reporting goes through the freshctxpi hands back.
Validation
From the package root:
node --test tests/helpers.test.mjs # 20 unit tests: paths, usage, timings,
# exclusive write, JSONL, checksum,
# document validation, revive plan,
# content-addressed request journal
pi --list-models >/dev/null # extension loads without errors
Interactive checklist: run a session with tools and branches; /export-my-chat;
verify mode 0600 (ls -l), confirm the entry/request counts in the notice,
confirm a second export to the same path fails; edit the JSON's entries
slightly and confirm revive refuses on checksum; revive on another machine
(or /new first at home) and confirm the name, tree (/tree), and
/resume listing.
Package
Published as pi-export-my-chat (see package.json). Install with pi's
package mechanism:
pi --install pi-export-my-chat
The pi.extensions manifest points at ./index.ts; helpers.mjs and
tests/ ship alongside. No runtime dependencies — only node builtins and a
peer dependency on pi itself. docs/export-format.md holds the full field
reference for the export JSON.
Repository layout
export-my-chat/
├── index.ts — the extension: capture, /export-my-chat, /export-my-chat:revive
├── helpers.mjs — pure logic: usage, timings, paths, canonical JSONL,
│ checksum, document validation, revive planning,
│ and the content-addressed request-journal codec
├── tests/helpers.test.mjs — node:test unit tests for all of the above
├── docs/export-format.md — complete schema reference for the export JSON
├── README.md — this file
└── package.json — npm-publishable manifest + pi entry point
MIT license. Sessions are yours.