Eliminate implicit-absence meanings from the wire before the baseline freezes #327

Closed
opened 2026-09-21 23:15:26 +09:00 by natsukium · 1 comment
Owner

Background

The pre-freeze review (parent #326) found that several wire shapes assign meaning to a field's absence. In proto3, absence cannot be told apart from "not sent by a peer that does not know the field", so every such meaning becomes ambiguous the moment the schema grows. Each case below is cheap to fix now (a oneof arm or a required field) and costs a major bump after the baseline freezes.

This does not reopen #47/#48/#313: it only asks that absence stop meaning something.

Cases

  1. RetargetTarget.carrier — unset means "default local daemon". The common case costs no bytes, but a future carrier added beside it cannot be told apart from an old peer that sent nothing. Add an explicit Default arm (or document unset as permanently the default and reserve against ever giving it meaning).
  2. OpsStop.mode — unset means if_empty. Same shape as (1): an unset oneof silently selects the safe posture today and silently misreads tomorrow's posture. Make it required or explicit.
  3. SessionAttach.id / id_prefix — sibling fields where exactly one is set, enforced by prose plus validator. A oneof target states the same constraint in the schema, so non-Rust peers read it from the descriptor instead of a comment.
  4. Reset spellings differ per family: GridThemeColor uses absent-Rgb = reset while GridPaletteColor uses oneof { set, reset }. The latter fails a truncated write at decode instead of reading it as a reset. Unify on the oneof form.

Acceptance criteria

  • Each of the four cases has an explicit decision (schema change or a recorded "unset stays X forever" rule) before the baseline in #115 lands.
  • felis.proto comments, docs/reference/ipc.md message-family tables, and crates/felis-protocol/src/convert/ agree on the result.
  • BREAKING.md acknowledges the change against the merge-base per the pre-release rule.

Parent: #326 (final interface simplification audit).
Related: #313 (protocol 1.0 baseline), #115 (baseline workflow).

## Background The pre-freeze review (parent #326) found that several wire shapes assign meaning to a field's *absence*. In proto3, absence cannot be told apart from "not sent by a peer that does not know the field", so every such meaning becomes ambiguous the moment the schema grows. Each case below is cheap to fix now (a `oneof` arm or a required field) and costs a major bump after the baseline freezes. This does not reopen #47/#48/#313: it only asks that absence stop meaning something. ## Cases 1. `RetargetTarget.carrier` — unset means "default local daemon". The common case costs no bytes, but a future carrier added beside it cannot be told apart from an old peer that sent nothing. Add an explicit `Default` arm (or document unset as permanently the default and reserve against ever giving it meaning). 2. `OpsStop.mode` — unset means `if_empty`. Same shape as (1): an unset oneof silently selects the safe posture today and silently misreads tomorrow's posture. Make it required or explicit. 3. `SessionAttach.id` / `id_prefix` — sibling fields where exactly one is set, enforced by prose plus validator. A `oneof target` states the same constraint in the schema, so non-Rust peers read it from the descriptor instead of a comment. 4. Reset spellings differ per family: `GridThemeColor` uses absent-`Rgb` = reset while `GridPaletteColor` uses `oneof { set, reset }`. The latter fails a truncated write at decode instead of reading it as a reset. Unify on the `oneof` form. ## Acceptance criteria - [ ] Each of the four cases has an explicit decision (schema change or a recorded "unset stays X forever" rule) before the baseline in #115 lands. - [ ] `felis.proto` comments, `docs/reference/ipc.md` message-family tables, and `crates/felis-protocol/src/convert/` agree on the result. - [ ] `BREAKING.md` acknowledges the change against the merge-base per the pre-release rule. Parent: #326 (final interface simplification audit). Related: #313 (protocol 1.0 baseline), #115 (baseline workflow).
Author
Owner

Decision: do, P1 (reviewed)

All four cases verified against the tree. Policy applied uniformly: every mode-selecting oneof has named arms, and an unset union decodes as MissingOneof (malformed), so an unknown future arm on an old decoder fails closed instead of mapping to a default.

  1. RetargetTarget.carrier: add an explicit empty default_local arm.
  2. OpsStop.mode: add an explicit empty if_empty arm; unset becomes malformed, matching SwitchScope.
  3. SessionAttach: id = 1, live_only = 2, id_prefix = 3 are siblings today. Becomes oneof target { bytes id = 1; string id_prefix = 3; } with live_only = 2 untouched, so no active field moves or retypes.
  4. GridThemeColor: absent Rgb means reset, while GridPaletteColor's own comment rejects exactly that shape. Unify on oneof { set, reset }.

Lands after #330 and #328 in the proto batch (own PR, rebased on the previous merge, own base: line). Doc cascade: felis.proto comments, ipc.md message-family tables, convert/, BREAKING.md, CHANGELOG.md.

## Decision: do, P1 (reviewed) All four cases verified against the tree. Policy applied uniformly: every mode-selecting `oneof` has named arms, and an unset union decodes as `MissingOneof` (malformed), so an unknown future arm on an old decoder fails closed instead of mapping to a default. 1. `RetargetTarget.carrier`: add an explicit empty `default_local` arm. 2. `OpsStop.mode`: add an explicit empty `if_empty` arm; unset becomes malformed, matching `SwitchScope`. 3. `SessionAttach`: `id = 1`, `live_only = 2`, `id_prefix = 3` are siblings today. Becomes `oneof target { bytes id = 1; string id_prefix = 3; }` with `live_only = 2` untouched, so no active field moves or retypes. 4. `GridThemeColor`: absent `Rgb` means reset, while `GridPaletteColor`'s own comment rejects exactly that shape. Unify on `oneof { set, reset }`. Lands after #330 and #328 in the proto batch (own PR, rebased on the previous merge, own `base:` line). Doc cascade: `felis.proto` comments, `ipc.md` message-family tables, `convert/`, `BREAKING.md`, `CHANGELOG.md`.
Commenting is not possible because the repository is archived.
No description provided.