132 lines
4.9 KiB
Markdown
132 lines
4.9 KiB
Markdown
# M0.5 — pi session adapter
|
||||
|
|
|
|||
|
|
| Field | Value |
|
|||
|
|
|---|---|
|
|||
|
|
| Phase | M0 — Read-only spine |
|
|||
|
|
| Size | M — 1–3 days |
|
|||
|
|
| Status | ⬜ Not started |
|
|||
|
|
| Flags | — |
|
|||
|
|
| Spec | inlined below |
|
|||
|
|
| Blocks | M0.3 |
|
|||
|
|
|
|||
|
|
## Goal
|
|||
|
|
|
|||
|
|
Turn pi's session JSONL into normalised `Record`s, and resolve the project key
|
|||
|
|
from the path without guessing.
|
|||
|
|
|
|||
|
|
## Facts (inlined — no spec read needed)
|
|||
|
|
|
|||
|
|
Layout, verified on this machine:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
~/.pi/agent/sessions/--Users-rockliang-workplace-Poimen-agent-rust--/<ts>_<uuid>.jsonl
|
|||
|
|
└─ cwd with / replaced by -, wrapped in leading and trailing --
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Record types observed in a real 2902-message session:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
message 2902
|
|||
|
|
model_change 112
|
|||
|
|
thinking_level_change 16
|
|||
|
|
compaction 8
|
|||
|
|
session 1 <- always first line
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Shapes:
|
|||
|
|
|
|||
|
|
```jsonc
|
|||
|
|
// first line
|
|||
|
|
{"type":"session","version":..,"id":"..","timestamp":"..","cwd":"/Users/.../Poimen"}
|
|||
|
|
// message
|
|||
|
|
{"type":"message","id":"..","parentId":"..","timestamp":"..",
|
|||
|
|
"message":{"role":"assistant|user|toolResult","content":..,"timestamp":".."}}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Role distribution in that same session — this is the whole reason the update gate
|
|||
|
|
exists:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
assistant 1445
|
|||
|
|
toolResult 1261 43%, mostly evidence-free
|
|||
|
|
user 196
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`cwd` in the `session` header is authoritative for the project key. The directory
|
|||
|
|
name is a lossy encoding (a real `-` in a path is indistinguishable from a
|
|||
|
|
separator) — **parse `cwd`, do not decode the directory name.**
|
|||
|
|
|
|||
|
|
`content` is not always a string. Assistant messages carry content blocks; tool
|
|||
|
|
results carry structured payloads. Normalise to text, and keep the block type in
|
|||
|
|
`Provenance` so a later filter can act on it.
|
|||
|
|
|
|||
|
|
## Steps
|
|||
|
|
|
|||
|
|
1. Implement `PiSessionSource` in `mem-ingest`, implementing `RecordSource`.
|
|||
|
|
2. Read the first line, require `type == "session"`, take `cwd` as the project
|
|||
|
|
key. A file whose first line is not a session header is an error naming the
|
|||
|
|
file, not a skip.
|
|||
|
|
3. Stream subsequent lines; emit a `Record` per `type == "message"`.
|
|||
|
|
4. Map roles: `user -> Role::User`, `assistant -> Role::Assistant`,
|
|||
|
|
`toolResult -> Role::ToolResult`.
|
|||
|
|
5. Flatten `content` to text for all shapes; preserve the original block type in
|
|||
|
|
`Provenance`.
|
|||
|
|
6. Ignore `model_change`, `thinking_level_change`. **Do not ignore `compaction`** —
|
|||
|
|
emit it as `Role::System` with the marker text, because a compaction boundary
|
|||
|
|
is where context was lost and that is worth seeing in the log.
|
|||
|
|
7. `Provenance` = `pi:<session-file-stem>` plus the record's `id` and line offset.
|
|||
|
|
8. A malformed line is a counted, reported skip — never a panic. These files are
|
|||
|
|
appended to by a live process and the last line may be a partial write.
|
|||
|
|
|
|||
|
|
## Acceptance
|
|||
|
|
|
|||
|
|
- Project key comes from `cwd`, matching for a path containing a literal `-`.
|
|||
|
|
- All three roles are emitted with correct counts on a real session.
|
|||
|
|
- A truncated final line is skipped with a warning, not a panic.
|
|||
|
|
- Compaction events appear in the record stream.
|
|||
|
|
|
|||
|
|
## Verify
|
|||
|
|
|
|||
|
|
**Harness:** two committed fixtures — one small hand-built session covering every
|
|||
|
|
record type and content shape, one real session copied verbatim (secrets
|
|||
|
|
scrubbed) for volume.
|
|||
|
|
|
|||
|
|
**Integration test** — `tests/it_pi_source.rs`:
|
|||
|
|
1. `a1_project_from_cwd` — fixture whose `cwd` is `/tmp/my-project`; assert the
|
|||
|
|
key is `/tmp/my-project`, proving the directory name was not decoded.
|
|||
|
|
2. `a2_role_counts` — on the real fixture, assert exact counts per role.
|
|||
|
|
3. `a3_content_shapes` — string content, block-array content, and structured tool
|
|||
|
|
result all flatten to non-empty text.
|
|||
|
|
4. `a4_truncated_tail` — append half a JSON object; assert the source yields all
|
|||
|
|
prior records and reports exactly one skip.
|
|||
|
|
5. `a5_missing_header` — file whose first line is a `message`; assert an error
|
|||
|
|
naming the path.
|
|||
|
|
6. `a6_compaction_emitted` — assert compaction events appear as `Role::System`.
|
|||
|
|
7. `a7_stream_is_lazy` — a source over a 50 MB fixture must yield its first
|
|||
|
|
record before reading the whole file (assert peak allocation, or instrument
|
|||
|
|
reads).
|
|||
|
|
|
|||
|
|
**Command:** `cargo test -p mem-ingest pi_source`
|
|||
|
|
|
|||
|
|
**False pass:**
|
|||
|
|
- Testing only against the hand-built fixture. It will contain the content shapes
|
|||
|
|
you thought of, which is the set you already handle. Assertion 2 against a real
|
|||
|
|
session is what finds the rest.
|
|||
|
|
- Asserting "no panic" on malformed input without asserting the *count* of
|
|||
|
|
skips. A source that silently drops every line panics never and ingests
|
|||
|
|
nothing.
|
|||
|
|
|
|||
|
|
## Traps
|
|||
|
|
|
|||
|
|
- Decoding the directory name to get the project. `--Users-rockliang-workplace-my-proj--`
|
|||
|
|
is ambiguous the moment a path component contains `-`, which is common.
|
|||
|
|
- Treating `content` as `String`. It parses for user messages and fails on
|
|||
|
|
assistant blocks, so the bug appears to be about assistants specifically and
|
|||
|
|
wastes an afternoon.
|
|||
|
|
- Materializing the file. These reach tens of MB and the trait is a stream for a
|
|||
|
|
reason.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
Background: [DESIGN.md](../DESIGN.md) — Context, `mem-chunk`
|