protocol, cli: pre-release review of frozen surfaces — six cheap-before-freeze gaps #347
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#347
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Context
A pre-release review of the hard-to-change surfaces (protobuf schema, IPC versioning, CLI contract) found the design sound; the findings below are all "free to decide now, costs a major after the freeze". Nothing blocks the release.
Findings
1. The negotiated effective minor is unobservable anywhere
ConnWelcomeechoes nothing, andOpsStatusReplydeliberately carries no version (the handshake is the one authority, #343). But the effective minor —min(client, daemon)— is computed on both ends and published nowhere: not inWelcome, not infelis daemon status(which prints the daemon's own minor, not any connection's), not in any machine output.Consequences after the minor ledger starts growing:
Proposal: add one informational field to
ConnWelcome(e.g.effective_minor, or echoclient_minor). It is display/diagnostic only; the preface stays the sole version authority — same reasoning #343 used to remove the duplicated copy fromOpsStatusReply, applied to an observation point rather than a second copy. Adding it at minor 0 is wire-free.2. Pin the
_UNSPECIFIEDmode →Refused { UnknownMode }promise with a testThe contract says a
ConnHellonaming a mode the daemon does not define is answeredRefused { REFUSAL_REASON_UNKNOWN_MODE }and closed — that is the documented remedy for a newer client against an older daemon. ButConnectionModeis a proto enum: a future mode value decodes as_UNSPECIFIEDon an older decoder, and whether the daemon then takes the typed-refusal path or the corruption (connection-kill) path is currently unpinned. The implementation is probably right; the promise is not.The path the first post-release mode addition lands on must not be undefined at freeze time. Add a test asserting: a
ConnHellowith modeCONNECTION_MODE_UNSPECIFIEDproduces exactlyConnRefused { reason: UNKNOWN_MODE }, never a decode-fatal teardown. This is a test-gap, not a code change, unless the test finds the decode refuses_UNSPECIFIEDbefore the handler can refuse it.3. Restate the
ConnEnd.countitem/trailer criterion at its definitioncountis defined as "the results that preceded it, never the acks, markers, and family trailers riding the same stream", and two precedents exist (RegionRowsDone= trailer, out;NotifySubscribed= ack, out). The criterion ("would a family-agnostic terminal carry this?") lives only in theRegionRowsdiscussion. Restate it in one sentence at theConnEndbullet so whoever writes the next minor-ledger row does not have to re-derive it. Doc-only; run the doc-cascade sweep.4. Promise that
error.kindtokens never narrow an existing token's meaningThe CLI
v:1epoch freezes the kind vocabulary's spellings and the kind→exit-code mapping, and the schema keepserror.kindopen withx-known-values. What is not stated: a new token must not narrow (respell the semantics of) an existing one, the wayfelis-jsonv1 states its additive-only rule. One line in cli.md "Machine output" (frozen-for-v:1paragraph) closes it: new tokens land within the epoch, existing ones are never respelled, remapped, or narrowed.5. Define
sessions send --timeout 0--timeout <secs>requires--wait, and an unbounded wait is the bare--waitspelling — a sound design. But the meaning of--timeout 0(immediate timeout? ignored? usage error?) is not pinned anywhere the contract freezes. After the release it cannot be redefined. Check the bridge/schema bound (minimum), pick a meaning, and state it in cli.md'ssendbullet plus the JSON schema annotation.6. Reference-side reason for the relay block's
version 0 = malformedruleipc.md states a payload format version of
0closes the connection while unknown nonzero versions take the fallback — correct, and deliberately asymmetric. The why (the version word is how a pre-release daemon without the word is told apart from one with it) sits in the explanation doc only. One sentence in the reference section (why this one value is malformed, unlike every other unknown version) so a reference-only reader implementing another relay does not have to find the explanation. Doc-only.Non-findings (checked, sound)
Field-numbering and reclaim policy, the
BREAKING.mdbase-ack mechanism, arm-as-routing-unit and the descriptor-carried arm table, the field-100 correlation slot,u128id encoding, zigzag signed ints, the row codec's opaque-bytes/version-byte split, the three-layer versioning (wire minor / CLIv:1/felis-jsonv1), the limit tiering table, and thenotify_id/notification_idspelling split.Two more cheap-before-freeze gaps from the same review pass, both about the schema's claim to be the single cross-language authority. Neither blocks the release.
A. Field-level minor metadata covers only six messages
crates/felis-protocol/src/minor.rsdeclares field requirements in hand-written tables (CONN_FIELDS,SESSION_FIELDS,OPS_FIELDS,PUSH_FIELDS,SESSION_INFO_FIELDS,SPAWN_ARGS_FIELDS), andgated_by_arm!coversInput/Image/Notify/Region/Searchon the assumption that "no post-baseline field or enum value rides inside one". The ledger-coverage test builds itsnamedset from those tables plus arms, closed-enum values, and row-codec versions.Consequence: a field added after the freeze to
SearchQuery,ImageNew,RegionRequest,NotifyEvent,InputResize, and so on has no place to declare its minor.requires()does not rise, andthe_ledger_covers_every_executable_additionstill passes. Protobuf's unknown-field tolerance then makes an older peer ignore the field silently, so the request loses the semantics it asked for instead of being refused by the send gate — the failure the gate exists to prevent for the six gated messages.Proposal: give fields a declared minor in the schema itself (a
(felis.v1.since)field option, or extend theArmRoutingpattern), and have the ledger test enumerate every field from the descriptor, requiring a minor and a ledger row for each post-baseline one. TheGatedFieldtables then become derived rather than hand-maintained.B. Bitmap layouts are not in the schema
felis.protosays bitflags carry "raw bits; see messages.rs" forInputMouseEvent.mods(line 990),InputKeyEvent.mods(line 1010), and the type note at lines 129-130. The layouts live only incrates/felis-protocol/src/messages/input.rs:InputModsis the xterm-mouse SHIFT=1 / ALT=2 / CTRL=4, andKeyModsis CTRL=1 / SHIFT=2 / ALT=4 / SUPER=8, which is neither the xterm mouse order nor the Kitty keyboard order (shift=1 / alt=2 / ctrl=4 / super=8). A non-Rust peer reading only the descriptor cannot encode or decode these, anddocs/reference/ipc.md"Application messages" promises it needs no other manifest.GridClipboardSet.selectionstates its bits inline ("c=1, p=2"), so the pattern exists.Proposal: carry the bit tables as a field option read from the descriptor (the
(felis.v1.arm)shape), or at minimum make the proto comments normative and name the standard each set follows.GridKittyKbdFlags.flagsdeserves the same treatment.C. Small shape inconsistencies (low priority)
SearchMatch.line_index/ColSpan.line_indexaresint64,RegionRow.rowissint32, andRowEntry.rowisuint32.ResolvedId.ambiguousis a bareuint32arm whileOpsInfoReply.ambiguousis anOpsInfoAmbiguous { matches }message;SpawnRefusedandSessionAttachFailedplacedetaildifferently.felis-grid/src/sink.rs), which noGridMsgarm carries, so the client never applies it. The support matrix's check mark may overstate what the sequence does.Correction to section B above, after checking the reference more carefully:
KeyModsbit layout is in the reference prose (docs/reference/ipc.mdline 1187: ctrl1, shift2, alt4, super8), matchingmessages.rs. So "the layouts live only in messages.rs" was wrong forKeyMods.InputModslayout is documented nowhere: theMouse(MouseEvent)bullet names themodsfield but never its bits, and theKeybullet only says it is "wider than theInputModsthe mouse arm carries".docs/reference/workspace.mdline 129 claims "the keyboard and capability bit tables stay in [ipc.md]", which is not true for the mouse triple.mods. I am adding the mouse triple to theMousebullet in a doc PR; the descriptor question stands as proposed.Triage (2026-09-22)
Scope narrowed to what is cheaper before the freeze than after it: items 2 (
_UNSPECIFIEDmode →Refused { UnknownMode }test), 3, 4, 6 (one reference sentence each), and 5 (sessions send --timeout 0defined in cli.md and the bridge schema). One PR.Item 1 (
effective_minoronWelcome) and the follow-up findings A and B (field-levelsincemetadata, bit tables in the descriptor) are additive minors and moved to #351. Finding C's OSC 1 support-matrix note is a doc-accuracy defect of the same kind as #349 and is folded in there; the remaining shape notes are recorded on #351.natsukium referenced this issue2026-09-22 21:18:49 +09:00