[v0.1/IPC Review] Decide unresolved grid registry handles before decoder freeze #308

Closed
opened 2026-09-21 12:09:06 +09:00 by natsukium · 1 comment
Owner

Parent: #12

Problem

The grid registry contract and the client's failure policy disagree at the semantic boundary.

The IPC reference guarantees that every Cluster or Hyperlink entry named by a RowDelta is sent before that row. A missing entry is therefore impossible between honest peers. The same reference nevertheless tells a client to treat an unreceived id as "not here yet", degrade the cell, and keep the connection. ShadowScreen::apply similarly drops zero, over-cap, or over-length registry entries and later resolves a missing handle as None.

This conflicts with the connection-local corruption policy: silently skipping an unreadable grid update leaves the client drawing a screen that differs from the daemon, while closing only this attachment permits a clean rehydrate and leaves the session running.

It also freezes an awkward decoder boundary. The IPC explanation records that narrowing accepted values after v1 ships is a compatibility break that needs a real old-peer answer. If invalid registry references should close the connection, that choice belongs before the first release.

Decision to make before the v0.1 contract freezes

Prefer treating daemon-impossible registry values and unresolved row references as semantic corruption:

  • reject id 0;
  • reject ids and entry text beyond the published caps before allocation;
  • after decoding a row, reject every cluster or hyperlink handle not already installed;
  • close only the affected connection and let the normal reconnect/rehydrate path recover.

Valid sparse delivery remains supported. "Sparse" means that unrelated ids may be absent; it does not mean that a row may reference an absent id.

If degraded rendering is retained, the explanation should define why this semantic invariant differs from the other fatal grid invariants, what later event repairs an already-applied row, and which malformed values are intentionally tolerated. "Not here yet" is not sufficient under the documented registry-before-row order.

Acceptance criteria

  • The reference states one unambiguous policy for invalid entries and unresolved row references.
  • Invalid ids and lengths are checked before any peer-controlled sparse-table growth.
  • A row referencing an absent cluster or hyperlink either fails the connection or follows a separately justified repair protocol.
  • Valid visible-first sparse ordering and lazy registry-tail convergence still work.
  • Tests cover id 0, an id at/past each cap, over-length text, an unresolved row handle, holes unrelated to the row, and a valid registry-before-row sequence.
  • A failure affects only the attachment; the daemon session and other subscribers survive.
  • ShadowError, IPC reference/explanation, grid data-model docs, and CHANGELOG.md agree.

Principle check

  1. Add only what earns its place — Pass. This settles an existing decoder invariant and adds no capability.
  2. Render everything, fast — Pass. The preferred policy prevents quietly missing graphemes or links; validation can run during the row walk already required for decode/apply.
  3. The daemon owns state, the client owns pixels — Pass. A client that cannot establish the daemon's state reconnects instead of inventing a degraded authoritative state.
  4. Explicit over heuristic — Pass. The decision is based on typed ids and published bounds, not shell content.

Evidence

  • crates/felis-client-core/src/shadow.rs:263 (ShadowScreen::apply)
  • crates/felis-client-core/src/shadow.rs:321 (GridMsg::Hyperlink)
  • crates/felis-client-core/src/shadow.rs:333 (GridMsg::Cluster)
  • crates/felis-client-core/src/shadow.rs:405 (row decode/apply)
  • crates/felis-daemon/src/serve/streaming.rs (ship_referenced)
  • docs/reference/ipc.md "Corruption" and "Registry delivery"
  • docs/explanation/architecture/ipc.md "A corrupt frame ends the connection" and the pre-release narrowed-decoder warning
  • docs/explanation/data-model/grid-and-cells.md "Referenced-before-use is causal, not positional"
Parent: #12 ## Problem The grid registry contract and the client's failure policy disagree at the semantic boundary. The IPC reference guarantees that every `Cluster` or `Hyperlink` entry named by a `RowDelta` is sent before that row. A missing entry is therefore impossible between honest peers. The same reference nevertheless tells a client to treat an unreceived id as "not here yet", degrade the cell, and keep the connection. `ShadowScreen::apply` similarly drops zero, over-cap, or over-length registry entries and later resolves a missing handle as `None`. This conflicts with the connection-local corruption policy: silently skipping an unreadable grid update leaves the client drawing a screen that differs from the daemon, while closing only this attachment permits a clean rehydrate and leaves the session running. It also freezes an awkward decoder boundary. The IPC explanation records that narrowing accepted values after v1 ships is a compatibility break that needs a real old-peer answer. If invalid registry references should close the connection, that choice belongs before the first release. ## Decision to make before the v0.1 contract freezes Prefer treating daemon-impossible registry values and unresolved row references as semantic corruption: - reject id 0; - reject ids and entry text beyond the published caps before allocation; - after decoding a row, reject every cluster or hyperlink handle not already installed; - close only the affected connection and let the normal reconnect/rehydrate path recover. Valid sparse delivery remains supported. "Sparse" means that unrelated ids may be absent; it does not mean that a row may reference an absent id. If degraded rendering is retained, the explanation should define why this semantic invariant differs from the other fatal grid invariants, what later event repairs an already-applied row, and which malformed values are intentionally tolerated. "Not here yet" is not sufficient under the documented registry-before-row order. ## Acceptance criteria - [ ] The reference states one unambiguous policy for invalid entries and unresolved row references. - [ ] Invalid ids and lengths are checked before any peer-controlled sparse-table growth. - [ ] A row referencing an absent cluster or hyperlink either fails the connection or follows a separately justified repair protocol. - [ ] Valid visible-first sparse ordering and lazy registry-tail convergence still work. - [ ] Tests cover id 0, an id at/past each cap, over-length text, an unresolved row handle, holes unrelated to the row, and a valid registry-before-row sequence. - [ ] A failure affects only the attachment; the daemon session and other subscribers survive. - [ ] `ShadowError`, IPC reference/explanation, grid data-model docs, and `CHANGELOG.md` agree. ## Principle check 1. **Add only what earns its place — Pass.** This settles an existing decoder invariant and adds no capability. 2. **Render everything, fast — Pass.** The preferred policy prevents quietly missing graphemes or links; validation can run during the row walk already required for decode/apply. 3. **The daemon owns state, the client owns pixels — Pass.** A client that cannot establish the daemon's state reconnects instead of inventing a degraded authoritative state. 4. **Explicit over heuristic — Pass.** The decision is based on typed ids and published bounds, not shell content. ## Evidence - `crates/felis-client-core/src/shadow.rs:263` (`ShadowScreen::apply`) - `crates/felis-client-core/src/shadow.rs:321` (`GridMsg::Hyperlink`) - `crates/felis-client-core/src/shadow.rs:333` (`GridMsg::Cluster`) - `crates/felis-client-core/src/shadow.rs:405` (row decode/apply) - `crates/felis-daemon/src/serve/streaming.rs` (`ship_referenced`) - `docs/reference/ipc.md` "Corruption" and "Registry delivery" - `docs/explanation/architecture/ipc.md` "A corrupt frame ends the connection" and the pre-release narrowed-decoder warning - `docs/explanation/data-model/grid-and-cells.md` "Referenced-before-use is causal, not positional"
Author
Owner

Direction agreed 2026-09-21 after three sol design-review rounds (final verdict PASS). Full plan and batch order: see the comment on #12.

  • #304 / #315 / #308 — one policy: a daemon-impossible Grid value returns
    ShadowError and closes only that attachment; reconnect/rehydrate recovers.
    Resize-race tolerance is kept only for row indices/widths within
    MAX_GRID_ROWS / MAX_GRID_COLS. Unresolved cluster/hyperlink handles in a
    decoded row are corruption; degraded rendering is removed. Id 0, over-cap
    ids and over-length text are rejected before allocation.
Direction agreed 2026-09-21 after three `sol` design-review rounds (final verdict PASS). Full plan and batch order: see the comment on #12. - **#304 / #315 / #308** — one policy: a daemon-impossible Grid value returns `ShadowError` and closes only that attachment; reconnect/rehydrate recovers. Resize-race tolerance is kept only for row indices/widths within `MAX_GRID_ROWS` / `MAX_GRID_COLS`. Unresolved cluster/hyperlink handles in a decoded row are corruption; degraded rendering is removed. Id 0, over-cap ids and over-length text are rejected before allocation.
Commenting is not possible because the repository is archived.
No description provided.