Skip to content

Ledger: persist UserTurnRecord and forward through ingest #94

@willwashburn

Description

@willwashburn

Context

PR #74 landed UserTurnRecord / UserTurnBlock in @relayburn/reader and emits them from the Claude parser, but stopped short of persistence. From the PR's Deferred section:

Persisting UserTurnRecord to the ledger. Currently in-memory parser output only; persistence (and any schema v bump) is a separate decision driven by the first consumer (burn waste).

Today the parser returns userTurns: UserTurnRecord[] (packages/reader/src/claude.ts:121-126, packages/reader/src/claude.ts:283, packages/reader/src/claude.ts:1149-1156) but the CLI ingest path drops them on the floor — no reference to userTurns exists in packages/cli/src/ingest.ts or packages/ledger/src/. The ledger schema (packages/ledger/src/schema.ts:38) defines LedgerLine = TurnLine | StampLine | CompactionLine; there's no UserTurnLine. The writer (packages/ledger/src/writer.ts:45-69 for appendTurns, :71-94 for appendCompactions) has no equivalent for user turns.

Without persistence, the per-tool-call cost attribution consumer (the original goal of #2) can't read user-turn block sizes back out of the ledger; it can only see them when re-parsing source session files at query time, which defeats the point of an incremental, append-only ledger.

Proposal

Wire UserTurnRecord end-to-end:

  1. Schema: add a UserTurnLine variant alongside TurnLine / StampLine / CompactionLine in packages/ledger/src/schema.ts. Add isUserTurnLine predicate. Update LedgerLine union.
  2. Writer: add appendUserTurns(records: UserTurnRecord[]) in packages/ledger/src/writer.ts, modeled on appendCompactions — dedup via index-sidecar.ts using a userTurnIdHash (hash of sessionId + userUuid), append-only, holds the same ledger lock.
  3. Index sidecar: add userTurnIdHash next to turnIdHash / compactionIdHash in packages/ledger/src/index-sidecar.ts. Reuse the existing id namespace.
  4. Reader: extend packages/ledger/src/reader.ts query API so consumers can fetch user turns alongside turns (e.g. queryUserTurns(q: Query) or include them in an enriched query result keyed by sessionId).
  5. Ingest: in packages/cli/src/ingest.ts, forward the userTurns returned by parseClaudeSession{,Incremental} into appendUserTurns. Future Codex/OpenCode parsers (issues Reader: populate UserTurnRecord for Codex sessions #81, Reader: populate UserTurnRecord for OpenCode sessions #86) plug into the same call site.
  6. Schema version: this is additive — existing readers already destructure specific line kinds and ignore unknown ones — so no v bump on TurnRecord. The new UserTurnLine itself carries v: 1.
  7. AGENTS.md / package CHANGELOGs: note the new line kind in @relayburn/ledger and the ingest forwarding.

Acceptance criteria

  • LedgerLine union includes UserTurnLine and isUserTurnLine predicate is exported.
  • appendUserTurns exists, dedupes by stable id, and holds the ledger lock.
  • Running burn ingest against a Claude session produces user-turn lines in the ledger file (grep '\"kind\":\"user-turn\"' \$RELAYBURN_HOME/ledger.jsonl returns rows).
  • Re-running ingest is a no-op for already-persisted user turns (dedup test).
  • Ledger reader exposes user turns either via a dedicated query function or as an enriched-turn association keyed by precedingMessageId / followingMessageId.
  • Existing ledger consumers (turns / stamps / compactions) keep working unchanged — older readers gracefully ignore the new line kind.
  • Tests: writer dedup, reader round-trip, ingest forward.

Out of scope

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions