Files
poimen-memory/tasks/M0.5-pi-session-adapter.md

132 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M0.5 — pi session adapter
| Field | Value |
|---|---|
| Phase | M0 — Read-only spine |
| Size | M — 13 days |
| Status | ✅ Done |
| 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`