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 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.