Stabilize the format as felis-cast v1 #8

Merged
natsukium merged 7 commits from feat/units into main 2026-10-14 16:20:32 +09:00
Owner

Follows felis to v0.2.0, renames the tool to felis-cast, and stabilizes the envelope as felis-cast v1.

  • Recordings are grouped into units (burst / compose cycle / single); the recorder dials pull-paced so each cycle keeps the daemon's cycle_end, and every frame of a unit shares one timestamp.
  • serve holds each cycle for a pull-paced client's NextGridFrame and joins due cycles under one cycle_end, answers Resize with the recording's Size, and closes on Detach. Without this a local felis GUI painted the first screen and stopped.
  • Keyframe semantics, narrow extension kinds (cast.* / x-*), and the migrate commitment for felis-json 2 are specified in the docs.
  • Verified live: a felis daemon recording replayed through the felis GUI, and in the browser through felis-web-component.
Follows felis to v0.2.0, renames the tool to felis-cast, and stabilizes the envelope as `felis-cast` v1. - Recordings are grouped into units (burst / compose cycle / single); the recorder dials pull-paced so each cycle keeps the daemon's `cycle_end`, and every frame of a unit shares one timestamp. - `serve` holds each cycle for a pull-paced client's `NextGridFrame` and joins due cycles under one `cycle_end`, answers `Resize` with the recording's `Size`, and closes on `Detach`. Without this a local felis GUI painted the first screen and stopped. - Keyframe semantics, narrow extension kinds (`cast.*` / `x-*`), and the `migrate` commitment for felis-json 2 are specified in the docs. - Verified live: a felis daemon recording replayed through the felis GUI, and in the browser through felis-web-component.
felis v0.2.0 is the release the format stabilization targets, so the
tool has to speak its wire before the spec is reworked on top of it.

The release raises the protocol minor to 1 and adds
SessionInfo.program_status (OSC 7501 records). serve advertises a
recording, which has no program reporting status, so its roster row
carries none. felis-json stays at v1 with only optional fields added,
so recordings written against v0.1.0 still read.
"FCast" is already taken in the same space: FUTO's open casting
protocol (Grayjay's receiver) and IETF RFC 6968's FCAST file delivery.
"felis-fcast" also reads as felis-felis-cast next to its siblings
felis-reel, felis-tui and felis-web.

The crate, binary, lib path, shipped skill and commit scope become
felis-cast / `felis cast <sub>` / `cast`. The `.fcast` extension stays:
the collision is the bare word, not the extension. The header `format`
id stays "felis-fcast" here on purpose, because changing it breaks
every reader; it moves with the envelope rework, the one break that
already invalidates existing recordings.
felis v0.2.0 is out and the format is to be stabilized, so the spec is
reworked from first principles before code follows it.

The unit rules come from a replay failure: a recorder that dials
eager-push receives no CycleEnd, while a felis client on a local socket
dials pull-paced by default and holds its paint until a CycleEnd closes
the cycle (felis-client-core pull.rs). Replaying such a recording paints
the first screen and stops. Recording from a pull-paced subscription
keeps the daemon's own cycle boundaries, one timestamp per unit keeps a
player from presenting a scroll before its rows, and serve answers each
pull with every due cycle joined so pacing does not drift.

The old "a mid-stream rehydrate burst is already a keyframe" claim was
wrong: felis sends each registry entry once per connection and a burst
names only visible ones, so a seek would land rows on ids the player
never installed. A keyframe is now defined by what it must restate.

The header id becomes "felis-cast" at version 1, the first envelope
with a stability promise; earlier files are re-recorded. Extension
points are narrowed so additions never need a bump: unknown cast.* and
x-* kinds are skipped, an unknown bare kind still rejects the file.
`migrate` for felis-json 2 is committed as a tool promise, not a format
guarantee, since v2's shape is unknown.
A felis client on a local socket dials pull-paced and holds its paint
until a CycleEnd closes the cycle it pulled. The recorder dialed
eager-push, so recordings carried no CycleEnd, and serve ignored
Hello.pull_paced: replaying to such a client painted the first screen
and stopped.

The recorder now dials pull-paced, re-pulls after every burst and
cycle, writes the attach size before the first burst, and stamps each
unit with its last frame's arrival time, as the felis-cast v1 envelope
specifies. serve decodes and groups the recording into units before it
binds, so a malformed file fails the command rather than a client
mid-replay; it holds each cycle for a pull and answers with every due
cycle joined under one cycle_end, so replay does not fall behind a
recording that ran faster than the client presents. An eager-push
client still gets units on their timestamps, without cycle_end. A
detach after the attach now closes the connection.

The parser reads "felis-cast" version 1, rejects bare kinds outside
grid/image, and keeps cast.* / x-* frames for consumers that know them.
The integration test now dials with felis-client-core's PullScheduler,
so a felis bump that changes what a pull-paced client expects fails
here instead of in a user's window. A live recording from a felis
0.1.4 daemon was checked to carry cycle_end per cycle.
The daemon closes the connection as soon as the recorded session exits,
which can land right after a cycle_end. The recorder's next pull then
hit a broken pipe, and propagating it threw away the whole recording.
A failed pull now ends the recording like a closed stream does; every
unit recorded so far is already complete.
The felis GUI resizes its grid to the window on attach and sends
InputMsg::Resize. serve discarded it, so the client kept a grid wider
than the recording and padded every replayed row (seen replaying a
16x60 recording into a 78x137 window).

A live daemon answers the resize of a mirror that does not own the size
with the authoritative Size, and the client letterboxes. The recording
owns the size here, so serve now does the same, outside the pull gate,
tracking the latest replayed `size`. Inbound handling moves to one
event channel so a resize is answered even while serve waits for a
unit to fall due or for a pull.
docs: correct the felis-web-component backlog entry
All checks were successful
ci / cargo fmt / clippy / test / deny (pull_request) Successful in 28s
ci / cargo fmt / clippy / test / deny (push) Successful in 37s
ef916a8f43
Replaying recordings in felis-web-component showed it does not depend on
this crate (it only names the old format id), and that its
time-budgeted frame drain can paint between frames of one unit, which
the v1 envelope forbids. The entry now names what it actually needs.
natsukium deleted branch feat/units 2026-10-14 16:20:32 +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!8
No description provided.