protocol: give input admission its own barrier instead of a roster query #358

Merged
natsukium merged 6 commits from protocol/issue-306-input-fence into main 2026-09-22 23:57:37 +09:00
Owner

Closes #306.

Implements the "Triage plan (2026-09-22)" comment on the issue.

What changed

Wire. SessionMsg gains SessionInputFence input_fence = 8 (to daemon, request opener, Window+Ops, Attached) and SessionInputAccepted input_accepted = 9 (to client, request reply). Both messages are empty, both at since 0, so PROTOCOL_MINOR stays 0 and no ledger row is added. The field-100 correlation-slot comment now names the pair. Domain variants, Directed::ARMS rows with the two correlation classes, and the convert/session.rs mapping move with it; the committed codegen is regenerated with just proto. MinorGated for SessionMsg folds the arms through its existing arm_requires catch-all.

Daemon. route_frame maps InputFence to SessionCmd::InputFence { sub, request } (the Session arm now keeps the Delivered so it can read the request id). The session actor answers through push_to with the new OutEvent::Session { msg, correlation }, encoded in streaming::body_for with CheckedFrame::encode_correlated.

Client-core / CLI / bridge. Connection::input_fence() beside list_sessions(). cmd_send's barrier is the fence; timeout and error-kind mapping are unchanged. The bridge's Link::send_input switches too, and Link::decode gained DaemonMsg::Session so the reply is not dropped as a droppable frame. No roster fallback.

felis-json v1. Both arms are refused at conversion with a new JsonError::Outside, because the format carries no correlation envelope. No schema or golden regeneration (just schema is a no-op, verified).

Tests

  • felis-protocol: both arms in session_cases(), a correlation-class assertion over the corpus, and a correlated round trip through encode_correlated.
  • felis-grid: both arms refused as outside v1.
  • Daemon actor: reply goes to the requester alone on its own request id; resize then fence writes nothing and still answers; a DECSET 1004 focus report is written before the fence behind it answers and reaches the PTY; a dropped focus report on a saturated writer does not hold back the fence.
  • Daemon connection: empty fence answered at once; a key parked on a full budget holds back the fence behind it (this replaces the roster-query form of the same test); input admitted before the fence survives the client hanging up (the child reads the bytes); a fence after the session ended gets no reply and the connection ends.
  • Bridge: sessions.send waits for InputAccepted, and the fake daemon asserts the frame after the input is the fence, not an Ops::List.

Verified

just fmt-check, just lint, just prose-check, just docs-links, just proto, just proto-compat (wire-compatible with the merge base), just schema (no-op). Host load was above 20, so the crate-scoped run was used in place of just check: cargo nextest run --all-features -p felis-protocol -p felis-grid -p felis-client-core -p felis-cli -p felis-daemon (2564 passed) plus -p felis-transport (156 passed).

Deviations from the plan

  • The plan expected the rogue_daemon CLI test to see the fence after the input. That daemon only serves sessions info in every_machine_failure_class_is_one_object_and_one_code, never send, so it needed no change. The scripted daemon inside cli_sessions.rs's own unit tests is the one that answers the fence, and it was updated.
  • The unsaturated focus-report test asserts the attempt order through the actor's PTY-step record and then that the writer drains, rather than asserting WriteOutcome::Queued directly: write_pty does not return the outcome to the actor.
  • cli.md's send bullet already worded the barrier as "the daemon's admission of the input" with no mention of a roster query, so it needed no edit; the grep sweep found no roster-barrier prose in docs/explanation/architecture/ipc.md or skills/felis/SKILL.md either.
Closes #306. Implements the "Triage plan (2026-09-22)" comment on the issue. ## What changed **Wire.** `SessionMsg` gains `SessionInputFence input_fence = 8` (to daemon, request opener, `Window`+`Ops`, `Attached`) and `SessionInputAccepted input_accepted = 9` (to client, request reply). Both messages are empty, both at `since 0`, so `PROTOCOL_MINOR` stays `0` and no ledger row is added. The field-100 correlation-slot comment now names the pair. Domain variants, `Directed::ARMS` rows with the two correlation classes, and the `convert/session.rs` mapping move with it; the committed codegen is regenerated with `just proto`. `MinorGated for SessionMsg` folds the arms through its existing `arm_requires` catch-all. **Daemon.** `route_frame` maps `InputFence` to `SessionCmd::InputFence { sub, request }` (the `Session` arm now keeps the `Delivered` so it can read the request id). The session actor answers through `push_to` with the new `OutEvent::Session { msg, correlation }`, encoded in `streaming::body_for` with `CheckedFrame::encode_correlated`. **Client-core / CLI / bridge.** `Connection::input_fence()` beside `list_sessions()`. `cmd_send`'s barrier is the fence; timeout and error-kind mapping are unchanged. The bridge's `Link::send_input` switches too, and `Link::decode` gained `DaemonMsg::Session` so the reply is not dropped as a droppable frame. No roster fallback. **`felis-json` v1.** Both arms are refused at conversion with a new `JsonError::Outside`, because the format carries no correlation envelope. No schema or golden regeneration (`just schema` is a no-op, verified). ## Tests - `felis-protocol`: both arms in `session_cases()`, a correlation-class assertion over the corpus, and a correlated round trip through `encode_correlated`. - `felis-grid`: both arms refused as outside v1. - Daemon actor: reply goes to the requester alone on its own request id; resize then fence writes nothing and still answers; a DECSET 1004 focus report is written before the fence behind it answers and reaches the PTY; a dropped focus report on a saturated writer does not hold back the fence. - Daemon connection: empty fence answered at once; a key parked on a full budget holds back the fence behind it (this replaces the roster-query form of the same test); input admitted before the fence survives the client hanging up (the child reads the bytes); a fence after the session ended gets no reply and the connection ends. - Bridge: `sessions.send` waits for `InputAccepted`, and the fake daemon asserts the frame after the input is the fence, not an `Ops::List`. ## Verified `just fmt-check`, `just lint`, `just prose-check`, `just docs-links`, `just proto`, `just proto-compat` (wire-compatible with the merge base), `just schema` (no-op). Host load was above 20, so the crate-scoped run was used in place of `just check`: `cargo nextest run --all-features -p felis-protocol -p felis-grid -p felis-client-core -p felis-cli -p felis-daemon` (2564 passed) plus `-p felis-transport` (156 passed). ## Deviations from the plan - The plan expected the `rogue_daemon` CLI test to see the fence after the input. That daemon only serves `sessions info` in `every_machine_failure_class_is_one_object_and_one_code`, never `send`, so it needed no change. The scripted daemon inside `cli_sessions.rs`'s own unit tests is the one that answers the fence, and it was updated. - The unsaturated focus-report test asserts the attempt order through the actor's PTY-step record and then that the writer drains, rather than asserting `WriteOutcome::Queued` directly: `write_pty` does not return the outcome to the actor. - `cli.md`'s `send` bullet already worded the barrier as "the daemon's admission of the input" with no mention of a roster query, so it needed no edit; the grep sweep found no roster-barrier prose in `docs/explanation/architecture/ipc.md` or `skills/felis/SKILL.md` either.
protocol: give input admission its own barrier instead of a roster query
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
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
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
101ef6c757
`felis sessions send` and the bridge confirmed that the daemon had taken
their input by reading an `Ops::List` reply back on the same connection:
the roster query cannot be answered until the input frames ahead of it
cleared admission, so the reply doubled as a barrier. That couples input
admission to an unrelated verb and makes every external implementation
learn an accidental ordering trick to get a guarantee the protocol never
states.

`Session::InputFence` and its `InputAccepted` reply state it instead.
The pair is correlated, so a caller may hold several barriers at once,
and it is the first use of the family's field-100 correlation slot. The
reply means processing order plus admission of the bytes the
byte-carrying arms encoded to; the reports the other arms generate are
unreserved writes attempted ahead of it and stay best-effort, so a
saturated writer drops one and the fence still completes.

No fallback to the roster query: no daemon is published, so the fence is
part of the base schema rather than a minor addition.

`felis-json` v1 excludes both arms. The format carries no correlation
envelope, so a reply stripped of the request id it answers would name no
fence.

Assisted-by: Claude Code
The fence's reference and proto wording attributed DECSET 2048 to the
color-scheme report and claimed the input arms that reserve no budget
reach no PTY. `Input::Resize` writes an in-band resize report under
DECSET 2048, unreserved and attempted before the session reaches the
fence, exactly like the focus report under 1004 and the color-scheme
report under 2031. A reader following the old wording would expect a
resize ahead of a fence to be invisible to the child.

The budget table's claim is narrowed to what it is really about: those
arms carry no bytes of their own for the child, which is why they
reserve nothing, not that nothing they cause is ever written.

Assisted-by: Claude Code
# Conflicts:
#	crates/felis-cli/src/cli_sessions.rs
protocol: keep the fence's proto comment inside the comment-length rule
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
59cb64d34c
The reference owns the full statement of what the acknowledgement means;
the schema comment names the arms and cites it.

Assisted-by: Claude Code
docs: stop saying no session arm correlates now that the fence does
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 2m50s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 2m43s
darwin / detect relevant changes (pull_request) Successful in 6s
darwin / darwin docs-only no-op (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 2m20s
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
pr / nix flake check (pull_request) Has been cancelled
5747f2a5ed
The explanation page and a test comment still described the session
family's correlation slot as unused; the fence pair uses it.

Assisted-by: Claude Code
Merge remote-tracking branch 'origin/main' into protocol/issue-306-input-fence
Some checks failed
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
bench / Criterion full-suite snapshot (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m13s
darwin / detect relevant changes (pull_request) Successful in 4s
darwin / darwin docs-only no-op (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m5s
pr / nix flake check (pull_request) Successful in 2m54s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 4m9s
darwin / publish felis (aarch64-darwin) (pull_request) Has been skipped
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m43s
pr / wire schema is compatible with the base (pull_request) Successful in 11s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 2m25s
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 4m1s
windows / frontend smoke (Windows) (pull_request) Successful in 1m53s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 12s
412978c217
natsukium deleted branch protocol/issue-306-input-fence 2026-09-22 23:57:37 +09:00
Commenting is not possible because the repository is archived.
No description provided.