Follow felis to 105b0899: wire-baseline reset, .fcast envelope v2 #1

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

Follows felis to 105b089979369fd310757375af3df7900ae7e523 (105b0899), from
the pin at 32775067 (2026-07-16). felis reset its wire-break baseline to 1.0
(f56f5529) and grew to PROTOCOL_MINOR 10, so this is renames and removals,
not additions — the old pin names a protocol that no longer exists.

Two commits: the code adaptation, then the doc cascade.

What felis changed, and what it forced here

Capability/encoding negotiation is gone. No Capabilities bitfield, no
Encoding enum, no SCROLL_OP, no self-describing tagged handshake. A
connection is now a frozen 8-byte preface agreeing a major and an effective
minor, and the wire carries protobuf alone.

  • record dials connect(.., Offer::window(false)) instead of naming an
    encoding preference. Window is deliberate: it is the only mode felis
    streams a grid to, so a recording of anything less is not a recording of the
    session.
  • serve answers the preface on the raw socket halves with
    DaemonPreface::select (refusing an unservable major as a daemon does) and
    holds its FrameWriter at the negotiated minor, so every frame it writes
    passes the same send gate a real daemon's does. Its advertised roster row is
    shaped for that minor — SessionInfo.sequence only at minor ≥ 3 — so the
    roster is never refused over a field the peer never asked for. Welcome
    carries identity only at minor ≥ 9.

That removal reaches the .fcast header, which recorded proto_version
(felis's retired WIRE_GENERATION) plus capabilities and capability_bits.
None of the three has a counterpart to carry forward, so the header now
records proto_major and proto_minor — the effective minor, not the
recorder's build, since the bodies can only hold what both peers agreed on.
Removing header fields is a breaking envelope change by
docs/reference/format.md "Versioning", so SUPPORTED_VERSION is 2 and v1
files are rejected rather than half-interpreted. The decision, including why
refilling proto_version with a major was rejected, is recorded in the
explanation twin per this repo's inline-decision rule.

Message shapes moved under that. GridMsg::RowDelta batches
rows: Vec<(u16, RowPayload)>; RehydrateBegin is a unit variant whose
dimensions now reach a client through Attached or Size, so the generators
emit an explicit Size for a reader that has only the file. SpawnArgs grew
env_base/tags and takes Option<RequestedDims>; create+attach is one
create_with round trip, which also closes the window where a rehydrate burst
could land before the recorder was subscribed. The row encoder is
encode_row_json(RowEncode { .. }), and transcode::body_to_json no longer
takes an encoding.

Docs. Every page stated felis facts that no longer exist — WIRE_GENERATION,
SCROLL_OP, picked_encoding, Conn::Ping, RowDeltaBatch, the Control /
Log kinds. The pseudo-daemon contract gains the version-preface exchange it
never had to describe (negotiation used to ride the handshake) and records two
things the code always did but the page did not say: Ops::List is answered
correlated, and the roster row is minor-shaped. The shipped skills/fcast
skill is cascaded too.

Also corrects felis-webfelis-web-component in the browser-replay
references, where the rename in 8171ada had not reached. (The partial work this
branch started from had reverted that rename; restored.)

One thing documented rather than fixed: filed as felis#193

felis's RowPayload used to splice its structural JSON inline under a
human-readable serializer; it is now #[serde(transparent)] over Vec<u8>.
So a recorded row lands as a JSON array of byte values wrapping the row
codec's JSON text — correct and round-trippable, but 3.5x the size and one
decode step from readable. This is felis's serde shape reaching every consumer
of transcode::body_to_json, not a felis-fcast choice: splicing it back here
would mean this repo defining part of the msg schema, which its format seam
exists to prevent.

So the reference states the shape, the explanation twin says why it is not
fixed here, and the skill grows a jq recipe that decodes a row. Filed upstream
as natsukium/felis#193, which also notes that felis's own round-trip test
asserts equality without asserting shape, so it passes while its name's
property does not hold.

Verified

All run in nix develop, mirroring .forgejo/workflows/ci.yml 1:1:

  • cargo fmt --all --check — clean
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo test --all-features — 20 passed, 0 failed
  • cargo deny check — advisories, bans, licenses, sources ok
  • cargo run --example gen_hello_fcast / gen_scroll_fcast — both regenerate
    and re-parse

The load-bearing check is tests/serve_roundtrip.rs: it binds serve on a
socket and drives felis-client-core's real connect/attach path against it —
preface, Hello/Welcome, correlated Ops::List, attach and create — then
decodes the replayed RowDelta back through the packed wire row codec and
compares the cells. That exercises the whole adapted stack against the actual
client, not a mock.

Also swept: no reference to the old rev or to any removed felis name survives
outside the v1→v2 decision record that names them as retired; every relative
doc link resolves.

Left open

  • Image replay is untested. felis made image transfers canonical and
    stateful (68fe54cf: Header now names New/Frame, total_bytes and
    chunk offset gone). serve replays recorded Image frames verbatim and
    record stores them, so both follow the new shape for free — but no test
    covers an image recording, and neither generator emits one. Tracked in
    docs/backlog.md alongside the --speed pacing and Detach gaps; not new
    to this change.
  • felis-web-component's sample recordings are stale. The .fcast files it
    ships are envelope v1, which no current reader accepts. Cross-repo, so it
    cannot land here; noted in docs/backlog.md.
Follows felis to `105b089979369fd310757375af3df7900ae7e523` (105b0899), from the pin at `32775067` (2026-07-16). felis reset its wire-break baseline to 1.0 (`f56f5529`) and grew to `PROTOCOL_MINOR` 10, so this is renames and removals, not additions — the old pin names a protocol that no longer exists. Two commits: the code adaptation, then the doc cascade. ## What felis changed, and what it forced here **Capability/encoding negotiation is gone.** No `Capabilities` bitfield, no `Encoding` enum, no `SCROLL_OP`, no self-describing tagged handshake. A connection is now a frozen 8-byte preface agreeing a major and an effective minor, and the wire carries protobuf alone. - `record` dials `connect(.., Offer::window(false))` instead of naming an encoding preference. `Window` is deliberate: it is the only mode felis streams a grid to, so a recording of anything less is not a recording of the session. - `serve` answers the preface on the raw socket halves with `DaemonPreface::select` (refusing an unservable major as a daemon does) and holds its `FrameWriter` at the negotiated minor, so every frame it writes passes the same send gate a real daemon's does. Its advertised roster row is shaped for that minor — `SessionInfo.sequence` only at minor ≥ 3 — so the roster is never refused over a field the peer never asked for. `Welcome` carries `identity` only at minor ≥ 9. **That removal reaches the `.fcast` header,** which recorded `proto_version` (felis's retired `WIRE_GENERATION`) plus `capabilities` and `capability_bits`. None of the three has a counterpart to carry forward, so the header now records `proto_major` and `proto_minor` — the *effective* minor, not the recorder's build, since the bodies can only hold what both peers agreed on. Removing header fields is a breaking envelope change by `docs/reference/format.md` "Versioning", so **`SUPPORTED_VERSION` is 2** and v1 files are rejected rather than half-interpreted. The decision, including why refilling `proto_version` with a major was rejected, is recorded in the explanation twin per this repo's inline-decision rule. **Message shapes moved under that.** `GridMsg::RowDelta` batches `rows: Vec<(u16, RowPayload)>`; `RehydrateBegin` is a unit variant whose dimensions now reach a client through `Attached` or `Size`, so the generators emit an explicit `Size` for a reader that has only the file. `SpawnArgs` grew `env_base`/`tags` and takes `Option<RequestedDims>`; create+attach is one `create_with` round trip, which also closes the window where a rehydrate burst could land before the recorder was subscribed. The row encoder is `encode_row_json(RowEncode { .. })`, and `transcode::body_to_json` no longer takes an encoding. **Docs.** Every page stated felis facts that no longer exist — `WIRE_GENERATION`, `SCROLL_OP`, `picked_encoding`, `Conn::Ping`, `RowDeltaBatch`, the `Control` / `Log` kinds. The pseudo-daemon contract gains the version-preface exchange it never had to describe (negotiation used to ride the handshake) and records two things the code always did but the page did not say: `Ops::List` is answered correlated, and the roster row is minor-shaped. The shipped `skills/fcast` skill is cascaded too. Also corrects `felis-web` → `felis-web-component` in the browser-replay references, where the rename in 8171ada had not reached. (The partial work this branch started from had reverted that rename; restored.) ## One thing documented rather than fixed: filed as felis#193 felis's `RowPayload` used to splice its structural JSON inline under a human-readable serializer; it is now `#[serde(transparent)]` over `Vec<u8>`. So a recorded row lands as a JSON array of byte values wrapping the row codec's JSON text — correct and round-trippable, but 3.5x the size and one decode step from readable. This is felis's serde shape reaching every consumer of `transcode::body_to_json`, not a felis-fcast choice: splicing it back here would mean this repo defining part of the `msg` schema, which its format seam exists to prevent. So the reference states the shape, the explanation twin says why it is not fixed here, and the skill grows a jq recipe that decodes a row. Filed upstream as **natsukium/felis#193**, which also notes that felis's own round-trip test asserts equality without asserting shape, so it passes while its name's property does not hold. ## Verified All run in `nix develop`, mirroring `.forgejo/workflows/ci.yml` 1:1: - `cargo fmt --all --check` — clean - `cargo clippy --all-targets --all-features -- -D warnings` — clean - `cargo test --all-features` — 20 passed, 0 failed - `cargo deny check` — advisories, bans, licenses, sources ok - `cargo run --example gen_hello_fcast` / `gen_scroll_fcast` — both regenerate and re-parse The load-bearing check is `tests/serve_roundtrip.rs`: it binds `serve` on a socket and drives felis-client-core's real connect/attach path against it — preface, `Hello`/`Welcome`, correlated `Ops::List`, attach and create — then decodes the replayed `RowDelta` back through the packed wire row codec and compares the cells. That exercises the whole adapted stack against the actual client, not a mock. Also swept: no reference to the old rev or to any removed felis name survives outside the v1→v2 decision record that names them as retired; every relative doc link resolves. ## Left open - **`Image` replay is untested.** felis made image transfers canonical and stateful (`68fe54cf`: `Header` now names `New`/`Frame`, `total_bytes` and chunk `offset` gone). `serve` replays recorded `Image` frames verbatim and `record` stores them, so both follow the new shape for free — but no test covers an image recording, and neither generator emits one. Tracked in `docs/backlog.md` alongside the `--speed` pacing and `Detach` gaps; not new to this change. - **felis-web-component's sample recordings are stale.** The `.fcast` files it ships are envelope v1, which no current reader accepts. Cross-repo, so it cannot land here; noted in `docs/backlog.md`.
felis reset its wire-break baseline to 1.0 (felis f56f5529) and grew the
schema to PROTOCOL_MINOR 10, so the pin this repo had frozen at 32775067
names a protocol that no longer exists. Every surface this tool touches
moved at once, and none of them degrade: they are renames and removals.

The negotiation surface is the break with the widest reach. The
`Capabilities` bitfield and `Encoding` enum are gone — a connection is
now a frozen 8-byte preface that agrees a major and an effective minor,
and the wire carries protobuf alone. `record` therefore dials
`connect(.., Offer::window(false))` instead of naming an encoding
preference, and `serve` answers the preface with `DaemonPreface::select`
and holds its `FrameWriter` at the negotiated minor, so every reply it
writes passes the same send gate a real daemon's does.

That removal reaches the `.fcast` header, which recorded `proto_version`
(felis's retired `WIRE_GENERATION`) plus the negotiated capability names
and bits. None of the three has a felis counterpart to carry forward, so
the header now records `proto_major` and `proto_minor` — the effective
minor, not this build's, since the bodies can only hold what both peers
agreed on. Removing header fields bumps the envelope version per
docs/reference/format.md "Versioning", so SUPPORTED_VERSION is 2.

The message shapes moved under that: `GridMsg::RowDelta` batches
`rows: Vec<(u16, RowPayload)>` where it carried one `row` +
`packed_cells`; `RehydrateBegin` is a unit variant, its dimensions now
reaching a client through `Attached` or `Size`, so the generators emit an
explicit `Size` for a reader that has only the file. `SpawnArgs` grew
`env_base`/`tags` and takes `Option<RequestedDims>`, and create+attach is
one `create_with` round trip rather than spawn-then-attach — which also
closes the window where a rehydrate burst could land before the recorder
was subscribed. The row encoder is `encode_row_json(RowEncode { .. })`,
and `transcode::body_to_json` no longer takes an encoding.

`record`'s `Offer::window(false)` is deliberate: `Window` is the only
mode felis streams a grid to, and a recording of anything less is not a
recording of the session.

Docs still state the retired facts and are cascaded separately.

fmt, clippy -D warnings, 20 tests and cargo deny green; both sample
generators regenerate.
docs: cascade the felis wire-baseline reset through the tree
All checks were successful
ci / cargo fmt / clippy / test / deny (pull_request) Successful in 27s
79dce37c3f
The pin bump left every page stating felis facts that no longer exist:
`WIRE_GENERATION`, the `Capabilities` bitfield and `SCROLL_OP`, the
one-byte `Encoding` tag and `picked_encoding` negotiation, `Conn::Ping`,
`RowDeltaBatch`, and the `Control` / `Log` message kinds. None of these
degraded — felis removed them — so a reader following these pages would
have been looking for surfaces that are gone.

The format reference becomes v2 and documents `proto_major` /
`proto_minor` in place of `proto_version` and the capability pair, and
its explanation twin records the decision, per this repo's rule that a
design decision lives inline in the owning explanation page. The
rationale worth keeping: refilling `proto_version` with a major was
rejected because the same field would then mean two things across files
with nothing to distinguish them, which is what `version` exists to
prevent — so v1 files are rejected outright rather than half-read.

The pseudo-daemon contract gains the version-preface exchange it never
had to describe before (the negotiation used to ride the handshake) and
loses the encoding-negotiation steps. Its pre-attach table now records
two things the code always did but the page did not say: `Ops::List` is
answered correlated, and the advertised roster row is shaped for the
effective minor so the send gate cannot refuse it over a field the peer
never asked for.

One new fact is documented rather than changed. felis's `RowPayload` used
to splice its structural JSON inline under a human-readable serializer;
it is now transparent over `Vec<u8>`, so a recorded row lands as a JSON
array of byte values wrapping the row codec's JSON text — readable, at
3.5x the size, one decode step away. The envelope stores what the
protocol enum serializes, and splicing it back here would mean this repo
defining part of the `msg` schema. Reference states the shape, the
explanation twin says why it is not fixed here, and the skill grows a jq
recipe that decodes a row.

Also corrects `felis-web` to `felis-web-component` where the rename in
8171ada had not reached (the browser-replay references), and repoints the
backlog at the sibling's now-v1 sample recordings.

fmt, clippy -D warnings, 20 tests and cargo deny green; every relative
doc link resolves.
natsukium deleted branch follow-felis-105b0899 2026-09-07 21:29:08 +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-fcast!1
No description provided.