pi-export-my-chat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/docs/export-format.md +140 -0
- package/helpers.mjs +509 -0
- package/index.ts +510 -0
- package/package.json +30 -0
- package/tests/helpers.test.mjs +345 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The export-my-chat authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# export-my-chat
|
|
2
|
+
|
|
3
|
+
A [pi](https://github.com/earendil-works/pi-mono) extension that turns a running
|
|
4
|
+
chat into **one complete, lossless, revivable JSON file** — and turns that file
|
|
5
|
+
back into a living pi session on any machine.
|
|
6
|
+
|
|
7
|
+
Two commands, one contract:
|
|
8
|
+
|
|
9
|
+
1. **`/export-my-chat`** — writes a single self-contained `.json`: the durable
|
|
10
|
+
session tree (header, every entry, active branch, active context), **every
|
|
11
|
+
provider request observed during the session** (deduplicated by content
|
|
12
|
+
hash, never dropped), usage/cost/context stats, and operation timings.
|
|
13
|
+
Refuses to run while the agent is mid-turn, never overwrites, writes
|
|
14
|
+
`0600`.
|
|
15
|
+
2. **`/export-my-chat:revive <path> [--force]`** — validates that export, rebuilds
|
|
16
|
+
a real pi session file from it, saves it into pi's session directory for the
|
|
17
|
+
project you're standing in, and switches the current pi window onto it. The
|
|
18
|
+
chat comes back — named, branchable, listed in `/resume` — on an entirely
|
|
19
|
+
different machine.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
/export-my-chat # -> ./pi-my-chat-export-<UTC>.json
|
|
23
|
+
/export-my-chat /abs/path/chat.json # -> exact file; must NOT exist
|
|
24
|
+
/export-my-chat:revive ~/backups/chat.json # -> rebuilds + switches this window
|
|
25
|
+
/export-my-chat:revive chat.json --force # relative paths ok for reads; --force skips the prompt
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Why this exists
|
|
31
|
+
|
|
32
|
+
pi already persists sessions as JSONL under `~/.pi/agent/sessions/` — so why an
|
|
33
|
+
export tool? Because the session file is pi's *internal logical record*. It
|
|
34
|
+
answers "what did pi store." It cannot answer:
|
|
35
|
+
|
|
36
|
+
- **"What did the model actually receive?"** — the provider-facing request
|
|
37
|
+
bodies (system prompt rendering, tool schemas, cache markers, the exact
|
|
38
|
+
context slice the provider saw at every turn) exist only at request time.
|
|
39
|
+
- **"What did the model see *over time*?"** — compaction and branching change
|
|
40
|
+
the context slice on every request; only a per-request record shows that
|
|
41
|
+
evolution.
|
|
42
|
+
- **"Can I pick this chat up, move it, and keep working?"** — a session file is
|
|
43
|
+
machine-local by convention (keyed by its original `cwd`, unlisted in other
|
|
44
|
+
projects), and carries no usage stats, timings, or request history.
|
|
45
|
+
|
|
46
|
+
This extension makes the portable artifact the *unit of work*: one JSON that is
|
|
47
|
+
simultaneously a forensic record of the session and a seed from which the
|
|
48
|
+
session can be reborn.
|
|
49
|
+
|
|
50
|
+
## Design philosophy — every choice, first principles
|
|
51
|
+
|
|
52
|
+
- **Lossless in content AND count.** The default is *all* requests, not the
|
|
53
|
+
latest one. Tool-heavy sessions re-send nearly identical context every turn,
|
|
54
|
+
so literal duplication is factored out by content hash (byte-identical
|
|
55
|
+
payloads are stored once, referenced by `sha256`) — but no request is ever
|
|
56
|
+
dropped, and ordering is preserved. "Lossless" is a property of the set of
|
|
57
|
+
observed requests, not of its most recent member.
|
|
58
|
+
- **Losslessness must be *provable*, not claimed.** The revivable core (header
|
|
59
|
+
+ ordered entries) is serialized by one canonical function — `buildSessionJsonl`
|
|
60
|
+
— used by *both* export (to compute the checksum) and revive (to rebuild the
|
|
61
|
+
file). Revive recomputes and refuses on mismatch. Corrupt or edited exports
|
|
62
|
+
fail loudly, before any state is touched.
|
|
63
|
+
- **The at-rest contract.** Both commands refuse while the agent is mid-turn.
|
|
64
|
+
A snapshot taken mid-flight describes a moving target; a refused command is
|
|
65
|
+
cheaper than a subtly-wrong export. (No silent `waitForIdle` — you asked for
|
|
66
|
+
idle, you get a refusal until idle.)
|
|
67
|
+
- **Strict writes, forgiving reads.** Export destinations must be absolute
|
|
68
|
+
`.json` paths that don't exist yet, with an existing parent directory —
|
|
69
|
+
exclusive `0600` creation, no overwrite, no symlink following, partial files
|
|
70
|
+
cleaned up on failure. Revive *inputs* are read-only, so relative paths and
|
|
71
|
+
quoting are accepted there. Asymmetry is deliberate: writing is dangerous,
|
|
72
|
+
reading is not.
|
|
73
|
+
- **The session file IS the wire format.** Revival doesn't need a bespoke
|
|
74
|
+
importer: pi sessions are header line + ordered entry lines of JSONL, and
|
|
75
|
+
`ctx.switchSession(path)` loads any session file. Revive reconstructs exactly
|
|
76
|
+
that shape, then hands it to pi's own machinery. No parallel schema to rot.
|
|
77
|
+
- **Portability with identity, not anonymity.** A revived session keeps its
|
|
78
|
+
original UUID on a new machine (so it's *the same chat*, not a clone) and
|
|
79
|
+
mints a fresh one only where the original still exists (same machine —
|
|
80
|
+
avoiding ambiguous `pi --session <id>` matches). The header `cwd` is
|
|
81
|
+
re-rooted to the reviving project: the session becomes native to where you
|
|
82
|
+
are, which is what puts it in the right `/resume` bucket. The original cwd is
|
|
83
|
+
never lost — it's in the export and in the revive notice.
|
|
84
|
+
- **Versioned on three axes.** Every export records the export format
|
|
85
|
+
`schemaVersion`, pi's `CURRENT_SESSION_VERSION`, and the extension version.
|
|
86
|
+
Revive refuses (rather than half-loads) anything newer than what the local
|
|
87
|
+
machine understands. Older session formats migrate up automatically — pi
|
|
88
|
+
migrates v1/v2→v3 on load, so *older* is always fine.
|
|
89
|
+
- **The name is data, not metadata.** pi stores the session name as a
|
|
90
|
+
`session_info` entry *inside the entry tree*, so a faithful rebuild carries
|
|
91
|
+
the name for free — revived sessions show up in `/resume` with their names.
|
|
92
|
+
The export also surfaces `session.name` for humans; if the entries somehow
|
|
93
|
+
lost the name, revive re-attaches it as a trailing `session_info` entry —
|
|
94
|
+
the same mechanism `/name` uses. No name → nothing appended → pi's default
|
|
95
|
+
naming applies.
|
|
96
|
+
- **Capture to disk, not RAM.** Every request is appended to a per-session
|
|
97
|
+
scratch journal in the agent cache dir the moment it's observed. "All
|
|
98
|
+
requests" can mean tens of MB for a long session; the journal keeps that on
|
|
99
|
+
disk and only merges + deduplicates at export time. The journal resets on
|
|
100
|
+
`session_start` (new session, resume, or `/reload`), is written `0600` like
|
|
101
|
+
the exports themselves, and journals untouched for 30+ days are pruned
|
|
102
|
+
automatically on `session_start`.
|
|
103
|
+
- **Destructive acts ask.** Reviving replaces the current window's context. A
|
|
104
|
+
non-empty current session gets a confirmation prompt. Headless callers have
|
|
105
|
+
no dialog UI, so pi's confirm would silently answer "no" — revive therefore
|
|
106
|
+
requires `--force` when there is no UI. The old session always stays on disk.
|
|
107
|
+
- **Honest capture metadata.** `requests.note` records the exactness
|
|
108
|
+
boundaries of `before_provider_request`: auth headers are never in a
|
|
109
|
+
payload; a response is not part of the request that produced it; later-loaded
|
|
110
|
+
extensions may mutate the payload after this hook sees it; capture resets
|
|
111
|
+
when the runtime resets. The export says what it knows *and* what it can't.
|
|
112
|
+
|
|
113
|
+
## What the JSON contains that a pi session file doesn't
|
|
114
|
+
|
|
115
|
+
| In the export, not in the session file | Why |
|
|
116
|
+
|---|---|
|
|
117
|
+
| 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 |
|
|
118
|
+
| The full request sequence | Shows how compaction/branching changed the context slice on every turn |
|
|
119
|
+
| Per-request live context snapshots | `used / window / remaining / percentUsed` at the moment of each request |
|
|
120
|
+
| Per-request model metadata | provider / model / api as of each call, plus a request counter |
|
|
121
|
+
| 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 |
|
|
122
|
+
| Operation timing summaries | Per-kind min/max/avg/status, with overlap notes |
|
|
123
|
+
| The revivable core + checksum | The session tree re-serialized canonically, with a sha256 that proves the rebuild is byte-faithful |
|
|
124
|
+
|
|
125
|
+
The genuinely unique content is the request payloads and per-request context
|
|
126
|
+
snapshots; the stats and timings are *derived* from records that do live in the
|
|
127
|
+
session file, re-organized for analysis.
|
|
128
|
+
|
|
129
|
+
## Quick start
|
|
130
|
+
|
|
131
|
+
1. The extension is auto-discovered at `~/.pi/agent/extensions/export-my-chat/`
|
|
132
|
+
(or installed as an npm package — see below). Restart pi or run `/reload`.
|
|
133
|
+
2. Chat. Say things, run tools, branch, compact — live your best life.
|
|
134
|
+
3. When the agent is idle, run `/export-my-chat`. Note the reported entry and
|
|
135
|
+
request counts.
|
|
136
|
+
4. Carry `pi-my-chat-export-….json` wherever it needs to go.
|
|
137
|
+
5. On any machine with this extension: open pi in the project directory,
|
|
138
|
+
run `/export-my-chat:revive <path/to/export.json>`, confirm, and continue
|
|
139
|
+
the chat. The revived session is saved in this machine's
|
|
140
|
+
`~/.pi/agent/sessions/` and listed in `/resume`.
|
|
141
|
+
|
|
142
|
+
## How the pieces work
|
|
143
|
+
|
|
144
|
+
### Request capture
|
|
145
|
+
|
|
146
|
+
`before_provider_request` fires before every HTTP call with the complete
|
|
147
|
+
provider-specific payload. The handler serializes it immediately, records a
|
|
148
|
+
sha256, a live `getContextUsage()` snapshot (unknown token counts are recorded
|
|
149
|
+
as `null`, never as `0` — pi cannot estimate right after compaction), and the
|
|
150
|
+
model metadata, and appends the record to
|
|
151
|
+
`~/.pi/agent/cache/export-my-chat/<sessionId>.jsonl` with mode `0600`.
|
|
152
|
+
`session_start` truncates the journal — the capture window is per-runtime, so
|
|
153
|
+
resuming or `/reload` resets it — and prunes journals untouched for 30+ days.
|
|
154
|
+
At export time the journal is merged: byte-identical payloads collapse into a
|
|
155
|
+
`payloads` map keyed by hash, and the ordered `records` array keeps every
|
|
156
|
+
request with its `payloadSha256` reference.
|
|
157
|
+
|
|
158
|
+
### The revivable core
|
|
159
|
+
|
|
160
|
+
`session.header`, `session.entries` (insertion order — the last entry *is* the
|
|
161
|
+
leaf, i.e. the branch position), and `session.leafId`. The checksum is sha256
|
|
162
|
+
over the canonical JSONL built from exactly these. `session.revive` also
|
|
163
|
+
records `matchesOriginalFile`: whether the canonical rebuild is byte-identical
|
|
164
|
+
to pi's own on-disk session file. It's a diagnostic, not a gate — the checksum
|
|
165
|
+
covers what *revive writes*, which is what matters for fidelity.
|
|
166
|
+
|
|
167
|
+
### Revive, step by step
|
|
168
|
+
|
|
169
|
+
1. **Guards** — refuses mid-turn (checked first, before any reading); confirms
|
|
170
|
+
before replacing a non-empty current session (`--force` skips; headless
|
|
171
|
+
requires it).
|
|
172
|
+
2. **Parse + validate** — path (relative ok, quotes ok), format, schema
|
|
173
|
+
version, session header version vs this pi's `CURRENT_SESSION_VERSION`,
|
|
174
|
+
structural integrity (unique ids, resolvable parents, leaf = last entry,
|
|
175
|
+
declared entry count), and the sha256 checksum. Any failure refuses
|
|
176
|
+
before touching session state.
|
|
177
|
+
3. **Rebuild** — header re-rooted to `ctx.cwd`, original UUID kept unless it
|
|
178
|
+
collides locally, name re-attached if the entries lost it, written
|
|
179
|
+
exclusively `0600` into `ctx.sessionManager.getSessionDir()` using pi's own
|
|
180
|
+
filename convention (`<timestamp>_<uuid>.jsonl`, same timestamp format pi
|
|
181
|
+
itself mints).
|
|
182
|
+
4. **Switch** — `ctx.switchSession(path)` makes the revived session the current
|
|
183
|
+
chat; further messages append to it like any native session. If another
|
|
184
|
+
extension cancels the switch, the rebuilt file is still safe on disk and the
|
|
185
|
+
notice points at it. The success notice reports entry count, name changes,
|
|
186
|
+
and the original cwd.
|
|
187
|
+
|
|
188
|
+
### Stats
|
|
189
|
+
|
|
190
|
+
Usage and cost are collected from persisted records — assistant messages,
|
|
191
|
+
nested model usage on tool results, compaction, and branch summaries — with
|
|
192
|
+
NaN/Infinity clamps, provider-reported totals preferred over component sums,
|
|
193
|
+
and timing records excluded (an agent-total timing overlaps its child
|
|
194
|
+
records; counting both would double-count). Stats are reported for both the
|
|
195
|
+
active branch and the whole session, plus a live context snapshot. Timing
|
|
196
|
+
records from the `timings` extension (if installed) are exported raw and
|
|
197
|
+
summarized per kind, with an explicit note that kinds overlap in wall time.
|
|
198
|
+
|
|
199
|
+
## Security
|
|
200
|
+
|
|
201
|
+
Treat every export as maximally sensitive. It contains your system prompt,
|
|
202
|
+
context files, full conversations, tool output, source code, base64 images,
|
|
203
|
+
absolute paths, and anything secret that ever entered the chat or a request.
|
|
204
|
+
The export and every revived session file are written `0600`; the scratch
|
|
205
|
+
journals that hold the same request payloads are written `0600` under a `0700`
|
|
206
|
+
cache directory and are pruned automatically after 30 days of inactivity.
|
|
207
|
+
Export refuses existing destinations (including symlinks) and cleans up partial
|
|
208
|
+
files. Revive refuses exports that fail validation. Never commit an export;
|
|
209
|
+
leftover scratch journals can be deleted by hand as well.
|
|
210
|
+
|
|
211
|
+
## Caveats & limits
|
|
212
|
+
|
|
213
|
+
- **Requests are only what this runtime observed.** Requests made before the
|
|
214
|
+
extension loaded (or before a `/reload`) are not in the journal. The export
|
|
215
|
+
reports `captureStartedAt` and `observedRequestCount` so the window is
|
|
216
|
+
always visible.
|
|
217
|
+
- **Auth headers are never captured** (they aren't part of the payload), and
|
|
218
|
+
**responses are not requests** — final assistant replies live in the session
|
|
219
|
+
tree, not in the payload that produced them.
|
|
220
|
+
- **Later-loaded extensions may mutate payloads** after this hook observes
|
|
221
|
+
them. The record is exact for what *this* hook saw.
|
|
222
|
+
- **Version skew**: an export made by a newer pi (higher session header
|
|
223
|
+
version) or newer extension schema is refused on older machines with a clear
|
|
224
|
+
message. Older exports revive on newer pi via pi's own session migrations.
|
|
225
|
+
- **The working tree doesn't travel.** A revived session *remembers* files
|
|
226
|
+
from the original machine; the new machine's tree is whatever it is. Revive's
|
|
227
|
+
notice names the original cwd for exactly this reason.
|
|
228
|
+
- **The `withSession` rebind**: after `ctx.switchSession`, the old extension
|
|
229
|
+
instance's contexts are stale; all post-switch reporting goes through the
|
|
230
|
+
fresh `ctx` pi hands back.
|
|
231
|
+
|
|
232
|
+
## Validation
|
|
233
|
+
|
|
234
|
+
From the package root:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
node --test tests/helpers.test.mjs # 14 unit tests: paths, usage, timings,
|
|
238
|
+
# exclusive write, JSONL, checksum,
|
|
239
|
+
# document validation, revive plan
|
|
240
|
+
pi --list-models >/dev/null # extension loads without errors
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Interactive checklist: run a session with tools and branches; `/export-my-chat`;
|
|
244
|
+
verify mode `0600` (`ls -l`), confirm the entry/request counts in the notice,
|
|
245
|
+
confirm a second export to the same path fails; edit the JSON's entries
|
|
246
|
+
slightly and confirm `revive` refuses on checksum; revive on another machine
|
|
247
|
+
(or `/new` first at home) and confirm the name, tree (`/tree`), and
|
|
248
|
+
`/resume` listing.
|
|
249
|
+
|
|
250
|
+
## Package
|
|
251
|
+
|
|
252
|
+
Published as `pi-export-my-chat` (see `package.json`). Install with pi's
|
|
253
|
+
package mechanism:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
pi --install pi-export-my-chat
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The `pi.extensions` manifest points at `./index.ts`; `helpers.mjs` and
|
|
260
|
+
`tests/` ship alongside. No runtime dependencies — only node builtins and a
|
|
261
|
+
peer dependency on pi itself. `docs/export-format.md` holds the full field
|
|
262
|
+
reference for the export JSON.
|
|
263
|
+
|
|
264
|
+
## Repository layout
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
export-my-chat/
|
|
268
|
+
├── index.ts — the extension: capture, /export-my-chat, /export-my-chat:revive
|
|
269
|
+
├── helpers.mjs — pure logic: usage, timings, paths, canonical JSONL,
|
|
270
|
+
│ checksum, document validation, revive planning
|
|
271
|
+
├── tests/helpers.test.mjs — node:test unit tests for all of the above
|
|
272
|
+
├── docs/export-format.md — complete schema reference for the export JSON
|
|
273
|
+
├── README.md — this file
|
|
274
|
+
└── package.json — npm-publishable manifest + pi entry point
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
MIT license. Sessions are yours.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Export format reference
|
|
2
|
+
|
|
3
|
+
`/export-my-chat` writes one JSON document. This is the complete field
|
|
4
|
+
reference for `format: "pi-my-chat-export"`, `schemaVersion: 1`.
|
|
5
|
+
|
|
6
|
+
A minimal valid document has: `format`, `schemaVersion`, `session.entries`,
|
|
7
|
+
`session.header`, and `session.revive.checksum` consistent with the entries.
|
|
8
|
+
Everything else is additive — consumers should ignore unknown top-level keys
|
|
9
|
+
and tolerate missing optional fields (forward-compatible minor revisions).
|
|
10
|
+
|
|
11
|
+
## Top level
|
|
12
|
+
|
|
13
|
+
| Field | Type | Notes |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `format` | `"pi-my-chat-export"` | Discriminator. Revive refuses anything else. |
|
|
16
|
+
| `schemaVersion` | number | Currently `1`. Revive refuses values *newer* than it understands. |
|
|
17
|
+
| `exportedAt` | ISO string | Wall time of the export. |
|
|
18
|
+
| `exportedBy` | object | Provenance — see below. |
|
|
19
|
+
| `session` | object | The durable record + revive data. |
|
|
20
|
+
| `requests` | object | Every observed provider request. |
|
|
21
|
+
| `stats` | object | Derived usage/cost/context totals. |
|
|
22
|
+
| `timings` | object | Operation-timing records and summaries (if a timings extension produced any). |
|
|
23
|
+
|
|
24
|
+
### `exportedBy`
|
|
25
|
+
|
|
26
|
+
| Field | Notes |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `extension` | Always `"export-my-chat"`. |
|
|
29
|
+
| `extensionVersion` | Version of the extension that wrote the file. |
|
|
30
|
+
| `sessionHeaderVersion` | `session.header.version` as exported. |
|
|
31
|
+
| `currentSessionVersion` | The *writing* pi's max supported session format (`CURRENT_SESSION_VERSION`). A machine whose pi is older than the *header* version revives nothing — that check is against `session.header.version` itself. |
|
|
32
|
+
|
|
33
|
+
## `session`
|
|
34
|
+
|
|
35
|
+
| Field | Type | Notes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `id` | string | Session UUID. |
|
|
38
|
+
| `name` | string \| null | Latest `session_info` name; **also present in the entries** as a `session_info` entry. Metadata here is for humans and for revive's name fallback. |
|
|
39
|
+
| `cwd` | string | The *exporting* machine's working directory. Revive re-roots the rebuilt header to its own cwd; the original lives on here. |
|
|
40
|
+
| `sessionFile` | string \| null | Path of the session file on the exporting machine (diagnostic). |
|
|
41
|
+
| `header` | object | The session header line, verbatim (`type: "session"`, `version`, `id`, `timestamp`, `cwd`, optionally `parentSession`). |
|
|
42
|
+
| `leafId` | string \| null | Active leaf at export time. |
|
|
43
|
+
| `entries` | array | **The complete entry tree in insertion order.** The last entry is the active leaf. This ordering *is* the branch position; preserve it byte-for-byte when rebuilding. Entry shapes are pi's own (message / model_change / thinking_level_change / compaction / branch_summary / custom / custom_message / label / session_info). |
|
|
44
|
+
| `activeBranchEntryIds` | string[] | Ids of the entries on the active leaf→root path. |
|
|
45
|
+
| `activeContext` | object | `SessionManager.buildSessionContext()` — the message list the LLM would receive at export time, plus model/thinking settings. |
|
|
46
|
+
| `revive` | object | Revival contract — see below. |
|
|
47
|
+
|
|
48
|
+
### `session.revive`
|
|
49
|
+
|
|
50
|
+
| Field | Type | Notes |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `checksum` | string | **sha256 hex over the canonical JSONL**: `JSON.stringify(header)` line, then one `JSON.stringify(entry)` line per entry in order, joined by `\n` with a trailing `\n`. This is the exact byte sequence revive writes. Revive recomputes and refuses on mismatch. |
|
|
53
|
+
| `headerVersion` | number \| null | The header's `version` (convenience copy). |
|
|
54
|
+
| `entryCount` | number | `session.entries.length`. Revive cross-checks it against the actual entries array. |
|
|
55
|
+
| `lastEntryId` | string \| null | Last entry's id; revive cross-checks it against the actual last entry. |
|
|
56
|
+
| `matchesOriginalFile` | boolean \| null | Whether the canonical rebuild was byte-identical to pi's own on-disk session file at export time. Diagnostic only — a `false` never blocks anything, since the checksum covers what *revive writes*. |
|
|
57
|
+
| `note` | string | Human explanation of the above. |
|
|
58
|
+
|
|
59
|
+
**Declared revival transformations** (applied by revive *after* checksum
|
|
60
|
+
verification, so they never interact with it):
|
|
61
|
+
|
|
62
|
+
1. `session.header.cwd` → the reviving machine's cwd.
|
|
63
|
+
2. Session UUID kept as-is, unless a `<timestamp>_<uuid>.jsonl` with that UUID
|
|
64
|
+
already exists in the target session dir → fresh UUID minted.
|
|
65
|
+
3. If `session.name` is a non-empty string and no `session_info` entry exists in
|
|
66
|
+
`entries`, one `session_info` entry is appended (parentId = last entry id)
|
|
67
|
+
— the same mechanism pi's `/name` uses.
|
|
68
|
+
|
|
69
|
+
The revived file is named with pi's own timestamp convention
|
|
70
|
+
(`<toISOString().replace(/[:.]/g, "-")>_<uuid>.jsonl`, e.g.
|
|
71
|
+
`2026-04-13T12-34-56-789Z_11111111-….jsonl`) so it is indistinguishable from
|
|
72
|
+
sessions pi created itself.
|
|
73
|
+
|
|
74
|
+
## `requests`
|
|
75
|
+
|
|
76
|
+
| Field | Type | Notes |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `captureStartedAt` | ISO string | Start of the current capture window (resets on session_start / resume / `/reload`). |
|
|
79
|
+
| `observedRequestCount` | number | Total requests seen this window — equals `records.length` unless a capture error occurred. |
|
|
80
|
+
| `scratchFile` | string \| null | The per-session journal under the agent cache dir (diagnostic; safe to prune). |
|
|
81
|
+
| `latestCaptureError` | string \| null | Last capture failure, if any — the export still succeeds with whatever was journaled. |
|
|
82
|
+
| `records` | array | One record per observed request, **in order** — see below. |
|
|
83
|
+
| `payloads` | object | `sha256 → payload` store. Byte-identical payloads appear once; `records[i].payloadSha256` is the join key. Content and count are preserved; only duplicate *bytes* are factored out. |
|
|
84
|
+
| `note` | string | Capture boundaries: auth headers excluded; a response is not part of the request that produced it; later-loaded extensions may mutate the payload after capture; capture resets with the runtime. |
|
|
85
|
+
|
|
86
|
+
### `requests.records[i]`
|
|
87
|
+
|
|
88
|
+
| Field | Type | Notes |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `n` | number | 1-based request number within the window. |
|
|
91
|
+
| `capturedAt` | ISO string | When `before_provider_request` observed it. |
|
|
92
|
+
| `provider`, `model`, `api` | string \| undefined | Model metadata as of the request. |
|
|
93
|
+
| `contextUsage` | object | Live snapshot at capture: `used`, `window`, `remaining`, `percentUsed` — `used`/`remaining`/`percentUsed` are `null` when pi could not estimate tokens at that moment (e.g. right after compaction); `window` falls back to the usage snapshot's own `contextWindow` when the model's is unknown. |
|
|
94
|
+
| `payloadSha256` | string | Join key into `requests.payloads`. |
|
|
95
|
+
|
|
96
|
+
## `stats`
|
|
97
|
+
|
|
98
|
+
| Field | Notes |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `source` | Statement of what was counted. |
|
|
101
|
+
| `activeBranch`, `wholeSession` | Same shape; scope differs. `usageRecords` counts entries that carried usage. Token fields: `input`, `output`, `cacheRead`, `cacheWrite`, `totalTokens` (provider-reported total preferred, else component sum). Cost object: per-component plus `total`. Sources: assistant messages, toolResult nested usage, `compaction` and `branch_summary` entries. Timing records never contribute. NaN/Infinity/negative inputs clamp to 0. |
|
|
102
|
+
| `liveContext` | Context snapshot at export time (same shape as `contextUsage` above). |
|
|
103
|
+
|
|
104
|
+
## `timings`
|
|
105
|
+
|
|
106
|
+
| Field | Notes |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `source` | Where the records came from (the timings extension's persisted `operation-timing` custom entries). |
|
|
109
|
+
| `activeBranch.records` / `wholeSession.records` | Raw records: `schemaVersion: 1`, `operationId`, `kind`, `startedAt`, `endedAt`, `durationMs`, `status`. |
|
|
110
|
+
| `activeBranch.summary` / `wholeSession.summary` | `recordCount`, `byKind[kind]` = `{ count, totalDurationMs, averageDurationMs, minDurationMs, maxDurationMs, statuses }`, plus an overlap note. Kinds overlap in wall time — never sum across kinds. |
|
|
111
|
+
|
|
112
|
+
## Revive algorithm (normative)
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
1. **guard**: pi idle (checked before any reading)
|
|
116
|
+
2. path := resolve(args) # relative ok; quotes ok; --force flag
|
|
117
|
+
3. doc := JSON.parse(read(path))
|
|
118
|
+
4. validate(doc):
|
|
119
|
+
doc.format == "pi-my-chat-export"
|
|
120
|
+
doc.schemaVersion <= SCHEMA_VERSION
|
|
121
|
+
doc.session.header.version <= CURRENT_SESSION_VERSION
|
|
122
|
+
doc.session.revive.entryCount == session.entries.length (when declared)
|
|
123
|
+
validateSessionCore(header, entries,
|
|
124
|
+
expectedChecksum=doc.session.revive.checksum,
|
|
125
|
+
expectedLastEntryId=doc.session.revive.lastEntryId)
|
|
126
|
+
5. guard: non-empty current session -> confirm (or --force; headless has no
|
|
127
|
+
confirm dialog and must pass --force)
|
|
128
|
+
6. plan := buildRevivePlan(doc):
|
|
129
|
+
header.cwd := ctx.cwd
|
|
130
|
+
uuid := original unless collides in sessionDir, else fresh
|
|
131
|
+
if session.name and no session_info entry: append one
|
|
132
|
+
jsonl := canonical(header', entries')
|
|
133
|
+
filePath := <sessionDir>/<pi-format timestamp>_<uuid>.jsonl
|
|
134
|
+
7. write jsonl exclusively, 0600, fsync
|
|
135
|
+
8. switch := ctx.switchSession(filePath)
|
|
136
|
+
switch.cancelled -> notify (file is saved and /resume-able)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Everything from step 3 on uses the same `buildSessionJsonl` as the exporter,
|
|
140
|
+
so the checksum is consistent by construction.
|