You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
Schema: add a UserTurnLine variant alongside TurnLine / StampLine / CompactionLine in packages/ledger/src/schema.ts. Add isUserTurnLine predicate. Update LedgerLine union.
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.
Index sidecar: add userTurnIdHash next to turnIdHash / compactionIdHash in packages/ledger/src/index-sidecar.ts. Reuse the existing id namespace.
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).
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.
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.
Context
PR #74 landed
UserTurnRecord/UserTurnBlockin@relayburn/readerand emits them from the Claude parser, but stopped short of persistence. From the PR's Deferred section: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 touserTurnsexists inpackages/cli/src/ingest.tsorpackages/ledger/src/. The ledger schema (packages/ledger/src/schema.ts:38) definesLedgerLine = TurnLine | StampLine | CompactionLine; there's noUserTurnLine. The writer (packages/ledger/src/writer.ts:45-69forappendTurns,:71-94forappendCompactions) 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
UserTurnRecordend-to-end:UserTurnLinevariant alongsideTurnLine/StampLine/CompactionLineinpackages/ledger/src/schema.ts. AddisUserTurnLinepredicate. UpdateLedgerLineunion.appendUserTurns(records: UserTurnRecord[])inpackages/ledger/src/writer.ts, modeled onappendCompactions— dedup viaindex-sidecar.tsusing auserTurnIdHash(hash ofsessionId + userUuid), append-only, holds the sameledgerlock.userTurnIdHashnext toturnIdHash/compactionIdHashinpackages/ledger/src/index-sidecar.ts. Reuse the existing id namespace.packages/ledger/src/reader.tsquery API so consumers can fetch user turns alongside turns (e.g.queryUserTurns(q: Query)or include them in an enriched query result keyed bysessionId).packages/cli/src/ingest.ts, forward theuserTurnsreturned byparseClaudeSession{,Incremental}intoappendUserTurns. 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.vbump onTurnRecord. The newUserTurnLineitself carriesv: 1.@relayburn/ledgerand the ingest forwarding.Acceptance criteria
LedgerLineunion includesUserTurnLineandisUserTurnLinepredicate is exported.appendUserTurnsexists, dedupes by stable id, and holds theledgerlock.burn ingestagainst a Claude session produces user-turn lines in the ledger file (grep '\"kind\":\"user-turn\"' \$RELAYBURN_HOME/ledger.jsonlreturns rows).precedingMessageId/followingMessageId.Out of scope
userTurns— issues Reader: populate UserTurnRecord for Codex sessions #81 / Reader: populate UserTurnRecord for OpenCode sessions #86.burn rebuild) — note in CHANGELOG; can be a separate issue if needed.Refs