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

Packages

Package details

extension

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:

  1. /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, writes 0600.
  2. /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.
  • 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 .json paths that don't exist yet, with an existing parent directory — exclusive 0600 creation, 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 header cwd is re-rooted to the reviving project: the session becomes native to where you are, which is what puts it in the right /resume bucket. 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's CURRENT_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_info entry inside the entry tree, so a faithful rebuild carries the name for free — revived sessions show up in /resume with their names. The export also surfaces session.name for humans; if the entries somehow lost the name, revive re-attaches it as a trailing session_info entry — the same mechanism /name uses. 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 on session_start (new session, resume, or /reload), is written 0600 like 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 --force when there is no UI. The old session always stays on disk.
  • Honest capture metadata. requests.note records the exactness boundaries of before_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

  1. 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.
  2. Chat. Say things, run tools, branch, compact — live your best life.
  3. When the agent is idle, run /export-my-chat. Note the reported entry and request counts.
  4. Carry pi-my-chat-export-….json wherever it needs to go.
  5. 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

  1. Guards — refuses mid-turn (checked first, before any reading); confirms before replacing a non-empty current session (--force skips; headless requires it).
  2. 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.
  3. Rebuild — header re-rooted to ctx.cwd, original UUID kept unless it collides locally, name re-attached if the entries lost it, written exclusively 0600 into ctx.sessionManager.getSessionDir() using pi's own filename convention (<timestamp>_<uuid>.jsonl, same timestamp format pi itself mints).
  4. Switchctx.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 reports captureStartedAt and observedRequestCount so 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 payloads are JSON objects value-exactly equal to the wire request the provider received, and payloadSha256 identifies 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 withSession rebind: after ctx.switchSession, the old extension instance's contexts are stale; all post-switch reporting goes through the fresh ctx pi 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.