Broker roster rows drop exited / last_exit_code, so a tab picker cannot tell a corpse from a live shell #7

Closed
opened 2026-09-07 21:11:31 +09:00 by natsukium · 0 comments
Owner

Found by cross-checking the felis-broker.v1 JSON contract between
felis-web-gateway follow-felis-105b0899 (577138be) and this repo's
follow-felis-105b0899 (edfb8dfa).

Lower severity than the other two cross-check findings: it does not break a
live session — it makes a dead one indistinguishable from a live one.

The mismatch

felis's SessionInfo grew the post-exit grace fields
(crates/felis-protocol/src/messages/ops.rs):

    /// The shell has exited and the session is in the post-exit grace …
    /// A client's automatic pick must skip these: attaching keeps the
    /// session alive, but its shell never returns.
    pub exited: bool,
    /// Code of the youngest retained `OSC 133 ; D ; <code>` mark …
    pub last_exit_code: Option<u32>,

The gateway puts both on every roster row — src/broker.rs
session_info_json, covered by its
session_info_json_reports_the_post_exit_grace test — and docs/broker.md
states the row contract as

{host, session, rows, cols, title, cwd, idle_seconds, tags, last_notification, foreground, exited, last_exit_code}

with the reason spelled out: "a picker that does not read exited cannot tell
such a row from a working shell."

This component's decoder drops both. web/felis-broker.ts, SessionEntry and
toEntry, end at foreground — there is no exited / lastExitCode field,
so web/mobile.html's tab restore has no way to see them.

Consequence

Restoring tabs after a reload attaches to a session whose shell is gone and
shows it as an ordinary tab: the final screen paints, keystrokes go nowhere,
and the tab dies whenever the grace expires. The gateway attaches deliberately
precisely so this is possible; reading exited is how a picker labels it.

Also stale in the same type

SessionEntry.idleSeconds's doc comment still says "Seconds detached; 0 while
some client is attached." felis now returns None while attached — the
gateway forwards it as JSON null, and Some(0) means "detached for under a
second". toEntry already maps it correctly (typeof … === "number" ? … : null); only the comment is wrong, and it is the comment an app reads when
deciding what 0 means.

Fix

Add exited: boolean and lastExitCode: number | null to SessionEntry and
toEntry, correct the idleSeconds comment, and have web/mobile.html mark
an exited row rather than presenting it as a live shell.

Found by cross-checking the `felis-broker.v1` JSON contract between felis-web-gateway `follow-felis-105b0899` (577138be) and this repo's `follow-felis-105b0899` (edfb8dfa). Lower severity than the other two cross-check findings: it does not break a live session — it makes a *dead* one indistinguishable from a live one. ## The mismatch felis's `SessionInfo` grew the post-exit grace fields (`crates/felis-protocol/src/messages/ops.rs`): ```rust /// The shell has exited and the session is in the post-exit grace … /// A client's automatic pick must skip these: attaching keeps the /// session alive, but its shell never returns. pub exited: bool, /// Code of the youngest retained `OSC 133 ; D ; <code>` mark … pub last_exit_code: Option<u32>, ``` The gateway puts both on every roster row — `src/broker.rs` `session_info_json`, covered by its `session_info_json_reports_the_post_exit_grace` test — and `docs/broker.md` states the row contract as > `{host, session, rows, cols, title, cwd, idle_seconds, tags, > last_notification, foreground, exited, last_exit_code}` with the reason spelled out: "a picker that does not read `exited` cannot tell such a row from a working shell." This component's decoder drops both. `web/felis-broker.ts`, `SessionEntry` and `toEntry`, end at `foreground` — there is no `exited` / `lastExitCode` field, so `web/mobile.html`'s tab restore has no way to see them. ## Consequence Restoring tabs after a reload attaches to a session whose shell is gone and shows it as an ordinary tab: the final screen paints, keystrokes go nowhere, and the tab dies whenever the grace expires. The gateway attaches deliberately precisely so this is *possible*; reading `exited` is how a picker labels it. ## Also stale in the same type `SessionEntry.idleSeconds`'s doc comment still says "Seconds detached; 0 while some client is attached." felis now returns `None` while attached — the gateway forwards it as JSON `null`, and `Some(0)` means "detached for under a second". `toEntry` already maps it correctly (`typeof … === "number" ? … : null`); only the comment is wrong, and it is the comment an app reads when deciding what `0` means. ## Fix Add `exited: boolean` and `lastExitCode: number | null` to `SessionEntry` and `toEntry`, correct the `idleSeconds` comment, and have `web/mobile.html` mark an exited row rather than presenting it as a live shell.
Sign in to join this conversation.
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-web-component#7
No description provided.