Wave 1: total message model — parse totally, destroy nothing #51

Closed
mprihoda wants to merge 11 commits from wave-1-core-model into adr/session-is-a-stream
mprihoda commented 2026-07-15 11:30:06 +00:00 (Migrated from github.com)

Implements ADR 0001 decision 6 (docs/adr/0001-session-is-a-stream-not-turns.md) and proposal §2.1/§4 Wave 1.

What's here

  • Docstring hazard fix first (per the ADR's "goes first, today"): all three Session.stream scaladocs now state the real contract — shared long-lived queue, out-of-turn emission, mid-turn merge, single-consumer constraint.
  • UnknownMessage(messageType, json)parseMessage is total; rate_limit_event and the system subtypes survive as raw JSON instead of vanishing in case _ => None.
  • MessageId (vendor uuid) on AssistantMessage/ResultMessage — the live↔transcript join key.
  • ResultMessage models the full wire envelope: origin: Option[ResultOrigin] (key-absent ≠ null — the in-turn/out-of-turn discriminator), stopReason, terminalReason, permissionDenials (raw JSON, shape unsampled), apiErrorStatus, timings, and usage: Option[TokenUsage] with real token counts (Map.empty is dead).
  • AssistantMessage.parentToolUseId — the subagent/sidechain join key — plus model.
  • ResultOrigin open enum (TaskNotification / Other(kind)).
  • Control protocol types (ControlRequest/ControlRequestBody/ControlResponse) — types only; wiring lands with the Wave 2 Session redesign.
  • TokenUsage moved to core.model (shared by stream and log parsers via one fromJson decoder; counts widened to Long), no alias left behind.
  • Compile-fix pass across all four modules; positional patterns converted to the field-access idiom.

Recorded deviations from the proposal snippet

  • id is Option[MessageId] (not MessageId): a required id would destroy wire messages lacking uuid (observed: only 5/22 probe messages carry it), violating decision 6 itself.
  • New ResultMessage fields appended after result rather than interleaved, so existing constructions keep compiling — how the proposal priced this wave.
  • parseMessage: Json => Message (module parsers wrap in Some); known type with missing required fields degrades to UnknownMessage instead of vanishing.
  • origin: null (never observed) → Some(Other("null")) — a present key is never conflated with absent.

Verification

./mill __.compile warning-free; unit tests 424 → 451+ passing (new JsonParserTotalityTest matrix, property generators extended over the new fields incl. populated permission_denials, UnknownMessage, ControlResponse); full ./mill __.itest 730/730 against the real CLI. Reviewed by four dimension reviewers with adversarial verification; all confirmed findings fixed in the last four commits.

🤖 Generated with Claude Code

Implements ADR 0001 decision 6 (docs/adr/0001-session-is-a-stream-not-turns.md) and proposal §2.1/§4 Wave 1. ## What's here - **Docstring hazard fix first** (per the ADR's "goes first, today"): all three `Session.stream` scaladocs now state the real contract — shared long-lived queue, out-of-turn emission, mid-turn merge, single-consumer constraint. - **`UnknownMessage(messageType, json)`** — `parseMessage` is total; `rate_limit_event` and the system subtypes survive as raw JSON instead of vanishing in `case _ => None`. - **`MessageId`** (vendor `uuid`) on `AssistantMessage`/`ResultMessage` — the live↔transcript join key. - **`ResultMessage`** models the full wire envelope: `origin: Option[ResultOrigin]` (key-absent ≠ null — the in-turn/out-of-turn discriminator), `stopReason`, `terminalReason`, `permissionDenials` (raw JSON, shape unsampled), `apiErrorStatus`, `timings`, and `usage: Option[TokenUsage]` with real token counts (`Map.empty` is dead). - **`AssistantMessage.parentToolUseId`** — the subagent/sidechain join key — plus `model`. - **`ResultOrigin`** open enum (`TaskNotification` / `Other(kind)`). - **Control protocol types** (`ControlRequest`/`ControlRequestBody`/`ControlResponse`) — types only; wiring lands with the Wave 2 Session redesign. - **`TokenUsage` moved to `core.model`** (shared by stream and log parsers via one `fromJson` decoder; counts widened to `Long`), no alias left behind. - Compile-fix pass across all four modules; positional patterns converted to the field-access idiom. ## Recorded deviations from the proposal snippet - `id` is `Option[MessageId]` (not `MessageId`): a required id would destroy wire messages lacking `uuid` (observed: only 5/22 probe messages carry it), violating decision 6 itself. - New `ResultMessage` fields appended after `result` rather than interleaved, so existing constructions keep compiling — how the proposal priced this wave. - `parseMessage: Json => Message` (module parsers wrap in `Some`); known type with missing required fields degrades to `UnknownMessage` instead of vanishing. - `origin: null` (never observed) → `Some(Other("null"))` — a present key is never conflated with absent. ## Verification `./mill __.compile` warning-free; unit tests 424 → 451+ passing (new `JsonParserTotalityTest` matrix, property generators extended over the new fields incl. populated `permission_denials`, `UnknownMessage`, `ControlResponse`); full `./mill __.itest` 730/730 against the real CLI. Reviewed by four dimension reviewers with adversarial verification; all confirmed findings fixed in the last four commits. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mprihoda commented 2026-07-15 14:26:09 +00:00 (Migrated from github.com)

Superseded by the consolidated PR #53 (single review over the full ADR 0001 implementation); the branch and its per-wave review history remain.

Superseded by the consolidated PR #53 (single review over the full ADR 0001 implementation); the branch and its per-wave review history remain.

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/claude-code-query!51
No description provided.