# Pi session resume

Pi version inspected: `@earendil-works/pi-coding-agent@0.78.0`.
Sources checked: Pi docs plus installed `dist/core/session-manager.js`, `dist/main.js`, and our mirror at `packages/sandbox/src/pi-extension/thread-mirror.ts`.

## 1. What lives in `--session-dir`?

Only session JSONL files. Pi-created filenames are:

```text
<iso-timestamp-with-:-.-replaced>_<sessionId>.jsonl
```

Example:

```text
2026-05-31T10-12-13-456Z_019...jsonl
```

`--continue`/listing scan `*.jsonl`; filename is not parsed except extension. The session id comes from the header line.

First line:

```json
{"type":"session","version":3,"id":"session-id","timestamp":"ISO","cwd":"/working/dir","parentSession":"optional"}
```

Subsequent entries are append-only tree records:

```json
{"type":"message","id":"u1","parentId":null,"timestamp":"ISO","message":{"role":"user","content":"...","timestamp":1790000000000}}
{"type":"message","id":"a1","parentId":"u1","timestamp":"ISO","message":{"role":"assistant","content":[{"type":"text","text":"..."}],"api":"...","provider":"...","model":"...","usage":{...},"stopReason":"stop","timestamp":1790000000001}}
```

Other top-level entry types: `model_change`, `thinking_level_change`, `compaction`, `branch_summary`, `custom`, `custom_message`, `label`, `session_info`.

Tool calls are assistant content blocks:

```json
{"type":"toolCall","id":"call_1","name":"read","arguments":{"path":"x"}}
```

Tool results are separate `message` entries whose `message.role` is `toolResult`:

```json
{"type":"message","id":"t1","parentId":"a1","timestamp":"ISO","message":{"role":"toolResult","toolCallId":"call_1","toolName":"read","content":[{"type":"text","text":"..."}],"isError":false,"timestamp":1790000000002}}
```

Pi also has extended message roles such as `bashExecution` and `custom` inside `type:"message"` entries.

## 2. Can we hand-write a file into a fresh dir and use `--continue`?

Yes, if it is a valid `.jsonl` file with a valid first header and its header `cwd` matches the current cwd when using a custom `--session-dir`.

No extra index, pointer, lock, checksum, or pi-managed sidecar is required. Pi scans files directly.

But there are two gotchas:

1. For custom `--session-dir`, `--continue` filters sessions by header `cwd === process.cwd()`.
2. In non-interactive modes, Pi rejects a loaded session if the header `cwd` does not exist.

So for fresh-sandbox reconstruction, either:

- write `cwd` as the sandbox working dir that will run Pi, or
- avoid `--continue` and use `--session <path>` with a header `cwd` that exists.

## 3. How does `--continue` choose?

`SessionManager.continueRecent(cwd, sessionDir)`:

- scans `sessionDir` for `*.jsonl`
- reads only the first line/header
- filters invalid headers
- with a custom non-default `sessionDir`, filters by header `cwd` matching current cwd
- sorts by file `mtime`, newest first
- opens the newest

No pointer file. Not single-session-per-dir.

With `arbe-sessions/<threadId>`, it is only one session if we keep it that way. Pi does not enforce it.

## 4. Minimum records needed to remember context

For load: header + any `message` entries on a valid parent chain are enough. Pi does very little schema validation at load time.

For useful continuation: user + assistant text turns are enough for memory. Tool records are not required if you omit tool calls too.

Important: do not include assistant `toolCall` blocks without matching `toolResult` messages unless you know the provider accepts that. Most chat/tool APIs expect tool-result consistency. A safe context-only reconstruction should either:

- include full assistant toolCall + toolResult pairs, or
- flatten/summarize tool activity into plain text and omit toolCall blocks entirely.

## 5. Are our mirrored `pi.*` thread entries verbatim session lines?

No. Current mirror is a transformed/lossy projection.

Path:

```text
thread-mirror.ts
  -> createPiLiveEntryIngestor()
  -> decodePiEvent()
  -> emits arbe thread payloads
```

It emits normalized thread entries:

- `pi.chunk` from `message_update`
- `pi.assistant` from assistant `message_end`
- `pi.tool_result` from tool-result `message_end`
- `pi.compaction` from `compaction_end`

It synthesizes envelope ids like:

```text
<threadId>:pi:idx:<n>
```

Those are not Pi session-entry ids, and there is no Pi `parentId`. User turns are not mirrored as Pi session messages; in arbe they live as `chat` entries. Many Pi session features are absent: exact top-level session entries, tree lineage, labels, model/thinking entries, exact session-file ids, etc.

So: current thread entries cannot be concatenated back into a verbatim Pi session file. We can reconstruct an approximate valid file, but not the original file.

If we want lossless resume-anywhere, enrich the mirror to carry raw/full Pi session entries or periodic raw session-file snapshots. If we only need semantic resume, synthesize a linear Pi file from `chat` + `pi.assistant`/`pi.tool_result`.

## 6. Session header validity / cwd

Load-time validation is minimal: first parsed JSONL object must have:

```json
{"type":"session","id":"string"}
```

`version`, `timestamp`, and `cwd` are expected by types/docs but not strictly checked by `loadEntriesFromFile()`.

However, CLI startup then checks `sessionManager.getCwd()` exists. Since `SessionManager.open()` uses `header.cwd ?? process.cwd()`, a stale non-existent `cwd` makes non-interactive/RPC/print exit with:

```text
Stored session working directory does not exist: ...
```

A different-but-existing cwd is accepted; Pi uses that as the runtime cwd. Therefore, reconstructed sessions should set header `cwd` to the fresh sandbox's intended workdir.

## 7. `id` / `parentId`

Entry ids do not have to be UUIDs. Pi-generated entry ids are 8 hex chars, but open/load does not validate that. For a linear reconstruction, assign stable unique strings and a parent chain:

```text
u1 parent null
ass1 parent u1
u2 parent ass1
ass2 parent u2
```

The header session id must only be a string for open/load. If Pi itself creates a session id (`--session-id` / `SessionManager.create`), it validates: alphanumeric start/end, and only alnum `- _ .` inside.

## 8. Tool entries: required?

Required for exact replay: yes.

Required for Pi to load: no.

Required for provider-valid continuation: only if assistant messages contain tool calls. Keep tool-call/tool-result pairs internally consistent, or omit tool calls and turn prior tool work into normal text/summary.

For resume-anywhere, the safest synthesized transcript is often:

- user/chat text
- assistant final text
- optional plain-text summaries of tool outputs that matter
- no historical `toolCall` blocks unless we can reconstruct all matching `toolResult`s

## 9. Native import/seed/fork paths

No documented CLI flag imports arbitrary external transcripts.

Native paths are file-oriented:

- `--session <path|id>` opens a supplied file path or resolves a session id/prefix.
- `--fork <path|id>` copies an existing Pi JSONL session into a new session with a new header/cwd.
- SDK `SessionManager.open(path)` opens a file.
- SDK runtime mentions import-from-JSONL flows, but this is still Pi JSONL, not arbitrary transcript magic.

So option (c) only beats hand-writing if we already have Pi-compatible JSONL.

## 10. `--continue` vs `--session <path|id>`

For resume-anywhere, use `--session <reconstructed-path>` as the portable default.

Why:

- avoids cwd-filtered directory scan
- avoids mtime races if multiple sessions exist under `arbe-sessions/<threadId>`
- makes the selected lineage explicit

Use `--continue` only for same-sandbox disk cache where the session dir is trusted and current cwd/header cwd match.

`--fork` is not what we want for same-lineage continuation. It creates a new session file/header and records `parentSession`. Use `--fork` when intentionally branching/copying into a new lineage. For thread-backed resume, straight `--session <file>` is the right shape: append to the reconstructed lineage.

## Contract recommendation

Current mirror = projection. Therefore choose one:

1. **Semantic reconstruction now**: synthesize a linear Pi JSONL from arbe `chat` + mirrored `pi.assistant`/`pi.tool_result`; flatten tools unless complete pairs are available.
2. **Lossless later**: enrich the mirror to persist raw/full Pi session entries or snapshots in the thread. Then hydration is concat/copy instead of inference.

For fresh-sandbox robust resume, add:

```text
hydratePiSessionFromThread(thread, sandboxCwd, targetPath)
  -> writes header.cwd = sandboxCwd
  -> writes linear id/parentId chain
  -> run pi --session <targetPath>
```
