protocol, transport: split the two-way IPC families into a wrapper per direction #473
No reviewers
Labels
No labels
priority/P0
priority/P1
priority/P2
release/v0.1.0
status/blocked
status/planned
type/bug
type/design
type/test-gap
type/tracker
No milestone
No project
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
natsukium/felis!473
Loading…
Reference in a new issue
No description provided.
Delete branch "cleanup/wire-split"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Six IPC families (Conn, Session, Ops, Region, Notify, Search) carried both directions' arms in one oneof, told apart only by the per-arm
directionoption. The transport driver refused a wrong-direction arm at runtime, but each side still held the two-way enum, so the daemon and client matches neededunreachable!or catch-all arms for arms that can never reach them. Before v0.1.0 there is no compatibility baseline, so this is the cheapest point to make those states unrepresentable.What
felis.proto: each two-way family is split into<Family>ToDaemonMsg/<Family>ToClientMsgunder its unchanged frame kind. Every arm keeps its field number, so wire bytes are identical:buf breaking(WIRE_JSON) passes, no golden/seed/--format jsonchange, no protocol version bump. One-way families keep<Family>Msg.ConnectionDriver<R: Role>(ClientDriver/DaemonDriver);decode::<M>const-assertsMtravels towardR.codec::arm_in, not run for one-way families) and refuses any body naming an arm of the other direction, in either order. This is slightly stricter than the old last-arm-wins behavior.decode_either(felis-json) returnsMixedDirections.Connection), the parked-frame check, the daemon's unknown-mode Hello fallback, and the liveness probe behindfelis doctor.unreachable!, the Notify catch-all, the client Search catch-all, a dead CLIDrained::Item(Query)arm.docs/reference/ipc.md(wrapper naming, shared arm numbering, the mixed-body rule),docs/explanation/architecture/ipc.md(decision record with the rejected one-wrapper-per-family shape and a Revisit if), glossary and related explanation pages;extend-ipcskill; CHANGELOG.Verification
cargo nextest run --workspace --all-features, workspace clippy-D warnings, Windows cross clippy,just proto-compat,just schema/just goldenclean,just prose-check, fuzzcargo check --bins(ipc_bodynow also drivesarm_in).Three paths read a Conn body without the driver's direction check. The client's correlated-request loop peeks each frame to see whether it answers the awaited request. A Conn body that did not decode as ConnToClientMsg (after the split, any daemon-bound arm) returned the codec error straight out of the loop. The frame was lost, the error was not a direction fault, and the connection stayed usable. Before the split the combined ConnMsg decoded such an arm, parked it, and the driver refused it on the next read. So the loss is new. The still-usable connection after a protocol error, and an undecodable Conn body being dropped the same way, were already on main. A peek failure now goes to the driver's stateless verdict (admit_parked), which names the fault. A Driver or Codec error from a correlated request retires the connection, as an inadmissible refusal already did. admit_parked used to let every Conn frame through on the assumption that control frames are never parked, but this loop parks them. A mixed Welcome+Cancel body therefore waited in the park queue while the verb completed, which was also true of main's order-dependent decode. It now decodes the Conn body as the side's control wrapper, refuses any other-direction arm, and applies the envelope, arm and class checks like any other family, so the fault ends the verb that read it. The daemon's unknown-mode fallback re-decoded the first frame as ConnToDaemonMsg to answer a newer peer's Hello by name. A Hello with an unknown mode beside a client-bound arm was therefore answered with Refused { UnknownMode } instead of being closed unanswered as corruption. On main this depended on arm order: a Welcome last won the oneof and closed, a Welcome first did not. It now declines the fallback whenever the body carries a client-bound arm. The transport's liveness probe (felis doctor) decodes the first reply itself, so a Welcome carrying a daemon-bound arm reported the peer as live. It now reports it as connected but unverified. The other direct decodes of a split wrapper are left alone: - felis-grid's json_v1 already goes through decode_either, which refuses mixed bodies. - The bridge's body_to_json renders a body the driver has already admitted. - The remaining hits are tests. Assisted-by: Claude Code