Follow felis to 105b0899 #2

Merged
natsukium merged 3 commits from follow-felis-105b0899 into main 2026-09-07 21:31:21 +09:00
Owner

Moves the four pinned felis crates from 32775067 (2026-07-16) to
105b089979369fd310757375af3df7900ae7e523, across the wire-baseline reset, and adapts the
relay to what that wire is now.

What felis changed, and what it forced here

felis change adaptation
The capability bitmap and the encoding negotiation are gone. Hello states a ConnectionMode plus one pull_paced bool; additions ride the protocol minor (now 10). CLIENT_CAPS/cap_names are replaced by stream_offer() -> Offer::window(false). The attach header reports minor (the connection's effective min(gateway, daemon)) where it reported caps.
The carrier is erased at connect time: connect_carrier(Carrier::Local | Carrier::Ssh, offer, RemoteSpawn) returns one CarrierConnection. The generic DaemonConn two-arm split is gone; dial() builds a felis Carrier and hands back one type. Remote dials pass RemoteSpawn::Allow — a browser opening a cold host expects its daemon to come up.
A create states geometry by presence: SpawnArgs.dims is optional and a present dims naming rows: 0 is refused with GEOMETRY_OUT_OF_RANGE. The legacy path sends dims only when both axes are non-zero; the broker's rows/cols fields read a 0 as absence (dim_field).
Session::Attached / Created carry the whole SessionInfo, not id + dims. Session::Attach gained live_only; AttachFailed's reason is typed. conn.attach(id, AttachIntent::Deliberate) and create_with return the roster row. Deliberate on purpose: the operator or the browser named this session, so a shell that already exited still hands back its final screen.
SessionInfo gained exited, last_exit_code, attachments, sequence, and idle_seconds is now absent while a session is attached. list rows carry exited and last_exit_code so a tab picker can tell a finished shell from a working one.
An image transfer is canonical: ImageMsg::Header { id, target: New{…} | Frame{number} }, no total_bytes, no chunk offset, frames 1-based. Nothing to route — the gateway forwards Image frames verbatim — but the JSON the browser decodes changed shape (below).
Every correlated arm needs an envelope, and a frame without one ends the connection. The notify observer now allocates a stream id (driver.open_stream()), moves the driver to Observing, and writes the subscribe with Correlation::stream(id); events are read back through driver.classify/decode.
Ops verbs no longer close the connection after replying. Behavior unchanged (still a dial per request) but the rationale in broker.rs and docs/broker.md said the daemon closes it, which is now false.

The observer is the one behavior fix rather than a rename. The old spelling
(writer.send(&NotifyMsg::Subscribe { … })) still compiles against the new crates and the
daemon answers it by ending the connection — badges would have died silently on every host
under the redial loop. Reported upstream as natsukium/felis#196.

Broker-contract facts for felis-web-component

The JSON is still the domain-serde view (felis-grid's transcode::body_to_json), not
pbjson, and the row payload's {"Rle": {graphemes, attr_runs, sized_cells, soft_wrap_continued}} view is byte-compatible. What moved:

  • Attach header (legacy first message, and the broker's attach/create ok):
    {"rows","cols","minor"}caps is gone, minor is min(gateway, daemon) and is
    per-stream.
  • GridMsg: GridSizeSize; RowDelta{row,packed_cells} and RowDeltaBatch{rows}
    collapse into one RowDelta{rows:[[u16,row]…]}; RehydrateBegin{dims} → the unit variant
    "RehydrateBegin"; Bell/NotifyAttention{source:"Bell"|"Notification"};
    ClipboardSet{selection,data}ClipboardSet{write:{selection,data}}; ModeFlags
    gains reverse_video and its mouse_protocol / modify_other_keys are now strings
    ("Off"|"ButtonEvents"|"ButtonAndDrag"|"AnyMotion", "Off"|"Level1"|"Level2");
    KittyKbdFlags.flags is a typed flag set; new PaletteColor{index,action} and
    PaletteResetAll. SearchMatch, SearchDone, ScrollbackRow, ScrollbackDone and
    CaptureRegion are gone from the family.
  • ImageMsg: Header{id,target:{"New":{width,height,format}}|{"Frame":{number}}};
    Chunk{id,bytes} (no offset); ShowFrame{id,number} (1-based, was index); the
    FrameHeader/FrameChunk/FrameComplete trio is gone.
  • SessionMsg (the browser's "Control" bucket): Attached{info} / Created{info}
    where it was {id,dims}; AttachFailed{reason,detail} with a typed reason.
  • ConnMsg (also "Control"): Welcome{build_id,identity?}; Hello{mode,pull_paced};
    new Refused/Cancel/End/Error; Ping/Pong gone.
  • InputMsg (browser → gateway): Resize{dims:{rows,cols,pixel_w,pixel_h}} where it was
    four loose fields; NextGridFrame is payload-free; Search, SearchCancel,
    CaptureScrollback and CaptureRegion no longer exist.
  • list rows: {host,session,rows,cols,title,cwd,idle_seconds,tags,last_notification, foreground,exited,last_exit_code}idle_seconds is null while attached, exited
    marks the post-exit grace.
  • {"ev":"notify"} is unchanged, fields and all.

Verified

All under nix develop, on this host:

  • cargo fmt --all --check, cargo clippy --all-targets -- -D warnings, cargo test
    (26 passed), cargo deny check (advisories/bans/licenses/sources ok), nix build .#gateway.
  • Live, against a throwaway felis-daemon built from 105b0899 on a scratch socket,
    driven by a hand-rolled WebSocket client:
    • legacy contract: 101, first message {"rows":24,"cols":80,"minor":10}, Grid frames
      following;
    • broker contract: felis-broker.v1 echoed, hosts, create ({"sid":1,"host":"local", "session":"8ffb…","rows":30,"cols":100,"minor":10}), keystrokes reaching the PTY and
      producing frames, list across the roster showing idle_seconds: null for the attached
      session and 6 for the detached one, tag{"tags":["probe"]}, destroy
      {"existed":true};
    • the notify observer: an OSC 9 in a session arriving as
      {"ev":"notify","host":"local","session":"ef77…","notification":{"body": "probe-notification","urgency":"normal"},…}. Against the pre-fix binary the same run
      gets Driver(Correlation { kind: Notify, expected: "a stream_id allocating the next unopened stream", found: "no correlation envelope on Notify::Subscribe" }) in the daemon
      log and no events at all.
    • The daemon and gateway were both started by this run on a private socket and stopped
      afterwards.

Left open

  • Search and scrollback export do not reach the browser — they left the Input family
    for correlated families whose ids come from the driver the relay drops when it splits a
    connection into a reader task and a writer task. Filed as #1 with the three candidate
    shapes; the docs now record it as out of scope rather than implied.
  • NotifyMsg::Lagged (the daemon dropped events an observer was too slow to read) is read
    and discarded rather than surfaced — surfacing it would add an event to the browser
    contract, which belongs with whoever consumes it.
Moves the four pinned felis crates from `32775067` (2026-07-16) to `105b089979369fd310757375af3df7900ae7e523`, across the wire-baseline reset, and adapts the relay to what that wire is now. ## What felis changed, and what it forced here | felis change | adaptation | |---|---| | The capability bitmap and the encoding negotiation are gone. `Hello` states a `ConnectionMode` plus one `pull_paced` bool; additions ride the protocol minor (now **10**). | `CLIENT_CAPS`/`cap_names` are replaced by `stream_offer() -> Offer::window(false)`. The attach header reports `minor` (the connection's effective `min(gateway, daemon)`) where it reported `caps`. | | The carrier is erased at connect time: `connect_carrier(Carrier::Local \| Carrier::Ssh, offer, RemoteSpawn)` returns one `CarrierConnection`. | The generic `DaemonConn` two-arm split is gone; `dial()` builds a felis `Carrier` and hands back one type. Remote dials pass `RemoteSpawn::Allow` — a browser opening a cold host expects its daemon to come up. | | A create states geometry by **presence**: `SpawnArgs.dims` is optional and a present `dims` naming `rows: 0` is refused with `GEOMETRY_OUT_OF_RANGE`. | The legacy path sends `dims` only when both axes are non-zero; the broker's `rows`/`cols` fields read a `0` as absence (`dim_field`). | | `Session::Attached` / `Created` carry the whole `SessionInfo`, not `id` + `dims`. `Session::Attach` gained `live_only`; `AttachFailed`'s reason is typed. | `conn.attach(id, AttachIntent::Deliberate)` and `create_with` return the roster row. Deliberate on purpose: the operator or the browser named this session, so a shell that already exited still hands back its final screen. | | `SessionInfo` gained `exited`, `last_exit_code`, `attachments`, `sequence`, and `idle_seconds` is now absent while a session is attached. | `list` rows carry `exited` and `last_exit_code` so a tab picker can tell a finished shell from a working one. | | An image transfer is canonical: `ImageMsg::Header { id, target: New{…} \| Frame{number} }`, no `total_bytes`, no chunk `offset`, frames 1-based. | Nothing to route — the gateway forwards `Image` frames verbatim — but the JSON the browser decodes changed shape (below). | | Every correlated arm needs an envelope, and a frame without one **ends the connection**. | The notify observer now allocates a stream id (`driver.open_stream()`), moves the driver to `Observing`, and writes the subscribe with `Correlation::stream(id)`; events are read back through `driver.classify`/`decode`. | | Ops verbs no longer close the connection after replying. | Behavior unchanged (still a dial per request) but the rationale in `broker.rs` and `docs/broker.md` said the daemon closes it, which is now false. | The observer is the one **behavior** fix rather than a rename. The old spelling (`writer.send(&NotifyMsg::Subscribe { … })`) still compiles against the new crates and the daemon answers it by ending the connection — badges would have died silently on every host under the redial loop. Reported upstream as natsukium/felis#196. ## Broker-contract facts for felis-web-component The JSON is still the domain-serde view (`felis-grid`'s `transcode::body_to_json`), not pbjson, and the row payload's `{"Rle": {graphemes, attr_runs, sized_cells, soft_wrap_continued}}` view is byte-compatible. What moved: - **Attach header** (legacy first message, and the broker's `attach`/`create` `ok`): `{"rows","cols","minor"}` — `caps` is gone, `minor` is `min(gateway, daemon)` and is per-stream. - **`GridMsg`**: `GridSize` → `Size`; `RowDelta{row,packed_cells}` and `RowDeltaBatch{rows}` collapse into one `RowDelta{rows:[[u16,row]…]}`; `RehydrateBegin{dims}` → the unit variant `"RehydrateBegin"`; `Bell`/`Notify` → `Attention{source:"Bell"|"Notification"}`; `ClipboardSet{selection,data}` → `ClipboardSet{write:{selection,data}}`; `ModeFlags` gains `reverse_video` and its `mouse_protocol` / `modify_other_keys` are now strings (`"Off"|"ButtonEvents"|"ButtonAndDrag"|"AnyMotion"`, `"Off"|"Level1"|"Level2"`); `KittyKbdFlags.flags` is a typed flag set; new `PaletteColor{index,action}` and `PaletteResetAll`. `SearchMatch`, `SearchDone`, `ScrollbackRow`, `ScrollbackDone` and `CaptureRegion` are **gone from the family**. - **`ImageMsg`**: `Header{id,target:{"New":{width,height,format}}|{"Frame":{number}}}`; `Chunk{id,bytes}` (no `offset`); `ShowFrame{id,number}` (1-based, was `index`); the `FrameHeader`/`FrameChunk`/`FrameComplete` trio is gone. - **`SessionMsg`** (the browser's `"Control"` bucket): `Attached{info}` / `Created{info}` where it was `{id,dims}`; `AttachFailed{reason,detail}` with a typed reason. - **`ConnMsg`** (also `"Control"`): `Welcome{build_id,identity?}`; `Hello{mode,pull_paced}`; new `Refused`/`Cancel`/`End`/`Error`; `Ping`/`Pong` gone. - **`InputMsg`** (browser → gateway): `Resize{dims:{rows,cols,pixel_w,pixel_h}}` where it was four loose fields; `NextGridFrame` is payload-free; `Search`, `SearchCancel`, `CaptureScrollback` and `CaptureRegion` no longer exist. - **`list` rows**: `{host,session,rows,cols,title,cwd,idle_seconds,tags,last_notification, foreground,exited,last_exit_code}` — `idle_seconds` is `null` while attached, `exited` marks the post-exit grace. - **`{"ev":"notify"}`** is unchanged, fields and all. ## Verified All under `nix develop`, on this host: - `cargo fmt --all --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test` (26 passed), `cargo deny check` (advisories/bans/licenses/sources ok), `nix build .#gateway`. - **Live, against a throwaway `felis-daemon` built from `105b0899`** on a scratch socket, driven by a hand-rolled WebSocket client: - legacy contract: `101`, first message `{"rows":24,"cols":80,"minor":10}`, `Grid` frames following; - broker contract: `felis-broker.v1` echoed, `hosts`, `create` (`{"sid":1,"host":"local", "session":"8ffb…","rows":30,"cols":100,"minor":10}`), keystrokes reaching the PTY and producing frames, `list` across the roster showing `idle_seconds: null` for the attached session and `6` for the detached one, `tag` → `{"tags":["probe"]}`, `destroy` → `{"existed":true}`; - the notify observer: an `OSC 9` in a session arriving as `{"ev":"notify","host":"local","session":"ef77…","notification":{"body": "probe-notification","urgency":"normal"},…}`. Against the pre-fix binary the same run gets `Driver(Correlation { kind: Notify, expected: "a stream_id allocating the next unopened stream", found: "no correlation envelope on Notify::Subscribe" })` in the daemon log and no events at all. - The daemon and gateway were both started by this run on a private socket and stopped afterwards. ## Left open - **Search and scrollback export do not reach the browser** — they left the `Input` family for correlated families whose ids come from the driver the relay drops when it splits a connection into a reader task and a writer task. Filed as #1 with the three candidate shapes; the docs now record it as out of scope rather than implied. - `NotifyMsg::Lagged` (the daemon dropped events an observer was too slow to read) is read and discarded rather than surfaced — surfacing it would add an event to the browser contract, which belongs with whoever consumes it.
felis reset its wire baseline, so the four pinned crates no longer offer
the API this relay was written against: the capability bitmap and the
encoding negotiation are gone (a connection now states a mode plus one
pacing flag, and additions ride the protocol minor), the carrier is
erased at connect time, an attach answers with the session's whole
roster row, a create states its geometry by presence, and every
correlated frame carries a stream or request id.

The browser-visible consequences, since felis-web-component decodes what
this forwards: the attach header reports `minor` where it reported
`caps`; a roster row gains `exited` / `last_exit_code` and leaves
`idle_seconds` out while a session is attached; `GridMsg` renames
`GridSize` to `Size`, folds `RowDelta` and `RowDeltaBatch` into one
`RowDelta { rows }`, replaces `Bell` / `Notify` with `Attention` and
nests `ClipboardSet` under a `write`; and the search and capture verbs
have left the `Input` family entirely.

The notify observer is the one behavior change rather than a rename: a
subscribe opens a stream now, and one carrying no correlation envelope
is what the daemon ends the connection over — badges would have died
silently on every host, under a redial loop that never reported why.
Both documents argued from a wire that no longer exists: a negotiated
capability set, an Ops connection the daemon closes under every mutating
verb, and a carrier split visible as two connection types. A reader
following any of the three would design against felis's previous
release, and the broker contract is the one thing this repo shares with
the browser halves — a stale `caps` in the attach header is a field
somebody will try to read.

Also records what a `list` row carries now, because `idle_seconds`
states attachment by absence and `exited` is the only thing separating a
finished shell from a working one in a tab picker.
docs: record search and capture as out of scope for the broker
All checks were successful
ci / fmt / clippy / test / deny (pull_request) Successful in 41s
577138be16
They were never a broker verb: they rode the Input family, so the
contract needed nothing for them. Now that they are correlated families
a sid frame cannot carry, their absence is a gap somebody has to decide
about rather than a shape the protocol already covers.
natsukium deleted branch follow-felis-105b0899 2026-09-07 21:31:21 +09:00
natsukium referenced this pull request from a commit 2026-09-07 21:31:22 +09:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
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
natsukium/felis-web-gateway!2
No description provided.