Skip to content

OpenCode parser: populate TurnRecord.fidelity #89

@willwashburn

Description

@willwashburn

Context

PR #76 (#41 first cut) introduced TurnRecord.fidelity (granularity + per-field coverage + derived class) and the helpers EMPTY_COVERAGE, classifyFidelity, makeFidelity in @relayburn/reader. The Claude parser populates fidelity on every turn (packages/reader/src/claude.ts:255,1049 via buildClaudeFidelity at :604).

The OpenCode parser does not. Quoting PR #76's deferred-work paragraph:

Codex and OpenCode parsers (they still emit no fidelity; consumers treat absence as best-effort full for backward compat).

packages/reader/src/opencode.ts emits TurnRecord objects without any fidelity field. As a result, summarizeFidelity (packages/analyze/src/fidelity.ts) buckets every OpenCode turn under unknown, and hasMinimumFidelity(undefined, …) passes silently — the exact "0 vs unknown" ambiguity #41 was filed to fix.

Proposal

Populate TurnRecord.fidelity in parseOpencodeSession / parseOpencodeSessionIncremental (packages/reader/src/opencode.ts) on every emitted turn.

Coverage for OpenCode assistant messages (AssistantMessage.tokens at packages/reader/src/opencode.ts:27-35,46):

  • hasInputTokens: true when tokens.input is present on the assistant message (or rolled up from step-finish parts), otherwise false.
  • hasOutputTokens: true when tokens.output is present, otherwise false.
  • hasReasoningTokens: true when tokens.reasoning is present, otherwise false.
  • hasCacheReadTokens: true when tokens.cache?.read is present, otherwise false.
  • hasCacheCreateTokens: true when tokens.cache?.write is present, otherwise false (OpenCode collapses 5m/1h ephemeral spans into a single cache.write count).
  • hasToolCalls: truetool parts are captured.
  • hasToolResultEvents: true — tool state.output and state.metadata are captured for tool-result attribution.
  • hasSessionRelationships: true — OpenCode already exposes session.parentID (read at parse time to populate isSidechain).
  • hasRawContent: true — full content capture path exists when contentMode === 'full'.

Granularity: 'per-turn'. OpenCode's per-message tokens accumulate into a per-turn record at the assistant-message boundary.

Mirror Claude's pattern: track per-field token presence as parts are folded into the turn (analogous to the WorkingRecord.usageCoverage accumulator in claude.ts), so a turn that gets step-finish for input/output but no cache fields reports hasCacheReadTokens: false rather than silently rendering cacheRead === 0.

Acceptance criteria

  • Every TurnRecord emitted by parseOpencodeSession / parseOpencodeSessionIncremental carries a fidelity field.
  • granularity === 'per-turn'.
  • Coverage flags follow the matrix above; usage flags reflect presence on the upstream message, capability flags (hasToolCalls, hasToolResultEvents, hasSessionRelationships, hasRawContent) are always true.
  • A turn whose assistant message had no tokens block reports class === 'partial' (or 'cost-only' if literally only timestamps survive); a fully-populated turn reports class === 'full'.
  • New tests in packages/reader/src/opencode.test.ts cover a full-fidelity turn, a tokens-missing turn (partial), and a turn with cache fields populated (hasCacheReadTokens and hasCacheCreateTokens both true).
  • summarizeFidelity over a real OpenCode session yields unknown === 0 for the produced turns.

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