fix(tui): hide runtime-spawned agent hosts from the Sessions list - #2035
Merged
Conversation
#1964 put the render prohibition behind the navigation gate, so opening a checkpoint-writer host is refused — but the Sessions dialog still LISTED one `↳ checkpoint-writer: …` row per checkpoint. Those are two separate paths and the gate cannot stand in for the list. The leak: sync.sync() fetches children with `visible: true`, but the `session.updated` arm in sync.tsx inserts EVERY session it sees into the store, and checkpoint.ts creates the writer host with its title already set — before the actor row is registered — so it arrives on that path with a display-ready title and `isChildOfCurrent` passed it straight through. Filter the child arm through classifySession, the same predicate the gate uses, so the list cannot disagree with what opening the entry would do. Fails open (no actor rows ⇒ listed), which is what keeps orchestrator `session create` children listed: they own a mode "peer" row and classify renderable outright.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
In the TUI's Sessions dialog, every checkpoint adds one more internal-machinery session, with a title of the form:
The user's feedback was "this was supposedly already fixed, why is it still here".
Relationship to #1964: two paths, and the guard cannot substitute for the list
What #1964 fixed was the navigation guard —
routes/session/index.tsxusesverifySessionRenderableto refuse a machinery host before rendering, so opening one gets rejected. But the list itself was never filtered, so those hosts kept getting listed. These are two independent paths, and the former cannot substitute for the latter.The file comment in
visibility.tsin fact already spells out this distinction:The server-side capability is complete as well:
Session.children(parentID, { visible: true })filters by the peer actor row,GET /session/:id/children?visible=exposes it, andsync.sync()does fetch child sessions withvisible: true.The leak path is elsewhere: the
session.updatedarm insync.tsxunconditionally inserts any session it receives intostore.session, bypassing thatvisible: truefetch. And whensession/checkpoint.tscreates the writer host, the title is already set (title: "checkpoint-writer: ${rangeDesc}"), and this happens before the actor row is registered — so it enters the store viasession.updatedcarrying a directly displayable title, and is waved straight through by the filter indialog-session-list.tsx, which only looks at the parent/child relationship:Not just a noisy list: it pollutes the orchestrator's session inventory
These hosts are registered as real child sessions. Measured on one orchestrator fleet: 13 of 19 child sessions were
checkpoint-writer, all with a turn count of 0. So the impact goes beyond a dirty TUI list — the orchestrator's own session inventory is drowned in these empty shells.The change
The child-session arm gains a
listable(x)check, using the very sameclassifySessionpredicate as the guard:Why reuse
classifySessioninstead of writing a new rule: with the list and the guard sharing one predicate, the self-contradictory "it's in the list but opening it is refused" state becomes impossible. The rule has exactly one home.The actor row is taken from the sync store rather than from an additional network request, because the two populations line up exactly: a host can only be in the store if it was created within this TUI lifecycle (bootstrap loads only roots, and
sync.sync()loads child sessions withvisible: true), and that same lifecycle also delivers itsactor.registeredevent — so for exactly the set that can leak, the row read here necessarily exists.The fail-open choice
classifySessionjudges a session displayable when there is no actor row. This is deliberate, and it is the reason #1964 narrowed the rule down frommode !== "peer"in the first place: the old fail-closed proxy over-blocked 28 sessions (11ask:forks + 17 pre-registry@explore/@generaltranscripts), none of which were machinery. Over-hiding real sessions is the more expensive mistake.The cost is that a writer host that was just created and has not yet registered its actor row shows up briefly once; it has zero messages and disappears a moment later. Compared to the current "one permanent row per checkpoint" that is an order-of-magnitude improvement, and if you do click into it, the guard still refuses.
Regression risk and the test that holds it
The biggest risk is collateral damage to legitimate child sessions. Child sessions the orchestrator creates via
session create(actor/spawn.ts,mode: "peer", including the ones prefixed with[topic:…]) must remain visible. They hold amode: "peer"row and are judged renderable directly by the peer arm ofclassifySession, so this filter cannot catch them by association.Adds
test/cli/tui/session-list-visibility.test.ts(4 cases), built from real sessions plus real actor rows, pinning:[topic:memory-switch] memory 开关方案调研andbuild: 在 mimocode 引擎侧实现「memory 写入开关」— asserting they must be renderable, while the writer host must be refused.[topic:…]title → still hidden. This case exists specifically to prevent someone later taking the shortcut of filtering by title prefix — under which a user session that happens to be namedcheckpoint-writer: …would vanish into thin air.listable(x), and that no"checkpoint-writerliteral and nostartsWith(appears in the code.Verification
Baseline
8061d5fa0(latest main).Mutation testing (proving the regression pin actually fires): reverting the filter expression to its original form and re-running →
3 pass, 1 fail, reportingThat was subsequently restored from git, and
git statusis clean.Follow-up notes (unrelated to this PR, but reviewers should know)
The user hit this on
npm exec @mimo-ai/cli@latest, which at the time gave them 0.1.9 (released 2026-07-24), whereas #1964 landed on 2026-07-31 — so that binary did not even have the navigation guard, which explains the "it was fixed but it's still here".0.1.10 was released today (
2026-08-05T09:17:36Z, version bump8061d5fa0 chore: bump version to 0.1.10 (#2034)), and85a7faca4(#1964) is confirmed to be one of its ancestors. So 0.1.10 does carry the navigation guard and only lacks this PR's list filter: after upgrading to 0.1.10 the user will be refused when opening one, but they will still be listed until this PR reaches the next release.