Send added fields to every peer instead of gating them #127

Merged
natsukium merged 1 commit from protocol/ungated-fields into main 2026-10-12 21:42:49 +09:00
Owner

Split out of #67. Until now, a field added in a later protocol minor had to be stripped at every send site an older peer could be on. This PR sends added fields to every peer by default and keeps the send gate only for a field an older peer must not skip. The OSC 7501 SessionInfo.program_status field (PR 4) then needs one ADDED_FIELDS row instead of three downgrades plus three send sites.

Why

An older decoder skips an unknown field number, so a field sent to a peer that predates it lands that peer exactly where leaving the field out would. For a field that only adds detail, the downgrade buys nothing.

It is not safe for every field, and three cases stay gated:

  • A field on a family wrapper. A wrapper's top-level numbers are its arms, and the driver reads any of them as one. A plain field there would reach an older peer as the other direction's arm.
  • A new member of a payload message's oneof. An older peer skips it, reads the oneof as unset, and rejects the message.
  • A field whose skipping has no safe outcome. The case today is SearchQuery.source: an older daemon would skip it and answer a scrollback search, so the client keeps refusing locally.

What changes

  • minor.rs:

    • ADDED_FIELDS lists the ungated fields for the ledger sync test. Today that is ImageVirtualPlacement.placement_id and ImagePlacement.requested.
    • GatedField now holds only SearchQuery.source.
    • ImageMsg::for_minor and its daemon fan-out call are gone, and streaming::body_for no longer takes a minor.
  • Schema tests, which parse felis.proto into statements rather than matching lines:

    • Every wrapper carries only its arms, reserved, and the correlation envelope.
    • The members of all 22 payload oneofs are pinned, so the first oneof to grow has to bring a send gate.
  • Receive-side test: a stand-in for the minor-0 schema reads a virtual placement carrying placement_id the same as one without it. The current decoder accepts the body an older sender writes.

  • Docs:

    • Reference ipc.md: "Versioning" and the minor-1 ledger row.
    • Explanation architecture/ipc.md: "The ledger is the review gate".
    • testing.md.
    • The extend-ipc skill.

    A field is ungated only when its ledger row can state a safe outcome for an older peer that skips it, in either direction.

Verification

  • just check is green.
  • Mutation checks: adding reserved 99; uint32 extra = 5; to a wrapper, or a new member to ImageHeader.target, fails the build.
Split out of #67. Until now, a field added in a later protocol minor had to be stripped at every send site an older peer could be on. This PR sends added fields to every peer by default and keeps the send gate only for a field an older peer must not skip. The OSC 7501 `SessionInfo.program_status` field (PR 4) then needs one `ADDED_FIELDS` row instead of three downgrades plus three send sites. ## Why An older decoder skips an unknown field number, so a field sent to a peer that predates it lands that peer exactly where leaving the field out would. For a field that only adds detail, the downgrade buys nothing. It is not safe for every field, and three cases stay gated: - **A field on a family wrapper.** A wrapper's top-level numbers are its arms, and the driver reads any of them as one. A plain field there would reach an older peer as the other direction's arm. - **A new member of a payload message's `oneof`.** An older peer skips it, reads the oneof as unset, and rejects the message. - **A field whose skipping has no safe outcome.** The case today is `SearchQuery.source`: an older daemon would skip it and answer a scrollback search, so the client keeps refusing locally. ## What changes - **`minor.rs`:** - `ADDED_FIELDS` lists the ungated fields for the ledger sync test. Today that is `ImageVirtualPlacement.placement_id` and `ImagePlacement.requested`. - `GatedField` now holds only `SearchQuery.source`. - `ImageMsg::for_minor` and its daemon fan-out call are gone, and `streaming::body_for` no longer takes a minor. - **Schema tests**, which parse `felis.proto` into statements rather than matching lines: - Every wrapper carries only its arms, `reserved`, and the correlation envelope. - The members of all 22 payload oneofs are pinned, so the first oneof to grow has to bring a send gate. - **Receive-side test:** a stand-in for the minor-0 schema reads a virtual placement carrying `placement_id` the same as one without it. The current decoder accepts the body an older sender writes. - **Docs:** - Reference `ipc.md`: "Versioning" and the minor-1 ledger row. - Explanation `architecture/ipc.md`: "The ledger is the review gate". - `testing.md`. - The `extend-ipc` skill. A field is ungated only when its ledger row can state a safe outcome for an older peer that skips it, in either direction. ## Verification - `just check` is green. - Mutation checks: adding `reserved 99; uint32 extra = 5;` to a wrapper, or a new member to `ImageHeader.target`, fails the build.
protocol: send added fields to every peer instead of gating them
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 10s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 44s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m12s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 45s
pr / nix flake check (pull_request) Successful in 41s
windows / cargo nextest (Windows) (pull_request) Successful in 3m43s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m57s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 28s
windows / frontend smoke (Windows) (pull_request) Successful in 53s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
92d6cb6eb1
The send gate held added fields to the same rule as arms: a value
carrying one could not reach a peer whose minor predates it, so every
field addition needed a downgrade at each send site an older peer
could be on (`ImageMsg::for_minor` in the daemon's fan-out today, and
three message types plus three send sites for the next field on
SessionInfo). For a field that downgrade buys nothing: an older decoder
skips an unknown field number, so the peer ends up exactly where it
would had the sender left the field out, and the ledger's older-peer
cell reads the same either way.

That equivalence fails in two places, and a schema test now closes
each. A wrapper's top-level numbers are its arm space, and the driver
reads any of them as an arm, so a plain field added to a wrapper would
reach an older peer as the other direction's arm: every wrapper is held
to its arms and the correlation envelope. A member added to a payload
message's oneof is skipped too, leaving the oneof unset, which the
older decoder rejects as malformed: the members of every payload oneof
are pinned, so the first to grow has to bring a send gate. Both tests
parse the schema into statements rather than matching line prefixes,
so a declaration cannot hide behind another on the same line.

Arms, closed-enum values, connection modes and row-codec versions stay
gated: none of them is skipped harmlessly. Neither is a field whose
skipping changes what the message asks for: `SearchQuery.source` keeps
its `GatedField` row, because an older daemon would skip it and answer
a scrollback search, so the client refuses locally. Every other field
stays in the ledger, declared in `ADDED_FIELDS` for the sync test
alone; `ImagePlacement.requested` loses its downgrade with
`placement_id`. The receive half is
pinned instead of the send half: a stand-in for the minor-0 schema
reads a virtual placement carrying `placement_id` as it reads one
without, and the current decoder accepts the body an older sender
writes.

Assisted-by: Claude Code:Opus 5.5
natsukium deleted branch protocol/ungated-fields 2026-10-12 21:42:49 +09:00
Sign in to join this conversation.
No reviewers
No milestone
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
felis-terminal/felis!127
No description provided.