Encode keys where the keyboard modes are authoritative #323

Merged
natsukium merged 10 commits from daemon-key-encoding into main 2026-09-21 19:02:15 +09:00
Owner

Closes #309.

Stacks on #319 (connection-mode-minor, issue #312) and must merge after it. This branch is cut from
origin/connection-mode-minor, which holds PROTOCOL_MINOR 11; this one takes 12. The number is provisional: two
sibling branches (#307, #310) also take 12/13, and whoever merges later renumbers on rebase. It is a two-line change —
MINOR_STRUCTURED_KEY in crates/felis-protocol/src/preface.rs and its ledger row in docs/reference/ipc.md — plus
the since= on the arm's // arm: line, which just proto and the arm-table test keep honest.

The race

A keystroke's bytes depend on keyboard modes the daemon parses and the client only mirrors, through GridMsg::ModeFlags
and KittyKbdFlags. A producer can enable a mode and read input in the window between the daemon parsing it and that
grid frame reaching the client; an occluded, pull-paced window holds the window open indefinitely. Pushing mode frames
eagerly does not close it, because nothing orders the daemon's next frame against the user's next key.

Mouse input already avoids this by sending the event and letting the daemon encode it. Keyboard input now does the same.

Wire shape

InputMsg::Key(KeyEvent {
    key:      Key::Named(NamedKey) | Key::Character(String) | Key::Other,
    text:     Option<String>,   // the OS-composed text for this keystroke
    mods:     KeyMods,          // 4 bits: ctrl 1, shift 2, alt 4, super 8
    kind:     KeyEventKind,     // Press | Repeat | Release
    location: KeyLocation,      // Standard | Left | Right | Numpad
})

NamedKey, FKey, KeyLocation, Key and KeyMods move into felis-protocol as wire vocabulary;
felis-client-core's keymap re-exports them (KeyMods carries a CONTROL alias so the chord grammar keeps its
spelling). InputMods is untouched: the mouse arm still speaks xterm's shift/alt/ctrl triple, and KeyMods is wider by
SUPER alone.

On the proto side: InputKeyEvent at field 10 of the InputMsg oneof, with KeyId (a oneof over the NamedKey enum,
an F-key index, a character string, and KeyOther), optional string text (so an empty composition stays distinct from
an absent one), KeyEventKind and KeyLocation.

KeyBytes keeps its meaning for literal bytes: sessions send --raw, send_string, a committed IME string, a file
drop. Paste is unchanged.

Where the encoder lives

crates/felis-client-core/src/key_encode.rs → crates/felis-daemon/src/serve/key_encode.rs, with its 102 unit tests
and 11 proptests (the test module keeps a local press/release shim so the existing cases read unchanged). It is still
exactly one encoder — that is what the rewritten rejection record in control-surfaces.md now says, with the stale-mode
race as the reason instead of encoder duplication.

handle_admitted_input encodes under session.lock_core(), off grid.kitty_kbd_flags() + grid.mode_snapshot() —
the same lock InputMsg::Mouse reads — then writes through streaming::write_pty_reserved, carrying the permit. A
Key promotes input ownership only if it encodes to bytes, like Mouse.

Limits

Constant Value What
MAX_KEY_CHARACTER_BYTES 32 B Key::Character, refused at admission with InputOverLimit
MAX_KEY_TEXT_BYTES 32 B KeyEvent.text, same
MAX_KEY_REPORT_BYTES 280 B reserved by input_reservation_bytes for every Key

MAX_KEY_REPORT_BYTES = KEY_REPORT_FIXED_BYTES + 8 * MAX_KEY_TEXT_BYTES = 24 + 8 × 32. The widest form any mode
produces is Kitty CSI u with alternate keys and associated text. Fixed part: ESC [ (2), two keycodes of at most 7
decimal digits each (a Unicode scalar tops out at 1114111 and every Kitty functional code is smaller) plus their :
(15), ; plus a two-digit modifier field (3), : plus a one-digit event type (2), the ; opening the text field (1),
the final u (1) = 24. Each text codepoint costs at most 7 digits plus one separator, and a codepoint occupies at least
one UTF-8 byte, so the 32-byte text cap admits at most 32 of them. The derivation is written out in
docs/reference/protocols/key-encoding.md "Report size" and cited from the constant.

Repeat

New behavior, not a move. A held key reports Kitty event type 2 while bit 2 (event types) is active; without it, and
under every other encoding, a repeat encodes as a press. win32-input-mode reports both edges through Kd and keeps
Rc = 1, since each repeat arrives as its own event.

Mixed minor

A daemon below minor 12 cannot be typed at at all: the encoding reads modes only it owns, so there is no client-side
fallback left. check_key_input_minor refuses the landing in dial::land_on (every landing ends attached to a session
surface) rather than letting the send gate drop each key with a warning, and sessions send --key exits 2 under
protocol_mismatch. Both are documented in the ledger row and in cli.md.

Tests added

  • serve::session_task::tests::a_key_encodes_against_a_mode_the_subscriber_has_not_been_told_about — the required
    stale-mode race test. Advances ?1h straight into the grid so nothing is fanned out, sends a Key, and asserts the
    child received ESC O A; then drains the subscriber's queue and asserts the ModeFlags mirror was still sitting
    undelivered when the key was encoded.
  • serve::tests::a_key_parked_on_a_full_budget_holds_back_the_roster_query_behind_it — the saturated-budget barrier
    test. A budget-filling paste, then a Key, then Ops::List on the same connection: the roster must not be answered
    within a second, and must be answered once the child drains. This is the barrier felis sessions send relies on to
    know its input was not dropped.
  • serve::key_encode::tests::no_mode_encodes_a_capped_key_past_the_reserved_report_size — proptest over every named key
    and F-key, capped character and text, all 16 modifier sets, all three event kinds, all four locations, every Kitty
    flag subset, all three modifyOtherKeys levels, DECCKM, DECKPAM and win32.
  • serve::session_task::tests::a_repeat_reports_kitty_event_type_two_only_while_event_types_are_on
  • serve::tests::a_key_reserves_the_worst_case_report_and_refuses_an_oversized_one
  • convert::input::tests::{an_f_key_index_outside_the_supported_range_is_rejected, an_absent_or_unspecified_named_key_is_rejected, a_key_event_without_a_key_is_rejected}
  • messages::input::tests::key_mods_remap_every_bit_and_drop_super (moved from the keymap with to_input_mods)
  • input::tests::{a_held_key_reports_repeat_and_a_release_reports_release, a_key_event_carries_the_facts_the_daemon_encodes_from}
  • cli_sessions::tests::a_chord_becomes_the_key_event_a_keystroke_would, and the frame-inspection test is now
    key_enter_rides_its_own_structured_frame_after_the_payload
  • Fuzz seed fuzz/seeds/ipc_body/proto-input-key-kitty-text.bin

Gates

just proto, just proto-compat (wire-compatible), cargo nextest run --workspace --all-features,
cargo clippy --workspace --all-targets, just prose-check, just docs-links.

Two prose-check comment-length hits remain in the PR range and are not from this branch: felis.proto:162 and
connector.rs's check_mode_minor are both lines #319 adds. They clear when this rebases onto a merged #319.

Closes #309. **Stacks on #319 (`connection-mode-minor`, issue #312) and must merge after it.** This branch is cut from `origin/connection-mode-minor`, which holds `PROTOCOL_MINOR` 11; this one takes 12. The number is provisional: two sibling branches (#307, #310) also take 12/13, and whoever merges later renumbers on rebase. It is a two-line change — `MINOR_STRUCTURED_KEY` in `crates/felis-protocol/src/preface.rs` and its ledger row in `docs/reference/ipc.md` — plus the `since=` on the arm's `// arm:` line, which `just proto` and the arm-table test keep honest. ## The race A keystroke's bytes depend on keyboard modes the daemon parses and the client only mirrors, through `GridMsg::ModeFlags` and `KittyKbdFlags`. A producer can enable a mode and read input in the window between the daemon parsing it and that grid frame reaching the client; an occluded, pull-paced window holds the window open indefinitely. Pushing mode frames eagerly does not close it, because nothing orders the daemon's next frame against the user's next key. Mouse input already avoids this by sending the event and letting the daemon encode it. Keyboard input now does the same. ## Wire shape ``` InputMsg::Key(KeyEvent { key: Key::Named(NamedKey) | Key::Character(String) | Key::Other, text: Option<String>, // the OS-composed text for this keystroke mods: KeyMods, // 4 bits: ctrl 1, shift 2, alt 4, super 8 kind: KeyEventKind, // Press | Repeat | Release location: KeyLocation, // Standard | Left | Right | Numpad }) ``` `NamedKey`, `FKey`, `KeyLocation`, `Key` and `KeyMods` move into `felis-protocol` as wire vocabulary; `felis-client-core`'s keymap re-exports them (`KeyMods` carries a `CONTROL` alias so the chord grammar keeps its spelling). `InputMods` is untouched: the mouse arm still speaks xterm's shift/alt/ctrl triple, and `KeyMods` is wider by `SUPER` alone. On the proto side: `InputKeyEvent` at field 10 of the `InputMsg` oneof, with `KeyId` (a oneof over the `NamedKey` enum, an F-key index, a character string, and `KeyOther`), `optional string text` (so an empty composition stays distinct from an absent one), `KeyEventKind` and `KeyLocation`. `KeyBytes` keeps its meaning for literal bytes: `sessions send --raw`, `send_string`, a committed IME string, a file drop. `Paste` is unchanged. ## Where the encoder lives `crates/felis-client-core/src/key_encode.rs` → `crates/felis-daemon/src/serve/key_encode.rs`, with its 102 unit tests and 11 proptests (the test module keeps a local press/release shim so the existing cases read unchanged). It is still exactly one encoder — that is what the rewritten rejection record in `control-surfaces.md` now says, with the stale-mode race as the reason instead of encoder duplication. `handle_admitted_input` encodes under `session.lock_core()`, off `grid.kitty_kbd_flags()` + `grid.mode_snapshot()` — the same lock `InputMsg::Mouse` reads — then writes through `streaming::write_pty_reserved`, carrying the permit. A `Key` promotes input ownership only if it encodes to bytes, like `Mouse`. ## Limits | Constant | Value | What | | -------------------------- | ----- | ---------------------------------------------------------- | | `MAX_KEY_CHARACTER_BYTES` | 32 B | `Key::Character`, refused at admission with `InputOverLimit` | | `MAX_KEY_TEXT_BYTES` | 32 B | `KeyEvent.text`, same | | `MAX_KEY_REPORT_BYTES` | 280 B | reserved by `input_reservation_bytes` for every `Key` | `MAX_KEY_REPORT_BYTES = KEY_REPORT_FIXED_BYTES + 8 * MAX_KEY_TEXT_BYTES = 24 + 8 × 32`. The widest form any mode produces is Kitty CSI u with alternate keys and associated text. Fixed part: `ESC [` (2), two keycodes of at most 7 decimal digits each (a Unicode scalar tops out at 1114111 and every Kitty functional code is smaller) plus their `:` (15), `;` plus a two-digit modifier field (3), `:` plus a one-digit event type (2), the `;` opening the text field (1), the final `u` (1) = 24. Each text codepoint costs at most 7 digits plus one separator, and a codepoint occupies at least one UTF-8 byte, so the 32-byte text cap admits at most 32 of them. The derivation is written out in `docs/reference/protocols/key-encoding.md` "Report size" and cited from the constant. ## Repeat New behavior, not a move. A held key reports Kitty event type 2 while bit 2 (event types) is active; without it, and under every other encoding, a repeat encodes as a press. win32-input-mode reports both edges through `Kd` and keeps `Rc = 1`, since each repeat arrives as its own event. ## Mixed minor A daemon below minor 12 cannot be typed at at all: the encoding reads modes only it owns, so there is no client-side fallback left. `check_key_input_minor` refuses the landing in `dial::land_on` (every landing ends attached to a session surface) rather than letting the send gate drop each key with a warning, and `sessions send --key` exits `2` under `protocol_mismatch`. Both are documented in the ledger row and in `cli.md`. ## Tests added - `serve::session_task::tests::a_key_encodes_against_a_mode_the_subscriber_has_not_been_told_about` — the required stale-mode race test. Advances `?1h` straight into the grid so nothing is fanned out, sends a `Key`, and asserts the child received `ESC O A`; then drains the subscriber's queue and asserts the `ModeFlags` mirror was *still* sitting undelivered when the key was encoded. - `serve::tests::a_key_parked_on_a_full_budget_holds_back_the_roster_query_behind_it` — the saturated-budget barrier test. A budget-filling paste, then a `Key`, then `Ops::List` on the same connection: the roster must not be answered within a second, and must be answered once the child drains. This is the barrier `felis sessions send` relies on to know its input was not dropped. - `serve::key_encode::tests::no_mode_encodes_a_capped_key_past_the_reserved_report_size` — proptest over every named key and F-key, capped character and text, all 16 modifier sets, all three event kinds, all four locations, every Kitty flag subset, all three `modifyOtherKeys` levels, DECCKM, DECKPAM and win32. - `serve::session_task::tests::a_repeat_reports_kitty_event_type_two_only_while_event_types_are_on` - `serve::tests::a_key_reserves_the_worst_case_report_and_refuses_an_oversized_one` - `convert::input::tests::{an_f_key_index_outside_the_supported_range_is_rejected, an_absent_or_unspecified_named_key_is_rejected, a_key_event_without_a_key_is_rejected}` - `messages::input::tests::key_mods_remap_every_bit_and_drop_super` (moved from the keymap with `to_input_mods`) - `input::tests::{a_held_key_reports_repeat_and_a_release_reports_release, a_key_event_carries_the_facts_the_daemon_encodes_from}` - `cli_sessions::tests::a_chord_becomes_the_key_event_a_keystroke_would`, and the frame-inspection test is now `key_enter_rides_its_own_structured_frame_after_the_payload` - Fuzz seed `fuzz/seeds/ipc_body/proto-input-key-kitty-text.bin` ## Gates `just proto`, `just proto-compat` (wire-compatible), `cargo nextest run --workspace --all-features`, `cargo clippy --workspace --all-targets`, `just prose-check`, `just docs-links`. Two `prose-check` comment-length hits remain in the PR range and are **not from this branch**: `felis.proto:162` and `connector.rs`'s `check_mode_minor` are both lines #319 adds. They clear when this rebases onto a merged #319.
protocol: version connection modes through the minor ledger
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / windows docs-only no-op (pull_request) Has been cancelled
bench / Criterion regression gate (pull_request) Has been cancelled
darwin / detect relevant changes (pull_request) Has been cancelled
darwin / build felis (aarch64-darwin) (pull_request) Has been cancelled
darwin / darwin docs-only no-op (pull_request) Has been cancelled
fuzz / cargo fuzz smoke (per target) (pull_request) Has been cancelled
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
f5fe7e14e0
A mode is the extension path for a whole surface: the design says a new
surface earns a new mode rather than a per-feature bit, so mode growth
is an expected additive change, not a corner case. Until now the gate
covered `ConnWelcome.identity` alone, `ConnectionMode` carried no
`since_minor`, and a client naming a mode an older daemon predates lost
the connection at that daemon's decode with no sender-side verdict and
no frame back.

Every mode now carries a ledger row the schema walk enforces, the
`Hello` folds its mode's requirement into what the send gate reads, and
the client refuses the dial locally when the effective minor does not
define the mode it asks for. The receiving half is a typed refusal:
`RefusalReason::UnknownMode` (minor 11) names the skew before the close,
so the peer reports a daemon too old instead of an EOF it would retry.
Widening `Role` to cover it was rejected, since re-reading an existing
enum value is a semantic change the minor rule does not admit.

No mode down-maps: answering a peer with a mode it did not name would
wire up a surface it did not ask for, so the send gate and the refusal
are the whole accommodation.

Assisted-by: Claude Code
Keyboard bytes were encoded client-side from mode state the daemon
mirrors, so a producer that enables a keyboard mode and waits for input
can be answered with a key encoded under the mode the client last saw.
An occluded, pull-paced window makes that gap arbitrarily wide. Mouse
input already avoids it by sending the event and letting the daemon
encode; the keyboard now has the same vocabulary to do so.

The wire set is the one the existing encoder reads: logical key,
OS-composed text, a four-bit modifier set that adds Super to the xterm
triple the mouse arm speaks, press / repeat / release, and key location.
It lives in felis-protocol rather than felis-client-core because both
peers now name it.

Text and character are capped per keystroke so admission can reserve a
fixed worst case for the encoded report: a longer payload is a paste,
which has its own arm and its own cap.

Assisted-by: Claude Code
The encoder reads Kitty flags, DECCKM, DECKPAM, modifyOtherKeys and
win32-input-mode, all of which the daemon owns and the client only
mirrors. Running it beside the mode state removes the window in which a
client encodes under modes a grid frame has not delivered yet: the
snapshot comes off the same locked core `InputMsg::Mouse` reads.

The chord vocabulary follows the encoder's parameters into
felis-protocol, so felis-client-core re-exports it for the keymap rather
than defining a second copy.

Admission reserves the mode-independent worst case for a key, as it
already does for a mouse report, because the child can flip keyboard
modes between the permit and the write. The permit rides through the
encode into the reserved write, so the roster query `sessions send`
issues after its input still cannot be answered ahead of it.

Auto-repeat becomes its own event kind: the Kitty protocol reports it as
event type 2 while event types are on, and every other encoding has no
way to say it, so it encodes as a press there.

Assisted-by: Claude Code
The GUI and `sessions send --key` now both put the keystroke on the
wire and let the daemon encode it, so the two cannot drift on encoding
policy and neither depends on having received the session's current
keyboard modes. `sessions send --key` loses its drain of the rehydrate
burst's mode frames along with the guess it had to refuse on when they
were missing, and winit's auto-repeat flag reaches the wire, which no
byte form could carry.

A daemon below the minor that defines the arm cannot be typed at at all:
the encoding reads modes only it owns, so there is nothing to fall back
to. A window landing there is refused with the skew rather than losing
every key at the send gate, and `--key` against one exits 2 under
protocol_mismatch.

Composed text past the per-key cap takes the `KeyBytes` path instead of
being refused: a composition that long is an IME commit, which is what
that arm already carries.

Assisted-by: Claude Code
docs: record key encoding as the daemon's, and why
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Has been cancelled
darwin / detect relevant changes (pull_request) Has been cancelled
darwin / build felis (aarch64-darwin) (pull_request) Has been cancelled
darwin / darwin docs-only no-op (pull_request) Has been cancelled
fuzz / cargo fuzz smoke (per target) (pull_request) Has been cancelled
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
windows / detect relevant changes (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / windows docs-only no-op (pull_request) Has been cancelled
689f922b87
The standing rejection of a daemon Key message rested on keeping one
encoder; the encoder is still one, so the rejection is restated against
what actually decides it — a client cannot encode from modes it learns
about in frames that have not arrived. The revisit trigger moves with
it: a surface that must encode with no daemon in reach.

The ownership table gains the row, the input explanation gains the race
and the boundary it forces, and the reference pages gain the arm, its
two admission caps, the reservation the widest report needs, and the
repeat event a held key now reports.

Assisted-by: Claude Code
treewide: clear the clippy warnings the key arm added
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Has been cancelled
darwin / detect relevant changes (pull_request) Has been cancelled
darwin / build felis (aarch64-darwin) (pull_request) Has been cancelled
darwin / darwin docs-only no-op (pull_request) Has been cancelled
fuzz / cargo fuzz smoke (per target) (pull_request) Has been cancelled
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
windows / detect relevant changes (pull_request) Has been cancelled
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / windows docs-only no-op (pull_request) Has been cancelled
475c626576
`items_after_statements` and two `missing_const_for_fn` hits; the
workspace lints them at warn and CI compares counts against main.

Assisted-by: Claude Code
docs: correct three input facts the key arm left stale
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m38s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 52s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m23s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m11s
pr / wire schema is compatible with the base (pull_request) Successful in 14s
darwin / detect relevant changes (pull_request) Successful in 4s
darwin / darwin docs-only no-op (pull_request) Has been skipped
pr / nix flake check (pull_request) Successful in 40s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m28s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 4s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo nextest (Windows) (pull_request) Successful in 6m8s
windows / cargo clippy (Windows cross) (pull_request) Successful in 19s
windows / frontend smoke (Windows) (pull_request) Successful in 2m22s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
54ffb75cbf
The modifyOtherKeys section lumped repeats in with releases as sending
nothing, which contradicts both the Kitty section and the encoder: the
level has no event field, so a release has no form to take but a repeat
takes the press encoding.

A file drop is routed through the paste path so bracketed-paste guards
apply to the inserted path, so it belongs beside `Paste`, not among the
byte-for-byte `KeyBytes` callers.

`Input::Key` coalesces under no key and seals none, so the ordered half
of the send-queue rule has to name it.

Assisted-by: Claude Code
Merge origin/main into daemon-key-encoding
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m19s
darwin / detect relevant changes (pull_request) Successful in 4s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 52s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m20s
pr / nix flake check (pull_request) Successful in 42s
pr / wire schema is compatible with the base (pull_request) Successful in 9s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m10s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 4s
windows / windows docs-only no-op (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m1s
windows / cargo nextest (Windows) (pull_request) Successful in 8m3s
windows / cargo clippy (Windows cross) (pull_request) Successful in 13s
windows / frontend smoke (Windows) (pull_request) Successful in 2m28s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
60419785cb
This branch was stacked on connection-mode-minor, which main took in as a
squash (#319), so main is no longer a descendant of the branch point and
the two histories conflicted over the minor ledger, the proto header and
the Hello mode peek. Merging rather than rebasing keeps the branch's
published commits stable while PRs #321 and #322 are still in flight.

Resolutions:

- CHANGELOG keeps both entries, this branch's minor-12 entry first.
- PROTOCOL_MINOR stays at the provisional 12; main's merged 11
  (MINOR_UNKNOWN_MODE) keeps its ledger row, its Requires gate and its
  since_minor, and Input::Key remains the only row above it.
- serve.rs takes main's reviewed Hello mode peek and keeps this branch's
  Input::Key reservation arm.
- connector.rs keeps check_key_input_minor beside main's reworded
  check_mode_minor.
- Files this branch never touched (transport driver, architecture/ipc.md)
  are taken from main verbatim, and the generated proto is regenerated.

Assisted-by: Claude Code
Author
Owner

origin/main merged in at 60419785 (merge commit, no rebase, no force-push).

The stacking note at the top of the description is now stale: #319 landed as a squash (48fdbce8, PROTOCOL_MINOR 11), so main is no longer a descendant of this branch's base and the histories had to be merged rather than rebased.

Resolutions:

  • CHANGELOG.md keeps both entries, this branch's minor-12 entry first.
  • PROTOCOL_MINOR stays at the provisional 12. Main's merged 11 (MINOR_UNKNOWN_MODE) keeps its ledger row, its Requires gate and its since_minor, and Input::Key remains the only row above it. Renumbering to 14 after #321 and #322 land is still the same two-line change.
  • serve.rs takes main's reviewed Hello mode peek and keeps this branch's Input::Key reservation arm.
  • connector.rs keeps check_key_input_minor beside main's reworded check_mode_minor.
  • Files this branch never touched (the transport driver and docs/explanation/architecture/ipc.md) are taken from main verbatim, and the generated proto is regenerated.

Green after the merge: clippy clean across the workspace, 778 tests in felis-protocol + felis-daemon, 1112 in felis-client, felis-client-core, felis-cli and felis-transport, just proto-compat wire-compatible with the merge base, just docs-links and prose_check --range main...HEAD clean.

#317 landed on main after this merge. It touches only the transport driver, so it does not conflict and is not merged in here.

`origin/main` merged in at 60419785 (merge commit, no rebase, no force-push). The stacking note at the top of the description is now stale: #319 landed as a squash (48fdbce8, `PROTOCOL_MINOR` 11), so main is no longer a descendant of this branch's base and the histories had to be merged rather than rebased. Resolutions: - `CHANGELOG.md` keeps both entries, this branch's minor-12 entry first. - `PROTOCOL_MINOR` stays at the provisional 12. Main's merged 11 (`MINOR_UNKNOWN_MODE`) keeps its ledger row, its `Requires` gate and its `since_minor`, and `Input::Key` remains the only row above it. Renumbering to 14 after #321 and #322 land is still the same two-line change. - `serve.rs` takes main's reviewed Hello mode peek and keeps this branch's `Input::Key` reservation arm. - `connector.rs` keeps `check_key_input_minor` beside main's reworded `check_mode_minor`. - Files this branch never touched (the transport driver and `docs/explanation/architecture/ipc.md`) are taken from main verbatim, and the generated proto is regenerated. Green after the merge: clippy clean across the workspace, 778 tests in felis-protocol + felis-daemon, 1112 in felis-client, felis-client-core, felis-cli and felis-transport, `just proto-compat` wire-compatible with the merge base, `just docs-links` and `prose_check --range main...HEAD` clean. #317 landed on main after this merge. It touches only the transport driver, so it does not conflict and is not merged in here.
Merge origin/main and renumber the structured-key minor to 14
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 53s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m17s
windows / detect relevant changes (pull_request) Successful in 3s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m25s
darwin / detect relevant changes (pull_request) Successful in 7s
darwin / darwin docs-only no-op (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m15s
pr / nix flake check (pull_request) Successful in 39s
pr / wire schema is compatible with the base (pull_request) Successful in 10s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m14s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
877aae1e96
#321 and #322 took the provisional 12 and 13 this branch's number was
racing for, so `InputMsg::Key` now sits above them at 14. The number
lives in one constant and one ledger row by design, which is why the
renumber is mechanical: `MINOR_STRUCTURED_KEY`, its `MINOR_LEDGER` row
after main's 13, the ledger row in `docs/reference/ipc.md`, the proto
header comment, the `since=` on the `Input::Key` arm, the frozen preface
byte arrays, and the prose that names the minor.

Merging rather than rebasing keeps this branch's reviewed commits stable
while it is still open.

Conflict resolutions:

- CHANGELOG keeps every entry, this branch's (now minor 14) first.
- `cli_sessions.rs`: main folded the empty-payload short circuit and the
  attach into `attach_by_prefix`, so only the `--key` minor gate stays
  from this side.
- `limits.rs` imports both main's `AttachTarget` and this branch's `Key`.

Assisted-by: Claude Code
Author
Owner

origin/main (57d894ea) merged in at 877aae1e, and the provisional minor renumbered from 12 to 14.

#321 took 12 (GridMsg::CycleEnd) and #322 took 13 (attach by prefix), so InputMsg::Key sits above them. The renumber touched: MINOR_STRUCTURED_KEY, its MINOR_LEDGER row after main's 13, the ledger row in docs/reference/ipc.md, the PROTOCOL_MINOR header comment in felis.proto, the since= on the Input::Key arm line, both frozen preface byte arrays (0x0C to 0x0E), the Key(KeyEvent) and older-peer prose in docs/reference/ipc.md, docs/reference/cli.md, and the CHANGELOG heading. just proto regenerated.

Conflict resolutions:

  • CHANGELOG.md keeps every entry, this branch's (now minor 14) first.
  • crates/felis-cli/src/cli_sessions.rs: main folded the empty-payload short circuit and the attach into attach_by_prefix, so only the --key minor gate stays from this side.
  • crates/felis-protocol/src/messages/limits.rs imports both main's AttachTarget and this branch's Key.
  • preface.rs, felis.proto and the ipc.md ledger keep main's rows 12 and 13 and add this branch's row at 14.

Green: clippy clean across the workspace; 793 tests in felis-protocol + felis-daemon, 799 in felis-client-core + felis-cli; just proto-compat wire-compatible with the merge base; just docs-links 0 errors; prose_check --range origin/main...HEAD 0 hits. All pre-commit hooks ran (no --no-verify).

#318 is still open; expect one more small CHANGELOG re-merge once it lands.

`origin/main` (57d894ea) merged in at 877aae1e, and the provisional minor renumbered from 12 to **14**. #321 took 12 (`GridMsg::CycleEnd`) and #322 took 13 (attach by prefix), so `InputMsg::Key` sits above them. The renumber touched: `MINOR_STRUCTURED_KEY`, its `MINOR_LEDGER` row after main's 13, the ledger row in `docs/reference/ipc.md`, the `PROTOCOL_MINOR` header comment in `felis.proto`, the `since=` on the `Input::Key` arm line, both frozen preface byte arrays (`0x0C` to `0x0E`), the `Key(KeyEvent)` and older-peer prose in `docs/reference/ipc.md`, `docs/reference/cli.md`, and the CHANGELOG heading. `just proto` regenerated. Conflict resolutions: - `CHANGELOG.md` keeps every entry, this branch's (now minor 14) first. - `crates/felis-cli/src/cli_sessions.rs`: main folded the empty-payload short circuit and the attach into `attach_by_prefix`, so only the `--key` minor gate stays from this side. - `crates/felis-protocol/src/messages/limits.rs` imports both main's `AttachTarget` and this branch's `Key`. - `preface.rs`, `felis.proto` and the ipc.md ledger keep main's rows 12 and 13 and add this branch's row at 14. Green: clippy clean across the workspace; 793 tests in felis-protocol + felis-daemon, 799 in felis-client-core + felis-cli; `just proto-compat` wire-compatible with the merge base; `just docs-links` 0 errors; `prose_check --range origin/main...HEAD` 0 hits. All pre-commit hooks ran (no `--no-verify`). #318 is still open; expect one more small CHANGELOG re-merge once it lands.
Merge branch 'main' into daemon-key-encoding
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 4s
darwin / darwin docs-only no-op (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m25s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 52s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m18s
pr / nix flake check (pull_request) Successful in 52s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m57s
pr / wire schema is compatible with the base (pull_request) Successful in 9s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m8s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 3s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
windows / cargo nextest (Windows) (pull_request) Successful in 4m57s
windows / frontend smoke (Windows) (pull_request) Successful in 1m59s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
5c6fbf416e
Assisted-by: Claude Code
natsukium deleted branch daemon-key-encoding 2026-09-21 19:02:15 +09:00
Commenting is not possible because the repository is archived.
No description provided.