[v0.1/Input Review] Move mode-dependent key encoding to daemon authority #309
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#309
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?
Parent: #12
Problem
Keyboard bytes are encoded in the client from mode state mirrored by the daemon:
felis-client/src/event_handler.rscallsinput::encodewith the shadow's Kitty flags, DECCKM, DECKPAM, modifyOtherKeys, and win32-input-mode.InputMsg::KeyBytesthen carries only the resulting PTY bytes.GridMsg::ModeFlagsandKittyKbdFlagsare grid updates, and on a pull-paced window those updates may not have reached the client when the next physical key is pressed.This creates a state-authority race. A producer can enable a keyboard mode and wait for input after the daemon has parsed the mode change but before the client has received the corresponding grid frame. The client then encodes the key under stale state. An occluded window that has stopped pulling makes the gap especially clear.
Mouse input already uses the safer boundary: the client sends a structured
MouseEvent, and the daemon encodes it against the authoritative mouse mode. Keyboard input is the exception even though its encoding depends on daemon-owned terminal state in the same way.Decision to make before the v0.1 contract freezes
Prefer a structured keyboard input arm carrying the platform-independent facts needed by the existing encoder, for example:
The daemon should encode that event against its current grid mode snapshot before writing to the PTY. The client still owns OS event capture, IME, and keymap dispatch; only terminal-state-dependent byte encoding moves to the side that owns that state.
Literal byte injection remains necessary for
sessions send --raw,send_string, and other explicitly byte-oriented paths.Pasteand committed IME text also remain distinct from a physical key event.If structured input is rejected, record how the existing byte form prevents or accepts the stale-mode race. Merely pushing mode frames eagerly does not establish a cross-direction ordering barrier before a user key.
Acceptance criteria
sessions send --keyuse one encoding policy and cannot drift.--raw, paste, IME commit, andsend_stringretain their documented byte semantics.skills/felis, andCHANGELOG.mdagree.Principle check
Evidence
crates/felis-client/src/event_handler.rs:301crates/felis-client/src/input.rscrates/felis-client-core/src/key_encode.rscrates/felis-protocol/proto/felis.proto:727crates/felis-daemon/src/serve/session_task.rs(InputMsg::MousevsInputMsg::KeyBytes)docs/reference/ipc.md"Input (kind = 1)"docs/explanation/input.md"Keyboard"docs/explanation/architecture/control-surfaces.md"send --key"Direction agreed 2026-09-21 after three
soldesign-review rounds (final verdict PASS). Full plan and batch order: see the comment on #12.InputMsg::Key(named key or character, text,a 4-bit modifier set including Super, press/repeat/release, location).
Wire vocabulary (
NamedKey,KeyLocation, key modifiers) moves tofelis-protocol; the encoder (key_encode.rswith its 102 unit tests and11 proptests) moves to
felis-daemon, its only remaining runtime consumer,and encodes against
Grid::mode_snapshot()+kitty_kbd_flags()under thesame lock
InputMsg::Mouseuses. Input admission: the wire capsKey.characteratMAX_KEY_CHARACTER_BYTESandKey.textatMAX_KEY_TEXT_BYTES(both infelis_protocol::limits, UTF-8 bytes, sized for one composed key, which isa few codepoints at most; longer text is
Paste/KeyBytesterritory). Aframe over either cap is refused at admission with
InputOverLimitbeforeit reaches the session channel, like an oversized
Paste.input_reservation_bytesreservesMAX_KEY_REPORT_BYTES, a constantderived from those caps as the mode-independent worst case (Kitty CSI u with
alternate keys and associated text as decimal codepoints is the widest
form: per codepoint at most 7 digits plus a separator, plus the fixed
header, modifiers and event fields). A proptest over every mode combination
(Kitty flag subsets × modifyOtherKeys levels × DECCKM × DECKPAM × win32),
event kinds and locations, and capped inputs asserts
encode(...).len() <= MAX_KEY_REPORT_BYTES. The permit is carried throughencode and
write_pty_reserved, so the roster-query barriersessions sendrelies on still covers structured keys; a saturated-budget test proves
Keythen
Ops::Listcannot acknowledge dropped input.KeyBytesstays for--raw,paste, IME commit (already raw today),
send_string. Keymap dispatch isunaffected: it resolves on the logical chord before any encoding.
sessions send --keydrops its rehydrate-drain of mode frames and sendsKey. Repeatis new behavior (Kitty event type 2), not a move. The standing rejection in
docs/explanation/architecture/control-surfaces.md("no daemon Key message")is rewritten in place with the stale-mode race as the reason;
input.mdandkey-encoding.mdfollow.