Docs contradict each other on whether local image pixels travel over shm #194

Closed
opened 2026-09-07 20:43:36 +09:00 by natsukium · 1 comment
Owner

What

Two felis docs (and issue #120) contradict each other about whether daemon→client image
pixels travel over shared memory. A downstream client author reading them cannot tell
which is normative.

docs/explanation/architecture/overview.md — "Implications / No shared memory":

The boundary is the IPC protocol, full stop. Even when the daemon and client run on
the same host (the v1 default), they communicate over a Unix socket: never through
mmap, never through a shared file.

docs/explanation/architecture/ipc.md — "Open extensibility considerations", the
rationale for deferring the heavy-payload suppression bit:

Gate that decision on a measured cross-host bandwidth saving, not on principle:
locally, image bytes already travel over shm and the waste is marginal.

Issue #120 carries the same sentence ("locally, image bytes travel over shm and the
waste is marginal"), so the claim is in three places.

Reproduction

At 105b089979369fd310757375af3df7900ae7e523:

git show main:docs/explanation/architecture/overview.md | sed -n '119,124p'
git show main:docs/explanation/architecture/ipc.md      | sed -n '1064,1068p'

The only shm felis actually has is the Kitty t=s producer→daemon transfer mode
(docs/explanation/protocols/kitty-graphics.md, docs/explanation/data-model/image-store.md).
The daemon copies those bytes into its own image store; what reaches a client is
ImageMsg frames over the socket, exactly as "No shared memory" says.

Why it matters beyond wording

The deferral of #120 is justified by the false half. If local image pixels really rode
shm, the local waste would be zero and only the cross-host case could ever motivate the
bit. They do not: locally the pixels are copied into every subscriber's socket, so a
same-host TUI client that drops them already pays for bytes it discards. That may still
be small enough to not be worth a flag — but the deferral should rest on a fact rather
than on this one.

How it surfaced

felis-tui (natsukium/felis-tui) had copied the sentence into its own roadmap while
following felis to this rev, and a reader was sent looking for a local shm path in the
client that does not exist. Corrected downstream in
natsukium/felis-tui#follow-felis-105b0899, but the felis-side contradiction is what
produced it.

Out of scope for the downstream PR

Only felis can decide which of the two statements is the normative one, and the fix
cascades across two explanation docs plus the #120 body — a felis doc-cascade change,
not a change an ecosystem repo can make.

## What Two felis docs (and issue #120) contradict each other about whether daemon→client image pixels travel over shared memory. A downstream client author reading them cannot tell which is normative. `docs/explanation/architecture/overview.md` — "Implications / No shared memory": > The boundary is the IPC protocol, full stop. Even when the daemon and client run on > the same host (the v1 default), they communicate over a Unix socket: never through > `mmap`, never through a shared file. `docs/explanation/architecture/ipc.md` — "Open extensibility considerations", the rationale for deferring the heavy-payload suppression bit: > Gate that decision on a measured cross-host bandwidth saving, not on principle: > locally, image bytes already travel over shm and the waste is marginal. Issue #120 carries the same sentence ("locally, image bytes travel over shm and the waste is marginal"), so the claim is in three places. ## Reproduction At `105b089979369fd310757375af3df7900ae7e523`: ``` git show main:docs/explanation/architecture/overview.md | sed -n '119,124p' git show main:docs/explanation/architecture/ipc.md | sed -n '1064,1068p' ``` The only shm felis actually has is the Kitty `t=s` **producer→daemon** transfer mode (`docs/explanation/protocols/kitty-graphics.md`, `docs/explanation/data-model/image-store.md`). The daemon copies those bytes into its own image store; what reaches a client is `ImageMsg` frames over the socket, exactly as "No shared memory" says. ## Why it matters beyond wording The deferral of #120 is *justified* by the false half. If local image pixels really rode shm, the local waste would be zero and only the cross-host case could ever motivate the bit. They do not: locally the pixels are copied into every subscriber's socket, so a same-host TUI client that drops them already pays for bytes it discards. That may still be small enough to not be worth a flag — but the deferral should rest on a fact rather than on this one. ## How it surfaced felis-tui (`natsukium/felis-tui`) had copied the sentence into its own roadmap while following felis to this rev, and a reader was sent looking for a local shm path in the client that does not exist. Corrected downstream in `natsukium/felis-tui#follow-felis-105b0899`, but the felis-side contradiction is what produced it. ## Out of scope for the downstream PR Only felis can decide which of the two statements is the normative one, and the fix cascades across two explanation docs plus the #120 body — a felis doc-cascade change, not a change an ecosystem repo can make.
Author
Owner

Triage plan (2026-09-07)

Verdict: accepted, priority/P1 (docs-only, ~15 lines; the tag must not freeze an architecture doc that contradicts itself on the IPC boundary — a downstream client author has already been misled).

Verified: the "No shared memory" statement in docs/explanation/architecture/overview.md matches the code. Daemon→client pixels are ImageMsg::Chunk frames (crates/felis-daemon/src/graphics.rs, 256 KiB chunks) cloned into every window subscriber's outbound queue (serve/session_task.rs, materialize_image_eventsbroadcast), consumed from the socket in felis-client-core/src/image_shadow.rs. No SCM_RIGHTS/memfd/fd-passing path exists anywhere in crates/. The only shm in felis is the Kitty t=s producer→daemon transfer (graphics/image_decode.rs), which stops at the daemon's image store. The ipc.md sentence ("locally, image bytes already travel over shm") was false from birth (f2696d23) and #120's body repeats it.

Approach:

  1. docs/explanation/architecture/ipc.md "Open extensibility considerations": replace the shm sentence with the true statement — locally every window subscriber receives its own pixel copy over the socket (ImageMsg::Chunk), a cost that is unmeasured and bounded by image size; keep the deferral and the cross-host-first trigger. Link to overview.md#no-shared-memory. Say "unmeasured", never "marginal" (principle 2: no unmeasured optimization claims).
  2. overview.md "No shared memory": one sentence that the Kitty t=s producer→daemon transfer is the only shm in the system and ends at the image store; cross-link data-model/image-store.md.
  3. Edit #120's body (Forgejo, not git): drop the shm clause, keep the deferral, add "same-host TUI subscriber pays one socket copy per image" as the local cost to measure.
  4. doc-cascade sweep: after the edit git grep -in shm docs/ must hit only the Kitty t=s pages. No reference twin change, no CHANGELOG, no skills/felis, no schema. Fact correction, not a decision record — the why goes in the commit body.

Non-goal: this does not motivate a same-host suppression path before v0.1.0 (principle 1: nobody has shown the cost); #120 stays open unchanged in scope.

## Triage plan (2026-09-07) **Verdict:** accepted, `priority/P1` (docs-only, ~15 lines; the tag must not freeze an architecture doc that contradicts itself on the IPC boundary — a downstream client author has already been misled). **Verified:** the "No shared memory" statement in `docs/explanation/architecture/overview.md` matches the code. Daemon→client pixels are `ImageMsg::Chunk` frames (`crates/felis-daemon/src/graphics.rs`, 256 KiB chunks) cloned into every window subscriber's outbound queue (`serve/session_task.rs`, `materialize_image_events` → `broadcast`), consumed from the socket in `felis-client-core/src/image_shadow.rs`. No `SCM_RIGHTS`/memfd/fd-passing path exists anywhere in `crates/`. The only shm in felis is the Kitty `t=s` producer→daemon transfer (`graphics/image_decode.rs`), which stops at the daemon's image store. The `ipc.md` sentence ("locally, image bytes already travel over shm") was false from birth (`f2696d23`) and #120's body repeats it. **Approach:** 1. `docs/explanation/architecture/ipc.md` "Open extensibility considerations": replace the shm sentence with the true statement — locally every window subscriber receives its own pixel copy over the socket (`ImageMsg::Chunk`), a cost that is **unmeasured** and bounded by image size; keep the deferral and the cross-host-first trigger. Link to `overview.md#no-shared-memory`. Say "unmeasured", never "marginal" (principle 2: no unmeasured optimization claims). 2. `overview.md` "No shared memory": one sentence that the Kitty `t=s` producer→daemon transfer is the only shm in the system and ends at the image store; cross-link `data-model/image-store.md`. 3. Edit #120's body (Forgejo, not git): drop the shm clause, keep the deferral, add "same-host TUI subscriber pays one socket copy per image" as the local cost to measure. 4. doc-cascade sweep: after the edit `git grep -in shm docs/` must hit only the Kitty `t=s` pages. No reference twin change, no CHANGELOG, no `skills/felis`, no schema. Fact correction, not a decision record — the why goes in the commit body. **Non-goal:** this does not motivate a same-host suppression path before v0.1.0 (principle 1: nobody has shown the cost); #120 stays open unchanged in scope.
Sign in to join this conversation.
No description provided.