Repository navigation
feat(dash): confirm before quitting while a model is still serving - #595
Conversation
|
review-coverage · cde16ef Full-scope high-effort review (fresh Claude Opus context, able to fan out) ran three rounds against this change: round 1 (8 findings, all fixed), round 2 re-reviewed the fix and found 4 more items (1 real bug, 1 stale doc reference, 1 test gap, 1 duplication tradeoff — all fixed/resolved), round 3 re-reviewed that and found 1 non-blocking mechanical duplication (fixed) with 0 blocking findings otherwise. The final mechanical fix (extracting a shared |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved quit-confirmation behavior and regression-coverage gaps need correction.
4 open findings
What changed in this PR
Adds quit confirmation to the dashboard, chat, and launcher when models are serving, while leaving those services running after exit.
Changes:
- Shares quit gating and approval-style prompt handling.
- Integrates confirmation with input routing and footer state.
- Adds regression coverage, TUI-driver support, and documentation.
Validation was limited to source inspection; tests were not run.
| File | Description |
|---|---|
| tests/e2e-cucumber/tests/e2e/tui_driver.rs | Handles confirmation during test teardown. |
| tests/e2e-cucumber/tests/e2e/dash_steps.rs | Adds confirmation and service-check steps. |
| tests/e2e-cucumber/features/dash.feature | Adds dashboard and launcher scenarios. |
| README.md | Documents confirmation behavior. |
| crates/rocm-dash-tui/src/ui/tabs/pane.rs | Reuses overflow-list formatting. |
| crates/rocm-dash-tui/src/ui/tabs/instances.rs | Updates test state initialization. |
| crates/rocm-dash-tui/src/ui/mod.rs | Renders confirmation and updates footer handling. |
| crates/rocm-dash-tui/src/ui/launcher.rs | Adds launcher confirmation handling and tests. |
| crates/rocm-dash-tui/src/ui/format.rs | Adds shared overflow formatting. |
| crates/rocm-dash-tui/src/app/slash.rs | Gates chat quit commands. |
| crates/rocm-dash-tui/src/app/scrollbar.rs | Suppresses mouse interactions during confirmation. |
| crates/rocm-dash-tui/src/app/mod.rs | Adds confirmation state and resolution methods. |
| crates/rocm-dash-tui/src/app/event_loop.rs | Routes keys to pending confirmation. |
| crates/rocm-dash-tui/src/app/actions.rs | Centralizes quit gating and adds tests. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
|
review-coverage · 77414fb Three full-scope closing reviews (fresh Claude Opus 4 context each, fanning out, different model from the fixer) run back-to-back against Copilot's automated review and its own fallout, closing out all four of its findings plus what each pass itself turned up:
All 25 CI checks green on 77414fb (Skill checks/skillscope SKIPPED, advisory only). The four original Copilot inline comments have been replied to individually with the specific fixing commit and resolved. |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 77414fb
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
First automated round on this PR: every finding below is new.
Review — needs work
Full review of the whole change.
The description says "Fixes #145", but #145 asks for a way to stop a served model. This PR adds a warning before quitting, not a way to stop (see Blocking).
Blocking
-
--demo/--replaynow open the quit prompt for simulated instances, and its text is false there —crates/rocm-dash-tui/src/app/mod.rs:630,crates/rocm-dash-tui/src/app/actions.rs:68,crates/rocm-dash-tui/src/ui/mod.rs:235-236- Evidence:
has_live_instancenever checksstate.simulated. That field exists "so simulated data can never be presented as live".- Demo snapshots carry
InstanceStatus::Running(rocm-dash-daemon/src/demo.rs:615). The PR's own driver comment confirms the effect (tui_driver.rs:674: "Demo data … opens the quit-confirm prompt"). - So on every
qin demo mode the prompt says "It keeps running in the background after you quit. Stop it first: rocm services stop <id> --yes". Nothing is running. - This breaks the AGENTS.md §3 rule that a printed claim must be true of the behaviour. It also contradicts the description's own scope: "when at least one managed instance is actively serving".
- Confidence 85 · logic · Fix: return
falsefrom the gate whenstate.simulatedis set, and add a unit test fed a demo snapshot.
- Evidence:
-
The stop advice in the prompt can't be followed, and nothing checks it —
crates/rocm-dash-tui/src/ui/mod.rs:228,236- Evidence:
- The body lists only
model_name. It then printsrocm services stop <id> --yes, but<id>is the service id (container_id), which the prompt never shows. - Nothing tests that the command clears the condition. AGENTS.md §3 requires that for remediation advice that names a command: "the named command must exist, accept those flags, and actually clear the condition that printed it".
instancescan also hold Docker- or Lemonade-discovered entries when connected to an external daemon.rocm services stopcannot stop those.
- The body lists only
- Confidence 85 · logic · Fix:
- Print each service's id next to its model.
- Print the stop line only for registry-managed instances.
- Add a test that runs the printed command, or a unit-level equivalent, and shows that the next
qexits without a prompt.
- Evidence:
-
New behaviour that no test reaches —
crates/rocm-dash-tui/src/app/event_loop.rs:810,crates/rocm-dash-tui/src/ui/mod.rs:227-232- Evidence: I applied both mutations together on a clean tree and ran
cargo test -p rocm-dash-tui --lib. The result was 840 passed and 3 failed. The 3 failures are the pre-existingagent::clients::tests::*, which workers saw fail unmutated as well. The two mutations were:- Replace the
args.startup_has_live_service → statehand-off with a no-op. - Remove the
• {model}bullet push from the prompt body.
- Replace the
- So the startup-race fix itself is untested. Each half is tested (
dash.rsproduces the flag,has_live_instanceconsumes it), but not the wiring between them. - dash-25 waits for "the managed model is displayed" before pressing
q, so the first snapshot has already retired the fallback by then. The model listing and the "…and N more" overflow (worker mutation: cap 5→500 stays green) are likewise never asserted. - Confidence 100 · mechanical · Fix:
- Move the
args→stateseeding into a helper and test it, asapply_startup_focusalready is. - Add a body test with 6 serving instances that asserts 5 bullets plus "…and 1 more".
- Move the
- Evidence: I applied both mutations together on a clean tree and ran
-
declining_quit_from_the_esc_menu_returns_to_the_dashboard_not_the_menudoes not test declining —crates/rocm-dash-tui/src/app/actions.rs:901- Evidence:
- The test calls
on_quit_confirm_key('n'), then clears the prompt itself withs.quit_confirm = None. It never goes throughresolve_quit_confirm_key, which is what both event loops call. - I mutated the Deny/Cancel arm so it no longer clears
quit_confirm. This test stayed green (25 passed, 1 failed). Onlylauncher_quit_confirm_approve_exits_deny_and_cancel_stayfailed. - Its
modal == Noneassertion repeats the previous test. quit_confirm_key_approves_denies_and_cancelslikewise only exercises theon_quit_confirm_keypassthrough, never the verdict's side effect.
- The test calls
- Confidence 100 · mechanical · Fix: drive both tests through
resolve_quit_confirm_keyand assertSome(false)andquit_confirm.is_none().
- Evidence:
-
The description claims to close #145, but this change doesn't close it — PR description, "Fixes #145"
- Evidence:
- The issue reads: "After serving a model, the process keeps running when I exit
rocm. I don't see any obvious way to stop the model serving service." - This change warns before quitting and leaves the model running. It adds no stop action, and the hint it prints lacks the id (see above).
- The issue reads: "After serving a model, the process keeps running when I exit
- Confidence 70 · architectural · Fix: either change "Fixes" to "Refs", or finish the work by offering a stop option from the prompt and making the hint usable.
- Evidence:
Non-blocking
-
Leaving a dashboard opened from bare
rocmasks "quit anyway?" and then asks again at the launcher —apps/rocm/src/dash.rs:635,crates/rocm-dash-tui/src/app/actions.rs:68- The launcher
loopre-entersrun_launcherafterrun(None, ..)returns. Sod→q→ylands on the launcher, whereqprompts a second time. - The first prompt says "quit" and "after you quit", but the user is only going back to the menu.
- dash-25 and dash-26 test
rocm dashand the bare launcher separately, never this combined path. - Confidence 85 · logic · Fix: pass a "returns to launcher" flag through
ResolvedArgs, and either skip the gate or reword the prompt in that case.
- The launcher
-
Native Windows: the dashboard gate reflects only the state at launch —
crates/rocm-dash-tui/src/client.rs:181-187,crates/rocm-dash-tui/src/app/mod.rs:630- The Windows
connect_and_runalways errors, sohas_received_snapshotnever flips. - A model started in-session (serve wizard) quits with no prompt. One stopped in-session still prompts.
- The README's "If a model is still being served when you press
q, it asks first" doesn't hold there. Both scenarios are@requires-os:linux. - Confidence 85 · logic · Fix: re-read the registry at quit time on that platform (a seam like
tool_executor), or document the limitation.
- The Windows
-
startup_has_live_serviceduplicates registry classification that is already public, and its doc comment says otherwise —apps/rocm/src/dash.rs:303-348- The doc justifies the copy with "
instance_status_for_record… notpub, so re-derived here". - But
registry::discover_managed_servicesispuband already applies the port-0 filter and the status/phase mapping. The same file already uses it, inservices_past_attemptsover the samerecordsand inlauncher_serving_instances, the launcher half of this gate. - The dashboard gate and the launcher gate therefore classify "serving" through two copies. A worker's mutation dropping
.trim().to_ascii_lowercase()kept all 9resolved_argstests green. - Confidence 85 · architectural · Fix:
discover_managed_services(records).svcs.iter().any(|s| s.status.is_serving()). Delete the hand-written match and the paragraph.
- The doc justifies the copy with "
-
The prompt title can contradict its body —
crates/rocm-dash-tui/src/ui/mod.rs:185-186against:211-213- The title is always "Still serving — quit anyway?". The
total == 0 && !startup_fallbackbranch returns "Nothing is being served anymore." under it. - The code's own comment says that state is reachable: the model stops while the prompt is open.
launcher_renders_the_quit_confirm_prompt_when_openrenders exactly this state and only checks for "Still serving".- Confidence 85 · logic · Fix: derive the title from the same decision as the body, or close the prompt when
has_live_instance()turns false. Assert title and body together.
- The title is always "Still serving — quit anyway?". The
-
quit_and_waitnow answersyto any quit prompt it sees, in every scenario —tests/e2e-cucumber/tests/e2e/tui_driver.rs:688- A regression that opened the prompt with nothing serving would stay green across all dash/chat scenarios.
- "With nothing being served,
qexits immediately with no prompt" is user-observable, but only unit tests cover it. - The confirm half of dash-25/26 is also hidden inside the generic "the user quits …" step, so the Gherkin never shows it.
- Confidence 85 · mechanical · Fix: add an explicit
When the user confirms the quit promptstep, and havequit_and_waitfail if a prompt appears that the scenario did not opt into.
-
Docs claims that don't match the code
docs/manual-testing.md:316: declining "returns to exactly where you were".open_quit_confirmdeliberately closes any modal and overlay, and tests pin that (actions.rs:839,:901). Say "returns to the dashboard".README.md:165-166: "Ctrl-Cis the one gesture that always quits immediately, even then". Two sentences earlier, the README says Ctrl-C cancels a running job instead of quitting. "The same as today" will also go stale once merged.- Confidence 85 · mechanical · Fix: reword both.
-
Comments that are stale or point at reasoning that isn't there
app/scrollbar.rs:62-64says the gate lives in "scrollbar_hit's ownblocks_body_absolutely()check". The check is actually theDownbranch's early return.ui/mod.rs:169-172points at "quit_confirm's doc comment onAppState" for the claim that the approval and quit-confirm modals are never open together. That doc doesn't say it. The guarantee comes from theopen_approvalguard.app/mod.rs:270-273lists only "q, or/quit//exit". It omits the Esc-menu Quit row and the launcher.apps/rocm/src/dash.rsready-test doc says its sibling "pairsrunningwith other records". It writes one record.dash_steps.rs:581-586says the next assertion is the same "in both scenarios". It isn't the same in dash-26.- Review-round narration ("this round's other refactors", "a prior round's fix … missed this one", "Closing-review regression") won't mean anything after the squash-merge.
- Confidence 85 · mechanical · Fix: correct the pointers and drop the narration.
Decisions for the author
- Enter confirms quitting by default — tradeoff
open_quit_confirmusesApprovalChoice::default()(Approve). That fits "the user already asked to quit".- The other side: a prompt that exists to stop an accidental quit can be passed by a reflexive Enter.
- No scenario presses Enter here. #358 required a scenario for the chat approval's default-plus-Enter outcome.
- Tab-bar and footer-chip clicks are now swallowed during the chat approval as well — tradeoff
blocks_body_absolutely()reverses #358's documented "Tab-bar and footer-chip clicks above still work" for the approval modal. A new test pins the new behaviour.- This is consistent with the quit prompt, but it is a behaviour change to a different modal. The test plan mentions it in passing.
- Check liveness when the user quits rather than relying on snapshot or disk state — non-blocking-improvement
- A registry read at quit time would remove both the startup-race flag machinery and the Windows gap in one stroke, at the cost of a synchronous file read on
q.
- A registry read at quit time would remove both the startup-race flag machinery and the Windows gap in one stroke, at the cost of a synchronous file read on
Positive signals
- One
request_quitgate serves every quit entry point:q, the Esc-menu Quit row,/quit//exit, the footer chip, and the launcher'sq/Esc. No path bypasses it except the intentional Ctrl-C. resolve_quit_confirm_keyis shared by the dashboard event loop and the launcher, so what a verdict does can't drift between them.- It reuses
ui/approval.rs(draw_approval/approval_key) instead of a new modal, as AGENTS.md asks. open_approvalrefuses while the quit prompt is open. Otherwise an approval arriving asynchronously would capture aymeant to confirm the quit and approve an unreviewed tool call.decline_quit_promptwaits for the prompt to leave the screen. So the next "model is displayed" check can't pass on the prompt's own text.managed_model_still_listed_as_runningparses the JSONstatusfield rather than substring-matching--all, which also lists stopped records.
Deployment notes
None
What this covered
- What was read: every file in the change, completely,
prw-base(7f72556)…77414fb5. Also the callers and siblings:rocm-dash-daemon/src/registry.rsanddemo.rsclient.rs- the launcher hub loop in
apps/rocm/src/dash.rs ui/approval.rs
- How the work was split: 4 independent read-only workers. One for the state/gate logic plus
dash.rs, one for the UI/mouse/launcher files, one for the e2e/docs files, and one for the source-reading passes (agent-instruction adherence, history, prior PRs #358 and others, code comments). - Tests run:
- CI rollup for 77414fb: all checks green. Skill checks were skipped, which is not relevant to this change.
- A worker ran e2e dash-03/25/26: 3 of 3 passed. The full
dash-*run passed 25 of 26; dash-24 needs a WSL GPU and is unrelated. feature_naming: 4 of 4 passed.
- Mutation evidence: the workers' mutation runs overlapped in this shared checkout, and at least one worker reset others' files mid-run. Every mutation result cited above as decisive was therefore re-run by me, sequentially, on a clean tree.
- Did not run: Step 7's design questions were answered from the fan-out workers' evidence and my own reading, not by separate design workers. There was no hardware check on native Windows; that finding is traced from
client.rs, not run. - Not reconciled here: this report has not been checked against the PR's discussion or its earlier reviews.
|
Thanks for the thorough pass. Responding to each item (commits since this review: Blocking — all five addressed
Non-blockingFixed: the Not addressed, left open:
Decisions for the author
Full verification ( |
|
review-coverage · a67a936 Two full-scope closing reviews (fresh Claude Opus context each, fanning out, different model from the fixer) ran back-to-back against this review's five blocking + six non-blocking findings and their own fallout: round 1 (commit Left open by design, not by oversight: the bare- |
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 37ed114
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
Review — needs work
Full review of the whole change (prw-base...HEAD, 16 commits, 17 files).
Fixes #145 ("Stop serving a model"). The description's scope matches the diff. It discloses that managed_service_ids is a launch-time snapshot and that the approval modal now also swallows tab-bar and footer-chip clicks.
Blocking
-
The
startup_has_live_servicehand-off intoAppStateis covered by no test, and the test that claims to cover the wiring does not —crates/rocm-dash-tui/src/app/event_loop.rs:826-830,:229-248,:2186-2206seed_startup_live_service_wires_both_fields_onto_statecalls the helper directly. Its comment says it guards against "the call site deleted, or the field names swapped", and the doc onseed_startup_live_servicemakes the same claim.- I ran two mutations: deleting the call site, and passing a hard-coded
falsein place ofargs.startup_has_live_service.cargo test -p rocm-dash-tui --libstayed at 854 passed both times. - Scenario dash-25 does not reach the startup-race half either. A real snapshot has landed before
qis pressed, sostartup_has_live_serviceis retired by then. Only themanaged_service_idshalf has any end-to-end coverage, in the CI-run scenario. - The description's test plan presents the fallback as covered through this helper.
- Confidence 100 · logic · Fix: move the whole
ResolvedArgstoAppStateseeding into one function that takes&ResolvedArgsand test that, so the call site cannot regress unseen. Narrow the comment either way.
-
The
is_serving()filter inhas_live_instanceis untested —crates/rocm-dash-tui/src/app/mod.rs:650- Replacing
self.instances.values().any(|i| i.status.is_serving())with!self.instances.is_empty()leaves all 854 unit tests green. No test seeds an instance whose status is Starting, Stopped, Error or Unknown. The only "no prompt" test uses an empty map. - This filter is the rule that decides when the prompt appears, and the PR introduces it.
- The same gap applies to the
is_serving()filters inhas_stoppable_managed_instance(app/mod.rs:654-660) andquit_confirm_open_services_key. Every seeded instance in those tests is Running. - Confidence 100 · logic · Fix: add a test where a non-serving instance, including a managed one, neither opens the prompt nor offers or honours
s.
- Replacing
-
Two added tests cannot fail on the code they name —
crates/rocm-dash-tui/src/ui/mod.rs:1056,:1213-1227- In
quit_confirm_body_says_nothing_is_being_served_once_a_snapshot_confirms_it, the assertion!contains("Stop it first")checks a string the code never produces. The code writes lowercase"… — stop it first:"(ui/mod.rs:294). That state also returns early with a single line, and no managed ids are seeded. The assertion's message claims it guards against pointing at a stop command for a service that isn't running. quit_confirm_open_services_key_ignores_other_keys_and_a_closed_promptseeds no instances. Deleting the!quit_confirm_pending() || code != Char('s')guard entirely leaves the suite at 854 passed, because the later "no stoppable row" path returnsfalseanyway. The test claims to cover both guards.- Confidence 100 (the guard test by mutation) and 85 (the string, by reading) · mechanical · Fix: seed a stoppable managed instance so each guard is the only thing preventing the jump, and assert the lowercase string in a state where the body prints a stop line.
- In
-
The two decisions
offer_stop_actioncontrols are untested at both call sites —crates/rocm-dash-tui/src/ui/mod.rs:179(dashboard passestrue),crates/rocm-dash-tui/src/ui/launcher.rs:163(launcher passesfalse)- "The launcher must not advertise
s" is stated in the description, the README and the manual-testing doc. - I flipped each argument alone, and both runs stayed at 854 passed.
launcher_renders_the_quit_confirm_prompt_when_openseeds an instance with an emptycontainer_idand no managed ids, so it passes with either value. Nothing rendersui::drawwithquit_confirmopen.- Neither scenario asserts the "Press s to open Services" line, or its absence on the launcher.
- Confidence 100 · logic · Fix: assert the hint is present in a rendered dashboard prompt and absent in a rendered launcher prompt, using a seeded managed instance.
- "The launcher must not advertise
-
User-observable quit paths this PR changes have no Gherkin scenario, and the description does not argue why —
crates/rocm-dash-tui/src/app/slash.rs:55,crates/rocm-dash-tui/src/app/actions.rs:279,tests/e2e-cucumber/features/dash.feature:333-371- AGENTS.md §3 says a unit test asserting an internal helper does not discharge a scenario for changed CLI behaviour.
- dash-25 and dash-26 only press
q./quitand/exitin chat (the description calls outrocm chat) and the Esc-menu Quit row are separate code paths with no scenario. The same goes for the launcher's Esc and forCtrl-Cwith a model served, which the README now documents. - chat-03 and chat-04 go through
quit_and_wait, which sendsyonly if the prompt is on screen. With theslash.rsgate reverted,/quitexits at once and those scenarios still pass. - Confidence 85 · architectural · Fix: add a chat scenario that asserts the prompt on
/quitand the model still running afterwards. For the other paths, add scenarios or say in the PR text, per path, why unit coverage is enough.
Non-blocking
-
The staleness justification for
managed_service_idsis wrong, and the user docs do not mention the limit —crates/rocm-dash-tui/src/app/types.rs:121-127,app/mod.rs:208,README.md:159-173,docs/manual-testing.md- The comment says the id only changes "when the user runs
rocm serveagain, which this process does not observe". The dashboard's own Serve wizard runsrocm serve --managedas a job in this process (ui/serve_wizard.rs:190-218). The embedded daemon re-reads the registry each tick (rocm-dash-daemon/src/runner.rs:649-653), so the instance appears ininstancesbut never inmanaged_service_ids. - For a model started that way, the prompt shows no stop command and no
shint. The PR description discloses this, but the README says without qualification thatsjumps to the serving model. - Which side governs: the code. Correct the comment and add the qualification to the README and manual-testing text. See the decision below for whether to refresh the set instead.
- Confidence 85 · mechanical.
- The comment says the id only changes "when the user runs
-
A step comment claims the whitespace collapse reassembles a wrapped stop command, but a later commit made the opposite point —
tests/e2e-cucumber/tests/e2e/dash_steps.rs:553-566,crates/rocm-dash-tui/src/ui/mod.rs:283-293- The step comment says collapsing whitespace "reassembles the original command losslessly". The
ui/mod.rscomment (commit c30f00d) says a wrap lands the second half next to unrelated text outside the popup, which is why the command was moved onto its own line. - The
ui/mod.rscomment also says the command "never needs to wrap". Real ids come fromgenerate_service_id(rocm-core/src/lib.rs:4523) as{engine}-{slug up to 24 chars}-{13-digit ms}, so a typical command is well over 60 columns. Only the fixture ide2e-mockis short. I did not render a real-length id. - Which side governs: the layout facts in
ui/mod.rs. Fix the step comment, and soften the production one. - Confidence 70 · mechanical.
- The step comment says collapsing whitespace "reassembles the original command losslessly". The
-
The quit-gate comment about demo data is false —
tests/e2e-cucumber/tests/e2e/tui_driver.rs:674- It says "Demo data and several fixtures leave a model serving, which opens the quit-confirm prompt".
has_live_instancereturnsfalseundersimulated(app/mod.rs:647), which--demoand--replayset. The PR description says the same, so the comment contradicts it. Only the registry-backed fixtures prompt. Commit a6c7151's body repeats the claim. - Confidence 85 · mechanical · Fix: drop "Demo data and".
- It says "Demo data and several fixtures leave a model serving, which opens the quit-confirm prompt".
-
managed_model_still_listed_as_runningcannot fail on any branch of this change —tests/e2e-cucumber/tests/e2e/dash_steps.rs:680-709,dash.feature:353,371- The step's comment says it proves the prompt's "keeps running" claim against actual state. The fixture record is static JSON with no supervisor process, and no production path stops a service on quit. Dash-25 and dash-26 fail at the earlier prompt steps if the gate is reverted, so this step never decides the outcome.
- It is a legitimate guard for unchanged behaviour. Say that rather than claiming it proves the message.
- Confidence 70 · mechanical.
-
The stop-prompt body comments say "managed instance" but the gate counts every serving instance —
app/mod.rs:277(thequit_confirmfield doc) and:627-628(thehas_live_instancedoc)- The code is
instances.values().any(is_serving), which includes Docker- and Lemonade-discovered entries. Unmanaged instances are meant to prompt, perquit_confirm_body_omits_the_stop_command_for_an_unmanaged_instance. The code governs. - Confidence 85 · mechanical.
- The code is
-
A third copy of the "is anything serving" predicate —
ui/mod.rs:224-232nothing_is_currently_being_servedis!has_live_instance()for every state in which the prompt can open. The PR repoints the launcher'sis_runningathas_live_instanceto stop exactly this kind of drift, then adds another independent copy.- Confidence 70 · mechanical.
-
overflow_line's doc overclaims —ui/format.rs:340-349- It says sharing the helper means a cap bump in one place "can't quietly leave another's overflow arithmetic behind". The cap is still a separate literal at each call site, so changing
take(5)withoutoverflow_line(_, 5)diverges. A shared constant per list would deliver the claim. - Confidence 70 · mechanical.
- It says sharing the helper means a cap bump in one place "can't quietly leave another's overflow arithmetic behind". The cap is still a separate literal at each call site, so changing
-
launcher_quit_confirm_approve_exits_deny_and_cancel_stayis named for three verdicts but exercises two —ui/launcher.rs:896- Only
nandyare driven. Cancel (Esc/q) and theNone(cursor move) arm are not, so changingSome(false) | None => Continueso thatNoneexits stays green. - Confidence 70 · mechanical.
- Only
-
Comments narrate this PR's own earlier revisions as history on
main—apps/rocm/src/dash.rs:259-263("three walks … once instead of three times"; there was one walk onprw-base),ui/mod.rs:192("the way they used to"),app/scrollbar.rs:105-110("once let a tab-bar click still land"),app/actions.rs:840-844and:902-906("used to"). Readers will look for a history that is not there.- The
Regression:/ "used to" style in test comments matches existing tests and is fine. Production comments should describe the current behaviour. - Confidence 70 · mechanical.
- The
-
Commit history carries noise, and one message names internal tooling —
92c73a96,a33dfe6e, and the fix-up trail92c73a96is an empty "chore: empty commit to re-trigger GitHub's PR sync".- About six "close review gaps" and "closing review" commits sit on one logical feature, plus
f65d44ed(fmt) and1de1db7f(harden assertions) that belong in the commits they fix. - The body of
a33dfe6enames the "pr-review-watcher" tool, which does not belong in public history (AGENTS.md §2). - All commits carry a
Signed-off-bytrailer. I did not verify signatures cryptographically. Fix: squash or fixup before merge and reword that body. - Confidence 85 · mechanical.
Decisions for the author
- Launch-time
managed_service_idssnapshot — tradeoff- Keeping the snapshot is cheap, and the description discloses the gap. The cost is that a model served from the dashboard's own Serve wizard, likely the most common path for a dashboard user, gets a warning with no stop command and no
s. - Refreshing the set when the prompt opens, or on each snapshot, fixes that at the cost of a registry read or a daemon-side flag. The issue's ask is a way to stop a served model, so decide whether the first path meets it.
- Keeping the snapshot is cheap, and the description discloses the gap. The cost is that a model served from the dashboard's own Serve wizard, likely the most common path for a dashboard user, gets a warning with no stop command and no
- Quit prompt defaults to Approve, unlike the chat tool-call approval's Deny — tradeoff
- The prompt reuses the shared widget, so
qand Esc cancel. A doubleqdeclines the quit, and Enter on the default button quits. - The default is documented, and the user asked to quit, so Approve is defensible. Nothing tests Enter against the default.
- The prompt reuses the shared widget, so
- Launcher to dashboard to quit now asks twice — tradeoff
- In the hub (
apps/rocm/src/dash.rs:617-631), quitting the dashboard returns to the launcher loop, which gates its ownq. A user with a model serving confirms once per layer. That is consistent with the gate being per entry point, but it is a visible change.
- In the hub (
overlay_or_approval()is now misnamed, and the second operand of its use atscrollbar.rs:137is unreachable — non-blocking-improvement- After the early
blocks_body_absolutely()return it reduces tohas_open_overlay(). The comment says the indirection is deliberate. A rename would remove the need for that comment.
- After the early
- The approval modal now swallows tab-bar and footer-chip clicks — tradeoff
- The earlier comment said those clicks "still work" during an approval, and the description discloses this change. It is consistent with keyboard behaviour, where those controls are already inert.
tui_driver.rshas no mouse support, so only unit tests cover it, which the PR text could state.
- The earlier comment said those clicks "still work" during an approval, and the description discloses this change. It is consistent with keyboard behaviour, where those controls are already inert.
Positive signals
- Routing every quit entry point through one
request_quitgate, withresolve_quit_confirm_keyshared between the dashboard loop and the launcher, avoids the two loops drifting apart. - Replacing the hand-rolled startup classification with
discover_managed_services, and buildingManagedDiscoveryonce indash.rs, keeps the pre-launch read in step with the daemon's own rules. Theno bound portandstartup_phasetests pin that. open_approvalrefuses while the quit prompt is open. Without it aymeant to confirm the quit could approve an unreviewed mutating tool call, and the test covers it.dash-25matchese2e-mock, the service id, rather than the model name, to prove the Services overlay opened. The model name was already visible behind the dimmed modal.
Deployment notes
None
What this covered
- All 17 changed files were read in full, with their diffs and the surrounding code. That included
ui/approval.rs,ui/services_manager.rs,ui/serve_wizard.rs, the daemonregistry.rsandrunner.rs,rocm-core'sgenerate_service_id, the mock server fixture, and AGENTS.md. - The review was split as follows:
- App state and routing (
app/*). - UI, launcher and
apps/rocm(ui/mod.rs,ui/launcher.rs,ui/format.rs,tabs/*,dash.rs). - E2E steps, feature file and docs.
- The source-reading passes: agent-instruction adherence, history, prior changes, code comments.
- App state and routing (
- Reviewed against
prw-base...HEADat 37ed114. - I ran
cargo test -p rocm-dash-tui --libunmodified (854 passed) and six single-line mutations, each reverted afterwards:- Delete the
seed_startup_live_servicecall. - Hard-code
falsefor the startup flag. - Drop the
is_servingfilter. - Delete the
s-key guards. - Flip the dashboard's
offer_stop_actionargument. - Flip the launcher's
offer_stop_actionargument.
- Delete the
- Not run:
- The cucumber scenarios (dash-25 and dash-26 need a pty and the repo's runners). I took CI's reported state as the signal. Every check completed SUCCESS except "E2E tests (MI300X)", which was QUEUED, and "Skill checks (skillscope)", which was SKIPPED. Those two did not run and are not counted as passes.
cargo clippy, the full workspace test run, andscripts/smoke_local.py.- Windows behaviour. Both scenarios are
@requires-os:linux, like every scenario indash.feature, and the file header documents why.
- The pull request description and issue #145 were read after the findings were settled. Earlier review rounds and discussion were not read, so nothing here is reconciled against them. The author of the fixes is not the reviewer.
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 37ed114
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
Round 2 (round 1 was the change request at 77414fb). Each finding is marked [new] or [standing]. This round reviewed the whole change blind, then put each round-1 blocker it did not re-find back to the same reviewer, one at a time, together with the author's reply. Those verdicts are anchored evidence from reading the code; nothing was run (disk was below the build floor).
Round-1 blockers, settled:
- Demo/replay opens the prompt: resolved.
has_live_instancereturns false undersimulated(app/mod.rs:647), andsimulatedis set before the first key (event_loop.rs:839). The launcher never sees simulated data. - Decline tests bypass the real path: resolved. Both tests now go through
resolve_quit_confirm_keyand assertquit_confirm.is_none()(actions.rs:923-925,:946-951). - Stop command needs an id the prompt never shows: the id is now printed, and only for registry-managed instances. Whether the printed command actually clears the condition is still untested. This round re-found that independently; see Blocking.
- Untested startup-race wiring and model list:
- The wiring half was re-found independently; see Blocking.
- The list cap and the "…and N more" line are now covered (
ui/mod.rs:1285). - Still uncovered: the managed-instance bullet
"• {} — stop it first:"(ui/mod.rs:294). Its only unit test (:1230) sets a model name and never asserts it. The other assertion on that wording is the case-mismatched one below. [standing]
- "Fixes #145": partly holds.
rocm services stop <real-id>on the launcher and dashboard, pluss→ Services on the dashboard, now give a way to stop a model that was serving when the session started.- A model served from the dashboard's own Serve wizard still gets only the warning (stoppable ids are frozen at launch,
ui/mod.rs:282). On Windows it gets no prompt at all. - "Fixes #145" in the PR body closes the issue on merge.
- To resolve: change it to "Refs #145" and track the mid-session gap separately, or re-read the registry in
request_quit, which closes both gaps. [standing, narrowed]
- A model served from the dashboard's own Serve wizard still gets only the warning (stoppable ids are frozen at launch,
What this change request gates on: the standing items above, the remediation-command test, and the two checks that cannot fail, below. The launcher → dashboard double prompt was rated non-blocking in round 1, and nothing new has been found about it. So although the method lists it under Blocking this round, it does not count toward this change request; it remains the author's call.
Review — needs work
Full review of the whole change.
Says it fixes #145 ("Stop serving a model": the model keeps running after you exit rocm, with no obvious way to stop it). It warns at quit time and makes the existing stop path easier to find. The description scopes out the stop affordance for models served mid-session, and that part is retired under Decisions for the author.
Blocking
-
[standing: non-blocking in round 1, not counted toward the gate] Opening the dashboard (or Chat) from the launcher and quitting it asks "quit anyway?", but
ygoes back to the launcher, which then asks again —apps/rocm/src/dash.rs:617-630,crates/rocm-dash-tui/src/app/actions.rs(request_quit),crates/rocm-dash-tui/src/ui/launcher.rs(handle_launcher_key)run_launcherloops.LauncherRoute::Dashboard => run(None, …)andLauncherRoute::Chatreturn into that loop, soyon "Still serving — quit anyway? … It keeps running in the background after you quit." does not quit.- The loop then re-reads the registry, and
qat the launcher callsrequest_quitagain, which opens a second prompt. - So the first prompt makes a claim about quitting that is false on this path. That contradicts the description's "approving quits anyway". Bare
rocm→ Open dashboard is the documented front door. Neither dash-25 nor dash-26 enters the dashboard through the launcher. - Confidence 85 · logic · Fix: tell a dashboard hosted by the launcher that it is hosted (e.g. a field on
ResolvedArgs) and skip the gate there, since leaving it is "back to menu", not quit. Add a scenario for launcher → dashboard →q.
-
[standing, re-found] The remediation command is only checked as text; nothing shows that running it clears the condition (AGENTS.md §3) —
tests/e2e-cucumber/tests/e2e/dash_steps.rs:557-568(quit_confirm_shows_real_stop_command),crates/rocm-dash-tui/src/ui/mod.rs(quit_confirm_body_prints_a_working_stop_command_for_a_managed_instance)- §3 says a command named in remediation advice "must exist, accept those flags, and actually clear the condition that printed it", and the covering test asserts the message and the resulting state together.
- Both tests match the string
rocm services stop e2e-mock --yes. The World teardown runs that command but asserts nothing about the result. - The step's doc comment and the description's §3 checkbox both say this is satisfied.
- The command itself does exist with those arguments (
apps/rocm/src/main.rs:960, dispatched at:7988), andcontainer_id == service_id(registry.rs:166). - Confidence 85 · architectural (cited rule) · Fix: in dash-25 or dash-26, run the printed command and assert the service is no longer
ready/running(and that the prompt no longer appears). Otherwise mark the pin with a comment naming the test that proves the behavior, as §3 allows.
-
Two checks claim coverage they cannot fail on
- [standing, re-found]
seed_startup_live_service_wires_both_fields_onto_state—crates/rocm-dash-tui/src/app/event_loop.rs:2187, call site:826.- The test's comment says it exists because deleting the call site "would not fail any other test". But it calls the helper directly, so deleting the call at
:826leaves it green. - dash-25 waits for "the managed model is displayed" before pressing
q, so nothing reaches the startup-race path end to end.
- The test's comment says it exists because deleting the call site "would not fail any other test". But it calls the helper directly, so deleting the call at
- [new] A case-mismatched negative assertion —
crates/rocm-dash-tui/src/ui/mod.rs:1056.quit_confirm_body_says_nothing_is_being_served_once_a_snapshot_confirms_itasserts!contains("Stop it first"), but the code prints"— stop it first:"in lower case (ui/mod.rs:294). The assertion can never fail.
- Confidence 85 · mechanical · Fix: make the wiring test go through the code that
event_loopactually runs (e.g. extract the whole state-from-args setup and test that), or reword its claim. Match the real lower-case text.
- [standing, re-found]
Non-blocking
-
[new] The stop command wraps at 80 columns for real service ids, though a comment says it never will; with several managed models the popup also cuts off its own warning —
crates/rocm-dash-tui/src/ui/mod.rs:283-295,crates/rocm-dash-tui/src/ui/approval.rs:115-131- The wrap:
generate_service_id(crates/rocm-core/src/lib.rs:4523) builds ids from the engine, a slug of up to 24 characters, and 13 digits of milliseconds. A typical id is 36–43 characters, so" rocm services stop <id> --yes"is 63–70 characters. draw_approvalusescentered_rect(72,70,96,24,…). At 80 columns that leaves an inner width of about 55, andWrap{trim:false}breaks the command across two rows.- The comment's "keeps the command short enough to never need to wrap" only holds for the 8-character e2e id
e2e-mock. - The cut-off: at 80x24 about 10 text rows remain. Each managed instance takes 2–3 lines, plus 4 trailer/hint lines, so with 3 managed instances the lines that get dropped are "It keeps running…" and "Press s…". Nothing marks the cut.
- Confidence 85 (wrap) / 70 (cut-off), from arithmetic, not a render · logic · Fix: correct the comment, and render with a realistic id and several instances at 80x24 in a
TestBackendtest.
- The wrap:
-
[standing] On Windows, the dashboard's quit gate only ever sees the launch-time read, so a model served mid-session gets no prompt there at all —
crates/rocm-dash-tui/src/client.rs:181-187,apps/rocm/src/dash.rs:994-998,crates/rocm-dash-tui/src/app/mod.rs(has_live_instance)- On Windows the dashboard client never connects, so
has_received_snapshotstays false andinstancesstays empty. - A model started from the Serve wizard:
qexits with no prompt. That contradicts the description's "will show the quit-confirm warning (that part does use the live snapshot)" and README's "it asks first — from the launcher or the dashboard alike". - A model already serving at launch: it gets only the hedged "A model was serving when this started", even after it stops.
- Both scenarios are
@requires-os:linux. Not established by a run. - Confidence 70 · logic · Fix: re-read the registry in
request_quit. That also fixes the frozen-id problem under Decisions. Otherwise qualify README and the description for Windows.
- On Windows the dashboard client never connects, so
-
[new] Doc comments contradict the code
ResolvedArgs::managed_service_ids(crates/rocm-dash-tui/src/app/types.rs): it justifies the staleness because a newservice_idappears only when the user runsrocm serve, "which this process does not observe". The dashboard's own Serve wizard runsrocm serve … --managed(ui/serve_wizard.rs:190). The PR description itself states the opposite.has_stoppable_managed_instance(app/mod.rs): the doc says theskey handler shares it "so the two can't drift".quit_confirm_open_services_keyre-implements the predicate inline instead.- Separately, the startup-fallback condition
!has_received_snapshot && startup_has_live_serviceis written twice, inhas_live_instanceand innothing_is_currently_being_served(ui/mod.rs:230). That is the twin-copy pattern.
- Separately, the startup-fallback condition
- The
has_live_instanceandquit_confirmfield docs say "managed instance". The code counts any serving instance, including external Docker/Lemonade ones. quit_confirm_shows_real_stop_command(dash_steps.rs): its doc describes the whitespace-collapsing reassembly that commit c30f00d replaced.quit_and_wait(tui_driver.rs): it says demo data opens the prompt. Sincesimulatedshort-circuitshas_live_instance, it cannot.quit_confirm_body: its doc says the double iteration "avoids a per-render-frame allocation", but every frame allocates aVec<String>of formatted lines.- Which side governs: in each case the code is the intended behavior, so the comments are what need correcting.
- Confidence 85 · mechanical · Fix: call
has_stoppable_managed_instancefrom the key handler, expressnothing_is_currently_being_servedthroughhas_live_instance, and correct the remaining docs.
-
[new]
docs/manual-testing.mddescribes two states a tester cannot reach —docs/manual-testing.md:321-324- It says declining closes "a manager overlay" that was open when you pressed
q. But every manager overlay consumesqand closes itself (services_manager.rs:162,engine_manager.rs:149,config_manager.rs:129), so the prompt never opens over one. - "Returns to the dashboard" is wrong for the
rocm(launcher) half of the same step, which returns to the launcher. - Confidence 70 · mechanical · Fix: drop the overlay example and say "returns to where you were".
- It says declining closes "a manager overlay" that was open when you pressed
-
[standing in part] Scenario wording —
tests/e2e-cucumber/features/dash.feature:349-350- Two
Whenlines are consecutive; the second should beAnd. - The final
yconfirm is hidden inside "the user quits the dashboard/launcher", so the scenario never states the confirm step it relies on. - The "Press s to open Services" hint is never asserted on screen.
- Confidence 70 · mechanical.
- Two
-
[new] Commit history
- 92c73a9 is an empty "re-trigger" commit and f65d44e is an fmt fix-up.
- About ten "close review gaps" fix-of-the-fix commits make the history noisy.
- a33dfe6 names an internal automation tool in its message, which carries into a squash body.
- Confidence 70 · mechanical · Fix: squash, and edit that message.
Decisions for the author
-
A model served from the dashboard's own Serve wizard gets no stop command and no
shint until restart — answered-by-intent- The description says: "a model served mid-session through the dashboard's own Serve wizard will show the quit-confirm warning … but not yet the stop command or the
shint, until the session is restarted. No false claim is printed either way." - This would otherwise have been blocking.
managed_service_idsis frozen at launch, and this is probably the most common path behind #145. - Re-reading the registry in
request_quitis cheap, and it also closes the Windows gap above. Confirm that leaving this out is still intended.
- The description says: "a model served mid-session through the dashboard's own Serve wizard will show the quit-confirm warning … but not yet the stop command or the
-
The quit prompt reuses the tool-approval component verbatim — tradeoff
- Reusing it follows AGENTS.md and keeps the key handling in one place.
- The cost is that the quit prompt is titled "Review: …" with "Approve (y)" / "Deny" buttons, which reads oddly for a quit.
-
A model that is
Starting(downloading or loading) doesn't trigger the prompt — tradeoffis_serving()covers Ready/Running only, which matches "still serving".- But a model that has just been launched also keeps loading after quit, and nothing says this exclusion is deliberate.
-
The quit-confirm builders live in
ui/mod.rs(about 140 lines plus tests) rather than aui/quit_confirm.rs— non-blocking-improvement. Other prompts have their own files.
Positive signals
open_approvalrefuses while the quit prompt is open, so aymeant for quitting can't approve an unreviewed tool call. A regression test covers it.- All the quit entry points go through one
request_quitgate:q, the Esc-menu Quit row,/quit,/exit, and the launcher'sq/Esc. The twins were kept in sync. - The title and body are derived from one predicate, so "Still serving" can never sit above "Nothing is being served anymore."
resolved_argsreads and classifies the registry once and delegates todiscover_managed_services, instead of hand-copying the status,startup_phaseand port-0 rules.- The stop line is printed only for ids this host's own registry manages, so external Docker/Lemonade instances never get a command that can't stop them.
Deployment notes
None
What this covered
- Read: all 17 changed files, at
prw-base...HEAD(head 37ed114), across four coupled-group workers, plus the code around them: the registry, the serve wizard, the services manager, the launcher loop, and the Windows client stub. - Partial reads: the
#[cfg(test)]modules ofapp/mod.rsandevent_loop.rswere read where the diff touched them;slash.rspast line 330 was grep-scanned only; forui/tabs/instances.rsonly its test-fixture hunk was read. - Stale base: the PR's merge-base is 25 commits behind
prw-base. - Did not run:
- No tests or mutations. Free disk was below the 40G floor (36G at the end), so every test verdict and the wrap and cut-off arithmetic are static.
- Prior-changes pass: partial. It covered earlier PRs' commits, not their review comments.
- Design review: the strategic questions were investigated in the driver's own context, not by independent workers.
- CI: green for 37ed114, except the MI300X E2E lane, which is still queued.
- Not reconciled against any discussion here.
37ed114 to
3e3a932
Compare
|
Thanks for the thorough pass. Note up front: the commit history on this branch has been squashed (the ~15 incremental commits you cited by hash, e.g. Blocking — all five addressed
Non-blocking
Decisions you flagged for the author
All tests/clippy/fmt/crate-edges green on the current head; full details in the PR description, which I've updated to describe the final state rather than the superseded snapshot-based scope. |
|
Responding to round 2. Same note as on the other review: the commit history is squashed — every hash you cited ( Standing items, settled
Blocking items from this round
Decisions for the author
All tests/clippy/fmt/crate-edges green on the current head ( |
3e3a932 to
3940e91
Compare
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 3940e91
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
Round 3. Round 1 was the change request at 77414fb and round 2 the one at 37ed114; both commits are gone after the squash. Each finding is marked [new] or [standing]. This round reviewed the whole change blind, statically: disk was below the build floor, so nothing was built or run here. It then put each earlier blocker that the blind pass did not re-find back to the same reviewer, one at a time, together with the author's reply. Those verdicts are anchored evidence from reading the code.
Earlier blockers, settled:
- Remediation command checked only as text (AGENTS.md §3): the approach is now right, since dash-27 runs the printed command. But the proof does not exist yet, because dash-27 kills the test runner. This was re-found independently and is merged into the first Blocking item. [standing, re-found]
- Wiring test that cannot fail: re-found independently against the renamed
seed_registry_probe_and_hosting. See Blocking. [standing, re-found] - Case-mismatched
!contains("Stop it first")assertion: resolved. The assertion is gone, and the stop line is covered atui/mod.rs:1258and:1284. - Managed-instance bullet untested: still holds. See Blocking. [standing]
- "Fixes #145": narrowed again, and it still holds in part.
- The live registry read fixes the launcher on both platforms, and probably the Linux dashboard.
- A Windows user who runs
rocm dashorrocm chatdirectly still gets only the generic warning, with no model name, no stop command and nos(Blocking item 3). - The claim is in the commit message as well as the description, so it closes #145 on main. See Blocking. [standing, narrowed]
- Launcher → dashboard double prompt (non-blocking in rounds 1 and 2): not re-found this round. dash-29 now covers it, though that scenario did not run to completion on this head (see the CI note below).
Driver note on CI (checked directly in the job logs): all three self-hosted Linux lanes on this head died at the same point. They are Strix Halo WSL2, Strix Halo Ubuntu (release) and Strix Halo Ubuntu (nightly). In each, Scenario: dash-27 - Running the quit prompt's printed stop command actually stops the model starts, the step And the quit prompt shows the real stop command for the managed model passes, and then process didn't exit successfully: …/e2e-… (signal: 15, SIGTERM) follows. The hosted E2E tests job fails with the same SIGTERM, and that job passes on main at 213c2cb and 1e5aac8. So the two Ubuntu lanes are not separate GPU-preflight or pre-warm failures, as the "Separately" sub-bullet in the first Blocking item below says: their logs show normal VRAM/GTT readings and the same dash-27 death. Strix Halo Windows passed. MI300X had not reported.
Review — needs work
Full review of the whole change.
It says "Fixes #145" (issue: "After serving a model, the process keeps running when I exit rocm. I don't see any obvious way to stop the model serving service."). The warning, the printed stop command and the s jump to Services do answer that. Whether the PR holds up on Windows is covered under Blocking.
Blocking
-
[new; carries the standing §3 item] dash-27 sends SIGTERM to the e2e test runner itself, so the required
E2E testscheck is red —tests/e2e-cucumber/tests/e2e/dash_steps.rs:771(run_quit_prompt_stop_command),tests/e2e-cucumber/tests/e2e.rs:566-569,apps/rocm/src/main.rs:15474(terminate_recorded_service_pidsfilter),crates/rocm-core/src/proc_lifecycle.rs:103- What happens:
register_mock_service_withwritessupervisor_pid: std::process::id()andengine_pid: Some(std::process::id())into the fixture record. That is the PID of the cucumber harness.- dash-27 then runs
rocm services stop e2e-mock --yes.terminate_recorded_service_pidsskips only PID 0 and therocmchild's own PID. - The record has no
*_start_ticks, soidentity_statefalls through(None, _) => IdentityState::Matches("best-effort proceed"). terminate_verified(.., KillScope::Tree, ..)then signals the harness and its whole process tree.
- CI evidence:
- Strix Halo WSL2 lane:
Scenario: dash-27 …started at 14:27:55, thenprocess didn't exit successfully: …/e2e-… (signal: 15, SIGTERM)at 14:27:57. - Hosted
E2E testsjob (run 38262186241): the same SIGTERM about 30 s into the suite. mainpassed the same job at 213c2cb.- Because the suite aborted, the hosted lane and the WSL2 lane did not run the rest of their e2e scenarios, dash-25..29 included, on this head.
- Strix Halo WSL2 lane:
- The repo already knows about this hazard:
e2e.rs:658-661skipse2e-mockin teardown because "The planted mock record has no real process to stop".service_cleanup_steps.rsplantssupervisor_pid: 0because a live PID "makes the World's teardownservices stopkill this test process".
- Until this is fixed, the §3 proof that the printed stop command actually works does not exist.
- Separately: both Strix Halo Ubuntu lanes are also red, on
la03-s04hosts. The release lane failed GPU preflight on the VRAM/GTT floor. The nightly log stops during pre-warm. Nothing ties either to this change, but they are still red and need a rerun. - Confidence 100 · logic · Fix: plant dash-27's record with the PID of a disposable child the step spawns (for example
sleep), then assert that child is gone. Never point a stop at the harness PID.
- What happens:
-
[new] The live registry probe does not check process liveness the way
rocm servicesdoes, so a crashed service still triggers the prompt and its "keeps running" claim —apps/rocm/src/dash.rs:341-351,crates/rocm-dash-tui/src/registry_probe.rs:62,crates/rocm-dash-tui/src/ui/mod.rs:311LiveManagedRegistryProbe::snapshotdoesload_service_records+discover_managed_servicesand checkssvc.status.is_serving(). That reads the raw on-diskstatusstring.rocm services listgoes throughload_managed_services/refresh_managed_service_runtime_livenessinmain.rs, which demote aready/runningrecord whose PIDs are dead.- So after a crash that leaves
"status": "ready"on disk, quitting prompts "Still serving — quit anyway?" and prints "It keeps running in the background after you quit." It also gives a stop command (and on the dashboard,s) for a serviceservices listreports as stopped. - On the launcher, and on Windows where the probe decides alone for the whole session, nothing corrects this.
- The trait doc says the probe will "classify it the same way
rocm servicesdoes", which the code contradicts. The code is what runs, so the doc is the wrong side. - This is the outcome-claim shape AGENTS.md §3 names. The pre-existing raw-status read is now behind a blocking prompt that makes that claim.
- Confidence 70 · logic · Fix: add a local-only PID check (
recorded_service_pids+process_is_running, no network) to the probe. Otherwise hedge the printed claim and correct the trait doc.
-
[new] On the Windows dashboard the prompt never names the model, never prints the stop command and never offers
s, yet the description and README say it does —crates/rocm-dash-tui/src/ui/mod.rs:265-276,crates/rocm-dash-tui/src/app/mod.rs(has_stoppable_managed_instance),README.md:169-172- On Windows
instancesnever fills: the client connect path is unix-only, anddash.rsgets no daemon client there. quit_confirm_bodytherefore always takes thetotal == 0branch and prints only "A model was serving when this started." / "It may still be running…".- That holds even though
managed_idswas already read from the live probe on line 252. has_stoppable_managed_instanceiteratesinstances, so theshint and key never appear on Windows.- Claims this contradicts:
- Description: "quitting now shows a confirm prompt first, naming the serving model(s) and … the real
rocm services stop <id> --yescommand". It also says Windows "still gets a correct answer for the whole session". - README: "From the dashboard's own copy of that prompt, pressing
sinstead jumps straight to the Services overlay". It states this without condition.
- Description: "quitting now shows a confirm prompt first, naming the serving model(s) and … the real
- "When this started" is also wrong for a model started mid-session, which the probe exists to catch.
- The comment at lines 266-274 still describes the old launch-time snapshot ("a model was recorded live on disk at launch").
- Confidence 85 · logic · Fix:
- Either finish the work: when
!has_received_snapshot, build the list and stop commands from the probe'smanaged_ids, and say "A model is serving". - Or narrow the description and README to what Windows actually shows.
- Either finish the work: when
- On Windows
-
[standing, re-found] The comment on
seed_registry_probe_and_hostingsays its test catches a deleted call site, but it cannot —crates/rocm-dash-tui/src/app/event_loop.rs:229-236,event_loop.rs:2179-2205- The doc says the helper exists "so a regression test can exercise the actual call site". The test comment says that without it "a silent no-op here (the call site deleted, or the fields swapped) would not fail any other test".
- The test calls the helper directly. Deleting the call at
event_loop.rs:823, or passinglauncher_hosted: falsethere, leaves it green. - What it actually checks is two field assignments.
- Verdicts: (1) it passes with the wiring reverted; (2) the call site is the branch it misses.
- Confidence 85 · mechanical · Fix: say honestly what the test covers, or test the wiring through a constructor the event loop itself uses.
-
[standing, not re-found blind; settled as still holding] Nothing asserts the managed-instance line that names the serving model —
crates/rocm-dash-tui/src/ui/mod.rs:302("• {} — stop it first:"), test atui/mod.rs:1258-1281- The managed-instance unit test sets
model_name: "Qwen2.5-7B"but asserts only therocm services stop svc-managed --yesline. The only model name asserted anywhere is the unmanaged bullet's (ui/mod.rs:1303). - dash-25 and dash-26 match the "quit anyway" marker and the stop command against the whole screen. Their
the managed model is displayedchecks run before the prompt opens and after it closes, so nothing ties the model name to the popup. - Fix: assert a line equal to
"• Qwen2.5-7B — stop it first:"in the existing unit test. An e2e check would not prove it, because the dashboard behind the popup already shows the model name. - This verdict came from putting the earlier wording to the reviewer, so it is anchored rather than blind.
- The managed-instance unit test sets
-
[standing, narrowed] "Fixes #145" in the commit message closes the issue on main, but a Windows user who opens
rocm dashorrocm chatdirectly still gets no way to stop the model from the prompt — commit 3940e91 message, PR description; mechanism as in the Windows item above (ui/mod.rs:265-276,app/mod.rs:691-694)- The launcher on both platforms now re-reads the registry on every pass (
apps/rocm/src/dash.rs:643,ui/launcher.rs:303-318), so that part of the earlier objection is resolved. - On the Windows direct path the prompt opens, but shows only the generic warning. That is #145's own complaint: "I don't see any obvious way to stop".
- To resolve: fix the Windows item above, which also makes this claim true. Otherwise change the commit message to "Refs #145" and track the Windows gap separately.
- This verdict came from putting the earlier wording and the author's reply to the reviewer, so it is anchored rather than blind.
- The launcher on both platforms now re-reads the registry on every pass (
Non-blocking
-
[new] The prompt body is silently clipped once several managed models serve with real IDs —
crates/rocm-dash-tui/src/ui/mod.rs:302-303,ui/approval.rs:115-131- Each managed model takes 2 lines, and a 23-47-character real id wraps the command line to 3.
- The body area is the popup height (
centered_rect(72, 70, 96, 24)) minus 2 button rows. At 80x24 that is about 12 rows. - With about 3 managed models, the trailer and the "Press s" hint fall below the clip with no marker.
Paragraphhas no scroll. quit_confirm_popup_with_realistic_ids_does_not_panic_at_a_narrow_widthasserts only that each id appears, so it stays green.- Confidence 70 · logic · Fix: put the trailer and hint before the list, or cap the list to the rows available.
-
[new] Each frame reads the registry up to about four times while the prompt is open, and the title and body can disagree —
ui/mod.rs:206,ui/mod.rs:212,ui/mod.rs:252-253,app/mod.rs(has_live_instance,has_stoppable_managed_instance)- Each call is a full
read_dir+ JSON parse ofservices/, which grows with stopped records. - On Windows this happens for the whole session.
- Because the reads are separate, the title and body can be built from different registry states. That is the self-contradiction the change tests against elsewhere.
- Confidence 70 · mechanical · Fix: take one snapshot per frame in
quit_confirm_requestand pass it down.
- Each call is a full
-
[new] If the registry can't be read, the warning is silently skipped —
crates/rocm-dash-daemon/src/registry.rs:123-137,apps/rocm/src/dash.rs:344-349load_service_recordsreturns an emptyVecwhenread_dirfails and skips unreadable or half-written files.- The gate then reads "nothing serving" and exits without asking, which is the original bug. There is no log line either.
- Confidence 70 · logic · Fix: have the snapshot carry an "unknown" state, and make the gate ask when it is set.
-
[new] Several quit paths can break while every scenario stays green
- Launcher-hosted Chat route —
apps/rocm/src/dash.rs:653,dash.rs:715: dash-29 drives only the launcher → Dashboard route. Changingrun_chat(chat_mock, …, true)tofalse, or droppingargs.launcher_hosted = launcher_hosted;at line 715, breaks nothing that is tested. - Esc-decline and Enter-confirm:
docs/manual-testing.mdpromises both, but no scenario or test covers them;quit_and_waitalways confirms withy. - Ctrl-C while serving: the new README text promises "
Ctrl-Calways quits immediately without asking", but dash-20 uses--demo(never serving) and dash-21 has no serving model. This is the "will not happen" promise shape AGENTS.md §3 names. - Confidence 70 · logic · Fix: a launcher → Chat scenario that quits back, plus Ctrl-C-while-serving and Esc/Enter variants on an existing scenario.
- Launcher-hosted Chat route —
-
[new] A step comment cites a commit hash that will not survive the squash merge —
tests/e2e-cucumber/tests/e2e/dash_steps.rs:590("since commit c30f00d")- That object is not in the base history. It was an earlier head of this PR's branch, and the repo squash-merges.
- Confidence 85 · mechanical · Fix: name
ui::quit_confirm_bodyinstead of the hash.
Decisions for the author
-
Reusing the shared approval component for the quit prompt — tradeoff
- Reusing
ui/approval.rsfollows AGENTS.md §6 and keeps the key handling consistent. - The cost is that the quit question shows "Approve (y)" / "Deny (n)" and "Esc/q cancel". A user who presses
q, reads "quit anyway?" and pressesqagain is sent back to the dashboard, not out. - A per-request label override in the shared component would keep the reuse and fix the wording.
- Reusing
-
managed_registry_probe: Option<…>is documented "neverNone", andNonesilently means "nothing live" — non-blocking-improvement- A required field with a null-probe implementation would make that invariant hold by construction rather than by a doc sentence.
Positive signals
- One
request_quitgate shared byq, the Esc-menu Quit row,/quit//exitand the launcher, so the entry points cannot re-decide differently. open_approvalrefuses while the quit prompt is open, so a pendingycan never approve a tool call nobody reviewed.- The stop command is printed only for ids in the registry's managed set, so a Docker- or Lemonade-discovered instance never gets advice that cannot work.
- dash-27 runs the literal printed command and checks registry state, which is the right AGENTS.md §3 shape once the fixture PID is fixed.
decline_quit_promptwaits for the marker to leave the screen, so the next "model is displayed" check cannot pass on the prompt's own text.
Deployment notes
None
What this covered
What was read: every file in prw-base…3940e91c (fe47c48…3940e91c). Four workers each took a coupled group:
- the app state and quit gate;
- UI rendering, the launcher and the scrollbar;
- the bin's
dash.rsprobe and launcher; - the e2e tests and docs.
Each read its group's changed code completely, along with callers in apps/rocm/src/main.rs, crates/rocm-core/src/proc_lifecycle.rs and crates/rocm-dash-daemon/src/registry.rs. The very large app/mod.rs and event_loop.rs were read at the changed regions and their surrounding arms, not every unrelated line.
What did not run, and why:
- The review was static only. Free disk on this host is below the 40G floor, so no cargo build, test or clippy ran. CI results were read with
ghinstead. - The finding scored 100 rests on CI logs plus a code trace, not a local run.
- The
bin dash.rsworker hit the concurrent-subagent cap on its first spawn and was re-spawned once, successfully. - The agent-instruction, history and code-comment passes ran inside the tests/docs worker.
- The prior-changes pass did not run: reading earlier PR reviews is outside this review's remit.
- Step 7 (design questions) was answered by the driver from the workers' evidence, not by independent workers.
- The
self-hosted E2E (MI300X)lane was still queued when CI was read.
Discussion: not reconciled against any PR discussion here.
|
Thanks for the thorough pass. Responding against the new commit, Blocking — all five addressed
Non-blocking
Decisions for the author
CI
All of |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 6b7ac3c
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
Round 4. Earlier rounds: the change request at 77414fb, the one at 37ed114, and the one at 3940e91, which the author dismissed with "fixed". Each finding below is marked [new] or [standing]. This round reviewed the whole change blind, at 6b7ac3c. This time it built the code, ran mutation checks and ran the dash- e2e scenarios locally. It then put each of the six earlier blockers back to the same reviewer, one at a time, together with the author's round reply. Those verdicts are anchored evidence from reading the code, not blind findings.
Earlier blockers, settled:
- dash-27 SIGTERMs the e2e harness: resolved. The record now points at a disposable
sleepchild,KillScope::Treecannot reach the harness from there, and dash-25 to dash-29 passed on the WSL2 lane at this head. The second half of the earlier fix still holds: nothing asserts that the child process is gone. See Blocking. [standing, narrowed] - The probe counts a crashed service as serving: the trait doc is now honest, and the probe-only path hedges with "may still be running". Still holds on the launcher path. See Blocking. [standing, narrowed]
- The Windows prompt shows no model name or stop command: resolved in substance. On the probe-only path the prompt now lists each managed id with its real stop command, a unit test covers it, and the README and manual-testing docs carry the Windows caveat. Leftovers, none of them gating:
- "A model was serving when this started." is still at
ui/mod.rs:282. It is reachable only through a race between two registry reads. - The PR description still claims the model name and the
sjump with no Windows caveat. That mismatch is in the description only. - The first commit message makes the same unconditional claim, and the second commit message corrects it.
- "A model was serving when this started." is still at
- The wiring test cannot fail: resolved. The comments now say what the unit test covers. A mutation run confirmed that dash-29 catches the call site passing
false. - The managed-instance bullet's model name is not asserted: resolved.
ui/mod.rs:1329-1333asserts the exact line, and the fixture's name differs from its id. - "Fixes #145" is false on Windows when dash or chat is opened directly: resolved as far as the code shows. On Windows both
rocm dashandrocm chatnow reach the prompt and print the stop command. No scenario exercises that on Windows; see the Windows scenario item below.
Driver notes on gating:
- CI: the red
E2E tests (Strix Halo, WSL2)failure does not gate here.- Its one unexpected failure is
comfyui-04, which timed out waiting for a download-progress frame. This change touches no ComfyUI or install code. - dash-01 to dash-29 all passed on that lane, and the same lane passed on recent
mainmerge-queue runs, for example run 38368744433. - Re-running the job should clear it.
- Its one unexpected failure is
- Not escalated from earlier rounds:
- The launcher → Chat route and Ctrl-C-while-serving were non-blocking at 3940e91. They are not escalated here.
- Under the "quit paths not covered" item, the parts that gate are the new ones: the Esc-menu Quit arm with nothing serving, the
srow selection, and the launcher prompt's Esc andqkeys. - The popup clipping at three or more managed models stays non-blocking, as before. The test that cannot fail is new and gates.
- Description-only items: the Windows-scenario gap and the description's coverage wording do not gate on their own. Fixing the commit messages and the PR text together would clear them.
Review — needs work
Full review of the whole change.
Implements #145 (the confirm-before-quit prompt). The description and commit messages claim more test coverage than exists, and the first commit message claims Ctrl-C is gated when it is not.
Blocking
- [new] The first commit message says Ctrl-C now opens the prompt, but Ctrl-C still quits without asking — commit
3940e91message;crates/rocm-dash-tui/src/app/event_loop.rs:1104against:1166.- The message says: "Pressing q (or Ctrl-C with no job console open, /quit, /exit, or the Esc-menu Quit row) ... Every quit entry point ... now routes through one shared gate ... that opens a confirm prompt instead".
- The code says otherwise. The
ctrl_c_should_exitarm at:1104runs before thequit_confirm_pendingarm, and Ctrl-C never reachesrequest_quit. - The code governs. The README and the PR body both say Ctrl-C is not gated.
- rocm-cli builds the squash commit from the commit messages, so this claim would land on main.
- Confidence 85 · logic · Fix: reword the commit message to drop Ctrl-C from the gated list.
- [new] Nothing tests that the probe re-reads the registry, which is the main thing this change claims —
apps/rocm/src/dash.rs:344-358(LiveManagedRegistryProbe), and the sixresolved_args_*tests indash.rs.- Every test writes its records before
resolved_argsand never changes them. A probe that cached its first read would pass all of them. - The e2e scenarios don't catch it either. dash-25 and dash-27 wait for the daemon snapshot, after which
has_live_instancestops consulting the probe.managed_idsis unchanged from launch in every scenario. - The PR body says "the live registry read [is] exercised directly", and it names a model started mid-session as one of the two gaps this closes. No test exercises that case.
- Confidence 85 · logic · Fix: build the args against an empty services dir, then write a
runningrecord and assertsnapshot().has_live_serviceflips to true; remove it and assert it flips back.
- Every test writes its records before
- [new; the launcher → Chat part was non-blocking at 3940e91 and is not escalated] Some quit paths this change rewires are not covered by any test, although the description says they are —
apps/rocm/src/dash.rs(LauncherRoute::Chat => run_chat(.., true));crates/rocm-dash-tui/src/app/actions.rs:294;crates/rocm-dash-tui/src/app/mod.rs:794-799;crates/rocm-dash-tui/src/ui/launcher.rs(handle_launcher_key'sNonearm).- Launcher → Chat route. A worker flipped
launcher_hostedtofalsefor both launcher routes and ran the tests: all unit tests stayed green. dash-29 catches the Dashboard route only. Nothing opens chat from the launcher, yet the body says "a dashboard or chat session opened from inside the launcher no longer asks twice". - Esc-menu Quit with nothing serving. The arm changed from
return truetoreturn request_quit(state). Both new menu tests seed a Running instance, and no test before or after activates the Quit row with nothing serving. The PR body says theqkey, the Esc-menu Quit row and/quit"all still exit immediately with nothing serving". skey row selection. The.is_some_and(|i| i.status.is_serving())check in thepositionclosure can become.is_some()and every test stays green. No test has a stopped managed row sorting before a serving one.- Launcher prompt keys.
launcher_quit_confirm_approve_exits_deny_and_cancel_staysends onlynandy. It never sends Esc orq(the Cancel its name claims), and never sends a non-verdict key while the prompt is open. - Confidence 100 for the launcher → Chat result (a worker ran it); 85 for the rest (traced) · logic · Fix: add a launcher → chat →
/quitscenario (or unit-test the route →launcher_hostedmapping); an Esc-menu Quit test with no instances assertingtrue; a stopped-then-serving managed-row case; Esc,qandd-while-open cases for the launcher.
- Launcher → Chat route. A worker flipped
- [new] Unit tests pin the stop-command wording without pointing at the test that proves the command works (AGENTS.md §3) —
crates/rocm-dash-tui/src/ui/mod.rsquit_confirm_body_prints_a_working_stop_command_for_a_managed_instanceandquit_confirm_body_prints_the_stop_command_during_the_startup_fallback_when_the_registry_names_an_id.- §3 says: "Where the text genuinely has to be pinned on its own, the assertion carries a comment naming the test that proves the behavior."
- dash-27 proves the behaviour, but neither test names it. The first test's name says the command is "working", which a string match cannot show.
- Confidence 85 · mechanical · Fix: rename it to "names the real stop command" and point both tests at
@id:dash-quit-prompt-stop-command-actually-works.
- [new; the clipping half was non-blocking at 3940e91 and is not escalated] The narrow-width popup test cannot fail on what its comment says it checks, and real ids clip the prompt's last lines —
crates/rocm-dash-tui/src/ui/mod.rs:1437-1489(the test);quit_confirm_body, rendered throughapproval.rscentered_rect(72, 70, 96, 24, ..).- The comment says the test checks that every id's text still appears. But the fixture sets
model_name: format!("model-{id}"), so the id appears in the bullet line even if everyrocm services stop … --yesline is dropped. - Its ids are UUID-shaped, not the
<engine>-<slug>-<millis>format the comment cites fromgenerate_service_id. - It uses only 2 instances. At 80x24 the popup has about 10 text rows, and each real-length managed id takes about 3 rows because the stop line wraps. With 3 or more services, the trailer ("It keeps running in the background after you quit.") and the
shint are cut off with no marker. That geometry is argued from the code and was not rendered (70). - Confidence 85 for the test (traced) · logic · Fix: give the fixture a model name that doesn't contain the id; assert the full stop command and the trailer; use real-format ids with 3 or more services. Then size or reorder the body so the trailer always shows.
- The comment says the test checks that every id's text still appears. But the fixture sets
- [new; does not gate on its own, see driver notes] No scenario covers the Windows prompt, and the description does not name that gap (AGENTS.md §3) —
README.md:170-179;docs/manual-testing.md(new bullets);ui/mod.rsquit_confirm_body'stotal == 0branch; dash-25 to dash-29 are all@requires-os:linux.- On Windows the prompt behaves differently: it lists ids without model names, uses the hedged "It may still be running…" trailer, and offers no
shint. All of that is user-visible and only unit-tested. - The body's "Not verified" line says the pty scenarios and the "GPU/Windows/WSL lanes … are exercised there". That reads as if the Windows lane runs these scenarios. It runs none of them.
- Confidence 85 · mechanical · Fix: state in the PR text that the Windows prompt has no scenario and why (the pty harness is Linux-only), or add a Windows-runnable scenario.
- On Windows the prompt behaves differently: it lists ids without model names, uses the hedged "It may still be running…" trailer, and offers no
- [new; does not gate, see driver notes] A required check fails on this head: "E2E tests (Strix Halo, WSL2)" — job 115169790782.
- The log shows 180 scenarios, 176 passed and 4 failed. Three of the failures are expected-fail (xfail) rows.
- The one unexpected failure is
comfyui-04(comfyui-source-download-shows-live-progress). It timed out after 30s while the screen still showed "Downloading uv (latest) from https://github.com/…" / "Installing Python 3.12 via uv...". - This change touches no ComfyUI or install code, and dash-01 to dash-29, including all five new scenarios, passed on that lane. So it is very likely a host or network timing failure, not this change; comparing against main settles it. "E2E tests (MI300X)" was still queued.
- Confidence 100 (read from the log) · mechanical · Fix: rerun the job, or show the same failure on main.
- [standing, narrowed] dash-27 never checks that the stopped process is actually gone —
tests/e2e-cucumber/tests/e2e/dash_steps.rs:810-818,tests/e2e-cucumber/tests/e2e.rs:594-600.- Earlier wording (3940e91), whose fix still applies in part: "plant dash-27's record with the PID of a disposable child the step spawns (for example
sleep), then assert that child is gone." - The first half is done. The second is not.
- The scenario's only check after the stop reads the
services list --jsonstatus.E2eWorld::dropthen kills and reaps the child without checking anything. - So a
rocm services stopthat rewrote the record to stopped without signalling anything would leave dash-27, "Running the quit prompt's printed stop command actually stops the model", green. AGENTS.md §3 asks that the named command "actually clear the condition". - Fix: before teardown, assert that the child has exited (
try_wait()returnsSome). - This verdict came from putting the earlier wording and the author's reply to the reviewer, so it is anchored rather than blind.
- Earlier wording (3940e91), whose fix still applies in part: "plant dash-27's record with the PID of a disposable child the step spawns (for example
- [standing, narrowed] On the launcher path, a crashed service still gets the unhedged "It keeps running in the background after you quit." —
apps/rocm/src/dash.rs:673-688(launcher_serving_instances),crates/rocm-dash-tui/src/ui/launcher.rs:324,crates/rocm-dash-tui/src/ui/mod.rs:329, title atui/mod.rs:209.- Resolved since 3940e91: the trait doc (
registry_probe.rs:62-78) and the "known divergence" note no longer overclaim, and the probe-only branch hedges its trailer (ui/mod.rs:294). - Still holds:
- The launcher fills
instancesfrom the raw on-disk status, with no liveness check. - For a crashed record still marked
ready, barerocm→qtakes thetotal > 0branch. - It then prints the unhedged outcome claim, labels the service "stop it first", and gives a stop command for a service
rocm services listreports as stopped. - The title "Still serving — quit anyway?" is unhedged on every path, including the probe-only one whose trailer says "may still be running".
- The launcher fills
- That is the AGENTS.md §3 outcome-claim shape the earlier objection named.
- Neither branch of the earlier fix (a local PID check, or a hedged message) reaches the launcher path.
- Fix: hedge the launcher's trailer and title the same way the probe-only path is hedged, or add the PID check.
- Not settled: whether the Linux dashboard is also affected once the daemon's first snapshot lands. That depends on how the daemon marks an instance whose endpoint stopped answering, which was not read.
- This verdict came from putting the earlier wording and the author's reply to the reviewer, so it is anchored rather than blind.
- Resolved since 3940e91: the trait doc (
Non-blocking
- [new] The launcher decides from a snapshot that is stale while the user sits at the menu —
apps/rocm/src/dash.rs:650(launcher_serving_instances);ui/launcher.rslauncher_state(StaticManagedRegistrySnapshot).- The snapshot is rebuilt once per menu pass. The menu then blocks on
event::read()with no refresh. - So a model started from another terminal, or a record that goes from
startingtoreadywhile the menu is up, can be followed byqwith no prompt. - In the reverse case, the prompt says "Still serving" and prints a stop command for a service that already stopped.
- The comment saying the snapshot is "exactly as live as the launcher itself ever is" (
registry_probe.rs:89-94) overclaims. - Confidence 85 · logic · Fix: pass the live probe into
run_launcher.
- The snapshot is rebuilt once per menu pass. The menu then blocks on
- [standing, non-blocking since 3940e91] The title and body read the registry separately, so the "can't contradict" claim is false —
ui/mod.rsquit_confirm_request,quit_confirm_body,nothing_is_currently_being_served.- On Windows, or before the first snapshot, each frame reads the registry up to 4 times. Frames are drawn at least every 250 ms tick.
- A registry write between two of those reads can still produce "Still serving" over "Nothing is being served anymore.", the exact contradiction the doc comment says is impossible.
- Confidence 70 · architectural · Fix: take one
ManagedRegistrySnapshotper frame and pass it to the title, body and hint.
- [standing, non-blocking since 3940e91] A failed registry read means "nothing is serving", so
qexits silently —dash.rs:349-357;registry_probe.rs(snapshot()cannot return an error).load_service_recordsreturns an empty list on aread_direrror and skips unreadable files. That behaviour is pre-existing, but the quit gate now depends on it, and on Windows the probe is its only input.- Confidence 70 · architectural · Fix: let the snapshot carry "unknown" and show the hedged prompt, or document the fail-open choice.
- [new] Several comments contradict the code; the code governs in each case:
dash.rs:265-270: "shared by the three derived fields below", butdiscoveryhas one consumer (:305).app/mod.rs:670-671: "when none was injected (demo/replay/mock)", but the field doc at:204-205andresolved_argssay the probe is alwaysSome.app/mod.rs:1247-1248: the sibling case is "below", but it is above.app/types.rs:42-46andactions.rs:72-74: call the launcher's read "live", but it is static.ui/launcher.rs:307: "every managed-service record regardless of status", butdiscover_managed_servicesdrops failed, stopped and port-0 records.tui_driver.rs:672(quit_and_wait): still says "nothing is resent", but the loop now sendsy.- More generally, many new comments narrate review history ("used to claim…", "Regression guard…") and run to 15–25 lines. That makes them expensive to keep accurate, which is how the drift above happened.
- Confidence 85 · mechanical.
- [new] The quit prompt contradicts
docs/ux-guidelines.md:101-105. The guideline says Quit cards should "return the user to the same row they were on" and that letter shortcuts "must not be required". This change closes the open modal on decline, asdocs/manual-testing.mdnow documents, and makessthe only way to reach the stop action. Nothing says which document governs. Confidence 70 · logic · Fix: restore the previous modal and add a selectable Stop choice, or update the guideline with the reason. - [standing, non-blocking since 3940e91] No scenario tests the new README promise that Ctrl-C "always quits immediately without asking" while a model is serving. dash-20 runs
--demo, dash-21 has no model. §3 lists "will not happen" claims as needing the behaviour asserted. Confidence 70 · mechanical. - [new] An approval that arrives during the quit prompt is dropped, and its message is not asserted or actionable —
app/mod.rs:1255-1260.- The model has already been told the action was surfaced for approval.
- The error turn gives no way to recover (e.g. "ask again"), and
approval_request_is_discarded_while_quit_confirm_is_pendingchecks onlyrole == Error. - Confidence 70 · logic.
- [new] dash-27 does not check that the dashboard stops asking after the stop command runs; one step exposes harness plumbing.
- After running the printed command it checks the registry but never that the dashboard's own prompt clears. A worker's temporary probe found that it does clear once the instance leaves, so this is assertable.
- "the managed model's record points at a disposable process" is harness detail and belongs inside a Given.
- Confidence 70 · mechanical.
Decisions for the author
- Launcher-hosted sessions skip the prompt — tradeoff. One prompt instead of two, at the cost of the
s-to-stop shortcut, which only the dashboard's own copy offers. The design is sound because every launcher-hosted exit returns to a registry re-read. - Two independent
Options that each own the screen — non-blocking-improvement.approvalandquit_confirmare kept apart only by convention, andresolve_quit_confirm_keyreturns anOption<bool>tri-state. A singleScreenGateenum plus aQuitVerdicttype would make the bad states impossible to represent. - The root cause on Windows is that the dashboard has no live instance feed at all — tradeoff. The
cfg(windows)client stub means the Serving tab and Services overlay are empty there too. This change patches only the quit gate. That is a reasonable scope, but it is worth saying explicitly.
Positive signals
- dash-27 runs the printed
rocm services stop <id> --yesand checks the registry afterwards. That is §3 done properly: the remediation advice is proven, not just its wording. - The prompt reuses
ui/approval.rsinstead of adding new modal UI, and the registry seam sits in its own file without breaking the crate-layering rules. - The new auto-
yinquit_and_waitdoes not hide an unwanted prompt. A worker made the prompt always open and ran the e2e suite: 12 of 20 non-serving scenarios failed.
Deployment notes
- User-visible behaviour change outside the quit prompt: while a chat tool-call approval is pending, tab-bar and footer-chip clicks are now swallowed. The PR body mentions this.
What this covered
- Every file in
prw-base...HEAD(merge-base 213c2cb…6b7ac3cd), read completely, plus the callers and callees of the changed symbols (registry.rs, theServicesCommand::Stopdispatch, theevent_loopkey and mouse routing,approval.rs). - Five parallel workers grouped by coupling plus one design worker. The mutation checks and the e2e run (
-n dash-on the PR head: 28 of 29 passed; dash-24 failed only because the name filter bypasses its@requires-wsl/@requires-gpugating) were done by one worker in this checkout. - History and prior-review passes ran against earlier merged PRs that touched these files.
- Not run:
- No Windows build or Windows-target check.
- No e2e run of the Windows or WSL GPU lanes beyond reading the WSL2 job log.
- Three
agent::clientsunit tests were skipped because they fail in this environment with a permission error, whether or not anything is mutated.
- The review itself was not reconciled against any PR discussion. The six earlier blockers were settled afterwards, outside the review, as described at the top.
6b7ac3c to
45c597f
Compare
|
Thanks for the thorough pass. Responding against the new commit, Blocking — all addressed
Non-blocking
Verification
|
|
Addressed round-8's gating and reported findings in cf4d9eb, re-confirmed against the current tree before fixing (not from the review's own evidence), with a follow-up correction in 1f76c5a after a second independent review pass:
Left as-is, per the review's own framing — no change intended:
Verification: |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 1f76c5a
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
Review — needs work
Full review of the whole change.
Implements issue #145 ("Stop serving a model"). It warns before quitting, prints the stop command and adds the s jump to Services. It does not add a stop action to quit itself. "Fixes #145" is a fair claim for that request.
Round 9 status: nothing gates this round, so the standing change requests are lifted. Both standing objections from round 8 are settled (see the end of this report). The review reported three new findings as blocking. All three are test-strength gaps, and none of them gates. The production behaviour each one names is correct today:
qwith nothing serving exits at once. The demonstrated regression is caught by the unit testhas_live_instance_is_always_false_under_simulated.- Ctrl-C's arm runs ahead of the quit-confirm arm.
- dash-28's decline is checked by the step before the vacuous one.
Merging therefore leaves weaker tests behind but no defect, which is below the floor for a merge gate. Please still fix them.
Blocking
-
[new · reported, not gating] The e2e quit helper now presses
ythrough any quit-confirm prompt, so existing scenarios no longer prove thatqexits without asking when nothing is serving —tests/e2e-cucumber/tests/e2e/tui_driver.rs(quit_and_wait:let confirming = …contains(QUIT_CONFIRM_MARKER); self.send(if confirming { "y" } else { gesture }))
Every pre-existing "the user quits the dashboard/chat/launcher" step now accepts a prompt it never expected. The test was a mutation: remove thesimulatedshort-circuit inhas_live_instance_with, so that--demofalsely reports a serving model. dash-01 and dash-05 stayed green, and a log line inquit_and_waitshowed the false "May still be serving — quit anyway?" prompt opening and being answeredy.docs/manual-testing.mdsays that with nothing being served,qexits immediately with no prompt. No scenario guards that anymore; only the unit testhas_live_instance_is_always_false_under_simulateddoes. AGENTS.md §8 treats weakening a test as a change in its own right.
Confidence 100 · logic · Fix: pressyonly when the scenario opted in (for example a world flag set by "a running managed model is available locally"), and fail fast if the prompt appears otherwise. Or add a scenario whereqwith nothing serving (or under--demo) exits on the first keystroke and the prompt never appears. -
[new · reported, not gating] Nothing tests that Ctrl-C quits while the quit-confirm prompt is open —
crates/rocm-dash-tui/src/app/event_loop.rs:1105(Ctrl-C arm) vs:1167(quit-confirm arm)
This behaviour depends only on match-arm order. One mutation moved the quit-confirm arm above the Ctrl-C arm. The fullrocm-dash-tui --libsuite still passed (868 passed; the 3 failures are the knownagent::clients::*ones). dash-31 presses Ctrl-C only while the prompt is closed. The change claims this behaviour in three places:- the
request_quitdoc comment; - commit 9c38d9d ("remains an unconditional, immediate quit … even while either prompt is open");
- the description ("exits unconditionally above every overlay").
AGENTS.md §3 requires a promise like this to be exercised by a test.
Confidence 85 · logic · Fix: extend dash-31 (or add a scenario) to pressq, wait for the prompt, press Ctrl-C and expect exit 130. Alternatively, pull arm selection into a pure function and unit-test it. - the
-
[new · reported, not gating] One dash-28 step cannot fail —
tests/e2e-cucumber/features/dash.featuredash-28,When the user declines the quit prompt/Then the managed model is still listed as running
The record's pid is the harness itself, and nothing on the decline path touches services. That Then holds even if declining did nothing at all. The step that actually tests the decline is the prompt-closed wait inside the When.
Confidence 85 · mechanical · Fix: drop this Then, or replace it with an assertion that chat is still interactive.
Non-blocking
-
[new] The
has_stoppable_managed_instance_withshort-circuit inquit_confirm_open_services_keydoes nothing, and its doc says it prevents drift —crates/rocm-dash-tui/src/app/mod.rs:800-823
When the short-circuit was removed, every lib test still passed. Thepositionclosure re-checks the samemanaged_ids.contains && is_servingcondition over the sameinstances, so the result is identical either way.
Confidence 100 · mechanical · Fix: remove it and its doc paragraph, or describe it as an early exit with no behaviour of its own. -
[new] Comments describe this PR's own earlier design as if it had been on main —
crates/rocm-dash-tui/src/registry_probe.rs:13-14,crates/rocm-dash-tui/src/ui/mod.rs(quit_confirm_body_with'sdebug_assert!comment)
The module doc says the probe "replaces the one-shot, launch-time registry readResolvedArgsused to carry (startup_has_live_service/managed_service_ids)". Neither field exists anywhere onprw-base(git grepis empty). Commit 9c38d9d added these strings only in that doc.ui/mod.rsalso refers to "this PR's retired launch-time snapshot". A reader on main will look for code that never existed.
Confidence 85 · mechanical · Fix: describe what the probe does, not what it replaced inside this PR. -
[new] On Windows and during startup, the prompt lists models that are still starting under "stop it first" —
crates/rocm-dash-tui/src/ui/mod.rs(quit_confirm_body_with,total == 0branch);apps/rocm/src/dash.rs(LiveManagedRegistryProbe::snapshot:managed_ids: discovery.seen)
discovery.seenincludesstarting/recoveringrecords (is_scrapeable_status).has_live_serviceis filtered byis_serving(), butmanaged_idsis not. Once one record is serving, starting records are listed beside it and inflate the overflow count. The stop command still works for them, so the impact is wording only.
Confidence 70 · logic · Fix: add a separateserving_idsto the snapshot, or filter in the probe. -
[new] The
ManagedRegistryProbedoc overstates how often it is called —crates/rocm-dash-tui/src/registry_probe.rs:62-67
It says the gate may callsnapshot()"on every render … for the entire life of the session" on Windows. In the dashboard it is called only fromrequest_quit, and fromquit_confirm_requestwhile the prompt is open. Per-frame calls happen only in the launcher, which uses the static probe.
Confidence 70 · mechanical · Fix: state the actual call sites. -
[new] The dash-31 comment cites the wrong README section —
tests/e2e-cucumber/features/dash.feature(dash-31 comment: README's "Interactive interfaces" section)
The Ctrl-C text it refers to is under README's "First run" section.app/actions.rscites it correctly.
Confidence 85 · mechanical · Fix: correct the section name. -
[new] The squash-commit body will carry an AI co-author trailer and review-round history — commits cf4d9eb, dd4579b, 4ec28b7, ad2c4b4, 1f76c5a
rocm-cli builds the squash body from these commit messages:- cf4d9eb ends with
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>. AGENTS.md §11 says "avoid AI-generated boilerplate footers". - Several messages narrate review rounds ("close round-8 review gaps", "rominf's automated review (CHANGES_REQUESTED at 86ab905)", "Closes rominf's round-5 review").
- 1f76c5a is a doc fix-up of the previous commit.
None of that describes the change to a reader of main.
Confidence 85 · mechanical · Fix: rewrite the squash message at merge so it states the feature, and drop the trailer. - cf4d9eb ends with
-
[new] The description's test plan omits dash-31 — PR description, "Test plan" / "Not verified"
Both sections list dash-25–dash-30. dash-31 (Ctrl-C while serving), added in cf4d9eb, is not mentioned.
Confidence 85 · mechanical · Fix: add dash-31 to the description.
Decisions for the author
- The dashboard trusts only the daemon once its first snapshot lands, even when that daemon may not read this host's registry — non-blocking-improvement
has_live_instance_withstops consulting the live registry read oncehas_received_snapshotis set. With--connectto a daemon that does not discover the localservices_dir, a model this host serves would not open the prompt. The localmanaged_idswould also be matched against another daemon'scontainer_ids. The probe is a live read, so OR-ing it in permanently would still track a stop. This was reasoned from the code but not run; confirm whether--connectto such a daemon is a supported case. - The quit prompt reuses the tool-approval chrome — tradeoff
Reuse keeps one modal path with a shared key policy. The cost is that the user sees "Review: … quit anyway?" with "Approve (y) / Deny (n)" buttons, andqinside the prompt means Cancel. Pressingqtwice therefore keeps the dashboard open. Confirm that this is acceptable, or letApprovalRequestcarry its own button labels. - The launcher's prompt is only as fresh as the last menu redraw — tradeoff
run_launcherreads the registry once per menu pass and wraps it inStaticManagedRegistrySnapshot. A stop or start from another terminal while the menu sits open is not seen. The "may still be serving" wording softens this.docs/manual-testing.md's "a fresh, on-disk read" says more than that.
Positive signals
- Every quit entry point (
q, the Esc-menu Quit row, the footerqchip,/quit//exit, the launcher'sq/Esc) goes through onerequest_quit. A grep found noshould_quit = truethat bypasses it. open_approvalrefuses while the quit prompt is open. Without that, aymeant to confirm the quit could approve a chat tool call instead.launcher_route_hosts_sessionderives the hosted flag from the route. Mutating it tofalsefailed dash-29 and dash-30.- dash-27 runs the printed
rocm services stop <id> --yesagainst a disposable process that carries realsupervisor_start_ticks. It asserts both the registry state and that the process exited, which is the §3 shape done properly. - The title, body and
shint are built from one registry snapshot (quit_confirm_request), so the parts of the prompt cannot disagree.
Deployment notes
None
What this covered
Read against prw-base…1f76c5a6. Every changed file was reviewed in two coupled groups by independent workers:
rocm-dash-tuiapp/ui/probe;apps/rocmdash/main, the e2e harness, features, README and manual-testing.
The dash-tui worker read every diff hunk and the surrounding code for the feature. It did not read the unrelated remainder of the largest files (app/mod.rs, about 3,960 lines, and ui/tabs/instances.rs, about 2,280). Commit messages, the PR description and issue #145 were also read.
Evidence runs:
- 21 single-branch mutations against
cargo test -p rocm-dash-tui --lib(baseline 868 passed / 3 known failures). The mutations that survived are cited above. - dash-25..31 run locally: 7 scenarios, 83 steps, all passed.
feature_naming: 4/4 passed; no@id/index collisions againstprw-base.- e2e mutations: the route mutation was caught; the simulated-gate mutation was hidden by
quit_and_wait.
CI rollup for the head is green on every reported check (skillscope skipped). No self-hosted GPU, Strix or WSL lane appears in the rollup.
Did not run:
- The prior-changes pass (comments on earlier PRs touching these files): reading review discussion is outside this run.
- Design review (Step 7) as independent workers: the session's two-worker cap was spent on the code passes, so the design questions were judged in the driver's own context and are not independent.
Not reconciled against any discussion on the PR.
Standing objections from round 8 — settled
The blind review above re-found neither objection. After its report was frozen, each was put to the reviewer together with the author's reply of 07:36 UTC. That check is anchored: it was given the objection to judge. Both kinds of evidence agree, so both objections are dropped:
- [gating] The
draw_focusedquit-prompt arm could never be drawn — resolved.focused_should_exitnow also checks!self.quit_confirm_pending()(app/mod.rs:898). The reviewer removed that clause, andfocused_should_exit_stays_shut_while_quit_confirm_is_pendingfailed. With the clause restored it passes. Read against the final code, the sentence in commit 4ec28b7's message about an "invisible, key-eating prompt" is now true. In the squash body it sits next to cf4d9eb's message, which describes the gate fix.
Observation: the arm is still reachable only by a future focused caller, because no production path opens the prompt with a focus set. The new test also pins that declining in a focused flow exits that flow, so for such a caller "declining returns to where you were" would not hold. - [reported, not gating] The sort-order test caught its regression only about half the time — resolved. With the
sort_byremoved, the test failed 12 times out of 12; restored, it passed 12 times out of 12.
Observation: the test's comment (ui/mod.rs:~1552) still narrates the earlier "9 runs passed 4" history instead of stating the invariant.
The reply on this pull request (issue comment 6187010008) reported fixes and asked no question. It was recorded with a 👍 reaction, which GitHub confirmed.
Round 9 at 1f76c5a: both standing objections are settled. The draw_focused quit-prompt arm is now reachable because focused_should_exit checks quit_confirm_pending, and a test pins that. The sort-order test now fails 12 out of 12 runs without the sort. Nothing new gates. See review 5530044935.
Pressing q (/quit, /exit, or the Esc-menu Quit row) used to exit rocm immediately even while a model was actively serving, leaving it running in the background with no warning and no obvious way to stop it (#145). Every quit entry point in the dashboard, chat, and the pre-dashboard launcher now routes through one shared gate (request_quit / resolve_quit_confirm_key) that opens a confirm prompt instead, naming the serving model and, for an instance this host's own registry manages, the exact rocm services stop <id> --yes command to stop it. Pressing s from the dashboard's copy jumps straight to the Services overlay, pre-focused on that instance, instead of only printing the command. Ctrl-C is deliberately NOT part of this gate: it remains an unconditional, immediate quit exactly as before, the same established meaning it already has over the chat-approval modal — the gesture a user reaches for to get out of a program must stay reliable even while either prompt is open, rather than risk being swallowed by one. Whether anything is "still serving" is answered by a live, on-demand read of the managed-service registry (ManagedRegistryProbe), not a snapshot frozen at launch: a model started mid-session through the dashboard's own Serve wizard is picked up the next time the gate is consulted, and a host whose dashboard client never connects to a daemon (Windows) still gets a correct answer for the whole session rather than only a launch-time guess. A --demo/--replay session never shows the warning (nothing is actually being served); real GPU hosts are unaffected by that short-circuit. A dashboard or chat session opened from the pre-dashboard launcher no longer asks twice: its own q/Esc exits straight back to the launcher menu, since the launcher's own gate - re-entered when control returns - is what actually governs process exit. Fixes #145 Signed-off-by: Jussi Elo <jussi.elo@amd.com>
dash-27's fixture pointed its managed-service record at the cucumber harness's own PID (as every other quit-confirm scenario does, so startup liveness checks see a live process), but dash-27 goes on to run the literal rocm services stop e2e-mock --yes the prompt prints - signalling the harness itself. Give it a disposable sleep child to point at instead. On a host whose dashboard-embedded daemon client never connects (Windows, for the whole session), quit_confirm_body could only ever print the generic "a model was serving" sentence: no stop command, no id, even though the live registry read already had one. It now lists each managed id and its stop command there too, just not the model's pretty name (that still needs a daemon snapshot). README and manual-testing.md no longer claim the s-to-Services hint or model name appear unconditionally. Also: honest doc comments for seed_registry_probe_and_hosting (the unit test covers only the field assignment, not the real event_loop call site - that's the e2e lane's job) and ManagedRegistryProbe (it reads raw on-disk status, not process liveness - matching the same tradeoff apps/rocm::dash::services_past_attempts already documents, not a new pid check); an assertion that was missing for the managed-instance bullet's model name; and a stale commit-hash reference in a doc comment. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…gaps The quit-confirm gate read the managed-service registry up to four times per single logical decision (the title, the body, and the "press s" hint each called ManagedRegistryProbe::snapshot independently), so a registry write landing between two of those reads could make them disagree. Most concretely: quit_confirm_open_services_key checked has_stoppable_managed_instance and then re-read the registry a second time to find which row to focus, so a service that stopped in between could pass the first check and silently fail the second, swallowing the s keypress with no feedback. has_live_instance_with/ has_stoppable_managed_instance_with now take an already-fetched snapshot, and every caller asking more than one registry-backed question in the same decision fetches once and shares it. quit_confirm_body's serving-instance listing also iterated state.instances.values() in HashMap order while the s key's row lookup sorts by container_id (services_manager::service_rows), so the Nth model named in the prompt wasn't provably the one s would focus. Both now sort the same way. The pre-dashboard launcher seeds its copy of the prompt from a raw, unverified on-disk registry read (no liveness check, by the same tradeoff services_past_attempts documents), but always took the unhedged "it keeps running" trailer - the same claim the dashboard's own pre-snapshot fallback already hedges for an equivalent no-verified- liveness case. The launcher path now gets the identical hedge instead of a present-tense claim the read can't back up. launcher_hosted at the two run_launcher escalation call sites (Chat, Dashboard) was two independent `true` literals with nothing tying them to the route; a worker confirmed flipping either to false stayed green across the whole suite. launcher_route_hosts_session derives both from the route itself. Closes rominf's round-4 review: adds the missing live-reread proof for the registry probe, an Esc-menu-Quit-with-nothing-serving test, a mixed serving/non-serving row test for the s key, Esc/q/d coverage for the launcher's own prompt, a process-exit assertion in dash-27 (not just the registry record), renames the two stop-command unit tests to drop the unprovable "working" claim and points them at dash-27, fixes the narrow-width popup test's vacuous model-name fixture, and corrects several stale doc comments the review flagged as contradicting the code they describe. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…w round-5 gaps
The dashboard's own render arm trusted state.instances as "daemon-verified"
once a real snapshot had landed, so it kept the unhedged "Still serving"
title and "It keeps running in the background after you quit." trailer in
that case. That trust doesn't hold: rocm-dash-daemon's registry discovery
(runner.rs) only ever promotes a record from Starting to Ready on a
successful scrape, and a failed scrape just logs and leaves the prior
status in place - nothing on the rocm dash path calls the CLI's own
liveness reconcile (refresh_managed_service_runtime_liveness, wired only
into load_managed_services). A service that crashed since its last
successful scrape still reads is_serving() == true here, same as the
launcher's raw on-disk read already did. Both the title and the trailer
now hedge identically ("May still be serving" / "may still be running")
regardless of which arm is asking, instead of the dashboard's copy making
a stronger claim its own data can't actually back up. Chose this over
adding a liveness check to the registry probe itself: that would
duplicate the CLI's own classification in a second place that could
drift from it, the same tradeoff ManagedRegistryProbe's own doc comment
already argues against for a different read in this same gate.
Also closes the remaining items from round 5:
- Added regression tests for the hedge (both title and trailer, once a
real snapshot has landed) and for the serving-instance listing's sort
order (container_id, not insertion/HashMap order) - the existing
overflow test's ids happened to already sort the same both ways, so it
couldn't have caught a regression there.
- Added dash-30: Chat opened from the launcher (the other hosted route,
alongside the dashboard) doesn't ask to quit twice either, closing the
one launcher_hosted propagation path that had no test reaching it.
- ui::draw_focused now renders the quit-confirm prompt too, matching
draw's own arm - defensive, since no focused flow opens it today, but
the renderer had no arm for it at all, which would have left a future
one that did showing an invisible, key-eating prompt instead.
- Consolidated the two byte-identical with_registry_probe test helpers
(app/mod.rs and ui/mod.rs each had their own copy) into the shared
app/test_support.rs, widened to pub(crate) for the cross-module reach.
- Fixed the stale doc comments the review flagged: plain-text references
to has_stoppable_managed_instance/nothing_is_currently_being_served
missing their _with suffix, has_live_instance's "registry read is no
longer consulted" overclaim (the read still runs every call; only its
result is ignored once a snapshot has landed), overflow_line's doc
claiming cap-sharing it doesn't actually provide, and launcher_state's
test-helper comment describing the real production caller's filtering
instead of its own permissive test-only behavior.
Kept the quit-confirm logic in app/mod.rs/ui/mod.rs rather than
extracting dedicated quit_confirm.rs files for this round: the AppState
methods are entangled with the general overlay system already living in
app/mod.rs, and the ui-side functions share their test module and
helpers with the rest of ui/mod.rs. Confirmed the placement was
deliberate per the review's own alternative, rather than risk a late
mechanical relocation touching ~20 existing tests in the same round as
the correctness fix above.
Closes rominf's round-5 review (45c597f).
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The bare launcher's own dispatch hardcodes run_launcher(false) (main.rs), so there is no --chat-mock flag to carry through the Chat escalation the way every other chat-opening step in this file does. dash-30's own fixture endpoint is real enough for chat's auto-detection to find, so opening Chat from the launcher always hits the one-time "use this endpoint?" consent prompt first; open_chat_from_launcher now waits for it and sends y to accept, instead of timing out waiting for the empty chat surface that never appears behind it. Accepting that prompt (accept_chat_consent, app/mod.rs) also sets chat_focused = true, unlike a fresh --chat-mock session which starts unfocused - the premise try_quit_chat's own "i focuses the input" comment relies on. quit_chat_back_to_launcher no longer sends i first: doing so typed a literal leading i into the already-focused input, submitting "i/quit" as a chat message (and getting the mock's canned reply) instead of ever running the /quit command, so the launcher never saw the session exit and the step timed out waiting for the front door. Verified locally: dash-30 and the rest of the dash-25..29 quit-confirm group all pass (cargo xtask e2e -t '@id:dash-quit-confirm-when-serving or ...', 6 scenarios, 75/75 steps) - this environment can run the @requires-os:linux lane directly, unlike most review rounds on this PR. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Four standing findings from rominf's automated review (CHANGES_REQUESTED at 86ab905), each confirmed against the current tree before fixing: - dash-27's e2e harness pointed the stop command at a disposable process recorded in its own `disposable_service_process` field, duplicating `recorded_process` without a `supervisor_start_ticks` token, so the stop only ever exercised `identity_state`'s best-effort `(None, _) => Matches` arm instead of the start-time-verified path a real launch writes. Dropped the duplicate field; dash-27 now reuses `recorded_process` and writes its process's real start-time token via a new `ServiceRecordOptions.supervisor_start_ticks`. - The title/trailer tests asserted the ABSENCE of retired exact strings ("Still serving", "It keeps running") that production no longer prints once hedged, so they could never fail regardless of what the code actually renders (confirmed by mutation: corrupting both title branches to the same string left every assertion green). Switched the title checks to `assert_eq!` and keyed the trailer checks on the current "may still be running" hedge instead. - `quit_confirm_body_with`'s `total == 0 && managed_ids.is_empty()` branch was unreachable: every `ManagedRegistryProbe` in this tree derives `has_live_service` and `managed_ids` from one `discover_managed_services` pass that always names an id for a record it counts as live. Removed the dead branch (and the only test that exercised the otherwise-impossible combination), and added a `debug_assert!` documenting the invariant for a future probe. - `docs/manual-testing.md`'s expected result still quoted the retired present-tense trailer; reworded to the hedge the prompt actually prints, along with the same stale wording in comments across `app/types.rs`, `dash.rs`, and `app/mod.rs`. Also fixed two related non-blocking findings in the same files: a "provably the Nth instance" overclaim about which row the `s` key focuses (it always focuses the first managed, still-serving row in `service_rows`' own order, not whichever position the prompt happens to print it at), and `ManagedRegistrySnapshot::has_live_service`'s doc comment, which didn't mention the `port == 0`/startup-phase exclusions `discover_managed_services` already applies. cargo test -p rocm-dash-tui --lib (870 passed), cargo check -p rocm -p e2e-cucumber --tests, cargo clippy -p rocm -p rocm-dash-tui -p e2e-cucumber --all-targets -- -D warnings, and cargo fmt all pass. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The focused-host exit gate could fire before draw_focused ever rendered a pending quit-confirm prompt, since open_quit_confirm closes every manager overlay first; focused_should_exit now also waits for the prompt to resolve. The sort-order regression test used too few ids to reliably catch a missing sort (a 3-element HashMap matches sorted order by chance often enough to mask it); it now seeds 8. Also corrects several stale or inaccurate doc comments and README/manual-testing wording, and adds an e2e scenario proving Ctrl-C still quits immediately with nothing asked even while a model is serving. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The previous commit fixed one of two paired sentences naming the call site (event_loop, not run_async) but left the other saying `run`, which never calls this function either — both now say `event_loop`, the actual (and only) call site. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Add a scenario proving `q` exits immediately with nothing serving, bypassing the shared `quit_and_wait` test driver whose own retry loop would otherwise auto-confirm a regressed prompt away. Extend the Ctrl-C scenario to also fire while the quit-confirm prompt is open, matching the match-arm order that ranks it above both the prompt and the approval modal. Replace dash-28's post-decline assertion, which held regardless of whether declining did anything, with a proof that chat stays interactive. Correct three doc comments: a short-circuit's "prevents drift" claim it can't actually provide (the row lookup it guards checks the exact same predicate), a module doc describing a replaced design that never existed on `main`, and a probe doc overstating how often it is called outside the dashboard's own quit-confirm render path. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The registry-probe doc rewrite from the last commit had its own inaccuracies: it omitted quit_confirm_open_services_key as a caller of snapshot(), and overstated the launcher distinction as "snapshot not called per-frame" when has_live_instance calls it unconditionally every render; only the I/O behind it differs by probe implementation. dash-28's reused GPU-health send step typed a leading "i" meant to focus the chat input, but the input was already focused from the preceding /quit + decline round trip, so that "i" landed as a literal character instead. The scenario still passed because the mock replies to anything that isn't the approval-trigger phrase, masking it. Added a focus-agnostic send step for that call site instead. dash-31's second half re-opened the dashboard and sent the quit keystroke immediately, with no gate confirming the event loop was reading yet — unlike every other open-then-act in this file, including its own first half. Added the same home-view wait the rest of the file uses. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Rebasing onto main (#638) inserted its own new dash-25 ("Adopt existing opens the folder browser...") at the same point this branch's first commit added its own dash-25..29 for the quit-confirm feature, so resolving that conflict shifted this branch's scenarios to dash-26..30. That shift didn't propagate to a later commit in this branch's own stack, which had added three more scenarios (chat/Ctrl-C/nothing-serving quit-confirm coverage) using the pre-shift numbers dash-30/31/32 - colliding with the just-renumbered dash-30 and leaving every comment that cross-references a scenario by number out of date. Finish the renumbering: dash-30 (chat variant) to dash-31, dash-31 (Ctrl-C) to dash-32, dash-32 (nothing serving) to dash-33, and update every doc comment in ui/mod.rs, e2e.rs, and dash_steps.rs that names the old numbers. Signed-off-by: Jussi Elo <jussi.elo@amd.com> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Jussi Elo <jussi.elo@amd.com>
5187a03 to
848db9d
Compare
tomastola
left a comment
There was a problem hiding this comment.
Read this against #145 and the earlier rounds. I'm not re-raising anything those settled (Ctrl-C staying ungated, Fixes #145, the registry probe, the launcher_hosted tradeoff).
Every quit entry point I could find (q, the footer q chip, the Esc-menu Quit row, /quit//exit, the launcher's q/Esc) goes through request_quit, confirm/cancel behave as documented, and the terminal restore reuses the existing exits. I ran cargo test -p rocm-dash-tui (all targets) and cargo test -p rocm --bin rocm, plus clippy with -D warnings, all green on 848db9d, and I ran a few single-branch mutations. These five were caught by the unit tests: dropping the launcher_hosted early return, always OR-ing the registry read, /quit bypassing the gate, the Esc-menu row returning true, and the probe counting any record. Three wiring points survive the unit tier and are held only by the e2e scenarios, which is what the comments say: the event loop's Approve → break, launcher_hosted seeded as false, and dash::run dropping the hand-off.
Please fix (description only)
- The scenario ids in the description are off by one. The last commit renumbered them to dash-26..dash-33, but the Test plan and "Not verified" still say dash-25..dash-32.
dash-25on this branch is the unrelated "Adopt existing…" scenario, so the description points a reader at the wrong test. Please update the ids (dashboard = dash-26, launcher = dash-27, stop command = dash-28,/quit= dash-29, hosted dashboard = dash-30, hosted chat = dash-31, Ctrl-C = dash-32, nothing serving = dash-33).
Non-blocking
starting/recoveringrecords don't count as serving, so quitting mid-load gives no prompt, and the supervisor is detached, so the model still comes up afterwards. See the inline comment.- A stop in flight still counts as serving. See the inline comment.
- Ctrl-C is a tradeoff to confirm (below).
- I'd like a scenario for the Esc-menu Quit row (AGENTS.md §3). See the inline comment.
- README paragraph is too heavy. See the inline comment.
- Squash message: 848db9d and 2abb72f carry
Co-Authored-By: Claude …trailers and several messages narrate review rounds ("close round-8…"). AGENTS.md §11 says to avoid AI footers, so please rewrite the squash message at merge to state the feature. - Still open from the earlier round: once the first snapshot lands, only
instancesdecides, so--connectto a daemon that doesn't read this host'sservices_dirwould not prompt for a model served here. Fine if that isn't a supported setup; a sentence saying so would do.
Ctrl-C and SIGTERM (crates/rocm-dash-tui/src/app/event_loop.rs:1106, tradeoff, not a change request)
Ctrl-C (and SIGTERM via await_termination) skip the prompt by design, and README and dash-32 say so, so I'm not asking to gate it. Just noting what it does to the PR's own goal, "won't happen by surprise": Ctrl-C is probably the most common way out of the dashboard. If you want to cover it without a prompt, a one-line notice on stderr after the terminal is restored, on any exit path where the probe says something is serving, would do it. Your call.
Positive
- One
request_quitgate for every key/menu/chip/slash/launcher path, with no strayshould_quit = true. open_approvalrefuses while the prompt is open, so aymeant for the quit can't approve a tool call.- dash-32's second half (Ctrl-C with the prompt open) and dash-33 (
qwith nothing serving, bypassingquit_and_wait's auto-y) close the masking gaps from the last round.
| let records = load_service_records(&self.services_dir); | ||
| let discovery = discover_managed_services(&records); | ||
| ManagedRegistrySnapshot { | ||
| has_live_service: discovery.svcs.iter().any(|svc| svc.status.is_serving()), |
There was a problem hiding this comment.
Non-blocking. Only ready/running count here, so a managed starting/recovering record never opens the prompt. But rocm serve --managed starts a detached supervisor (spawn_detached_no_inherit), so quitting mid-load leaves the model coming up after you've gone. That is the same surprise #145 describes, and it's the likeliest moment to quit: Serve wizard → q → launcher → q. The tests pin the exclusion on purpose (launcher_state_does_not_warn_on_a_starting_only_registry, resolved_args_reports_no_live_managed_service_when_none_are_ready_or_running), so I read it as a wording call (the prompt says "serving") rather than an oversight. Would you count managed starting records and say "starting" in the title/body? If you'd rather keep it narrow, say so in the description.
There was a problem hiding this comment.
Agreed this is a real gap, and the likeliest moment to hit it, per your framing. Kept narrow for this round rather than widening the gate: quit_confirm_body_with only ever says "serving", so counting starting/recovering means deciding new wording too, and the two tests you cited pin the current exclusion on purpose. Documented as an accepted tradeoff in the description (new paragraph right before "Test plan") rather than silently left — happy to pick this up as a follow-up if you'd rather not carry it as a known gap.
| return false; | ||
| } | ||
| self.instances.values().any(|i| i.status.is_serving()) | ||
| || (!self.has_received_snapshot && snapshot.has_live_service) |
There was a problem hiding this comment.
Non-blocking. A stop in flight still reads as serving. stop_internal_managed_service_confirmed_with writes stop_requested_unix_ms first, keeps the record ready/running while it terminates (its own comment says that can take around 40s), and only then flips the status. rocm-dash-daemon's ServiceRecord and the probe never look at that field. So s → stop → close the overlay → q inside that window asks "May still be serving — stop it first: rocm services stop " about something already stopping. The hedged wording keeps it true, but the remedy is already in flight. Traced from the code, not run. Treating a record with the marker set as not serving, or saying "stopping", would close it.
There was a problem hiding this comment.
Confirmed — traced it the same way: stop_requested_unix_ms is set and the record stays ready/running for the duration, and neither rocm-dash-daemon::registry::ServiceRecord nor LiveManagedRegistryProbe look at that field, so the gate can't see it. Not closing it this round — it needs either a new field on ServiceRecord or a different way to read in-flight state, more surface than this round's fix warrants — but it's now written up as an accepted tradeoff in the description rather than left silent.
The Esc-menu Quit row shared `request_quit` with every other quit entry point but had no scenario proving it, only unit tests. Add dash-34, driven by selecting Quit from the real menu rather than the `q` shortcut. Trim README's quit-confirm paragraph to what asks, what yes/no and `s` do, and Ctrl-C not asking; the Windows and hosted-session detail already lives in docs/manual-testing.md. Name the real `dashboard.tui.connect` config value, not a nonexistent `--connect` flag, in the known-gap doc comment on `has_live_instance_with`. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
|
Responding to the rest of the review (replies on each inline thread cover those individually; commit d32b144): Please fix — description scenario ids: fixed. The description now says dash-26 (dashboard), dash-27 (launcher), dash-28 (stop command), dash-29 ( Squash message / AI footer: agreed, and noted for whoever does the squash at merge time — this branch's own commits will keep their normal history, but the squash commit body should state the feature and drop both the
Ctrl-C/SIGTERM stderr notice: noted, no change for now — agree it's the most common way out and that a one-line notice is a reasonable follow-up, but not folding a new exit-path behavior into this round. |
tomastola
left a comment
There was a problem hiding this comment.
Re-reviewed the latest commit (the dash-34 Esc-menu Quit row scenario, the README rewrite, and the new doc comment on has_live_instance). The scenario drives the real Esc → menu → jj + Enter path and checks the registry afterwards; CI is green on this head. One small docs inline comment, nothing blocking.
Question for the author: the new "Known gap" comment on has_live_instance says that when dashboard.tui.connect points at a daemon that doesn't discover this host's services_dir, a locally served model never opens the prompt. That reads accurate against the code, but it isn't among the accepted tradeoffs listed in the PR description. Should it be listed there too, or is the code comment alone the intended home for it?
| The one exception is the dashboard's console for a **running** job, where | ||
| `Ctrl-C` keeps its existing meaning of "cancel this job" and does not quit; once | ||
| that job finishes, `Ctrl-C` quits there too. On a non-interactive terminal (or | ||
| that job finishes, `Ctrl-C` quits there too. If a model is still being served, |
There was a problem hiding this comment.
The PR description deliberately hedges the prompt ("may still be serving") because nothing on this path re-verifies liveness, but this paragraph states it as fact. Suggest matching the prompt's wording here (the "keeps running in the background" a few lines below could be "may keep running" for the same reason):
| that job finishes, `Ctrl-C` quits there too. If a model is still being served, | |
| that job finishes, `Ctrl-C` quits there too. If a model may still be serving, |


tests/e2e-cucumber/expectations.tomlfor the fixed ticket ID and removed/narrowed any now-stale xfail rows. — no rows reference issue [Feature]: Stop serving a model #145.docs/architecture.md. — N/A, this extends existingapp/modules; the live registry-read seam got its own file (crates/rocm-dash-tui/src/registry_probe.rs).Fixes #145
Summary
Quitting the dashboard (
q, the Esc menu'sQuitrow,/quit//exitin chat — includingrocm chat, which is the same dashboard with the Chat tab focused), or the pre-dashboard launcher (barerocm), used to exit immediately regardless of whether a model was still being served, leaving it running in the background with no warning and no in-app way to stop it.When at least one managed instance is actively serving, quitting now shows a confirm prompt first, naming the serving model(s) and — for any instance this host's own registry manages — the real
rocm services stop <id> --yescommand to stop it (a Docker- or Lemonade-discovered instance from an external daemon gets the warning but not that command, since it genuinely can't be reached that way). Declining returns to where you were; approving quits anyway, same as before — the model keeps running either way, it just won't happen by surprise. Neither the title nor the trailer assert present-tense that a model is serving: both say "may still be" rather than "is"/"keeps", because nothing on this path — not the launcher's raw on-disk read, and not the dashboard's own daemon-sourced instance snapshot either — is actually re-verified against the live process. The daemon's registry discovery only ever promotes a record forward on a successful scrape and never demotes one on a failed scrape, and nothing on therocm dashpath re-runs the CLI's own liveness reconcile (refresh_managed_service_runtime_liveness, wired only intoload_managed_services), so a model that crashed since its last successful scrape can still read as "serving" here. Hedging the claim, rather than adding a second, duplicate liveness check inrocm-dash-tui, matches the deliberate call already made onManagedRegistryProbe::snapshot's own doc comment: read the registry's belief, not a re-derived verdict that could drift from the CLI's own.From the dashboard's own copy of the prompt, pressing
sinstead jumps straight to the Services overlay, pre-focused on the serving model, so it can be stopped there through the overlay's own approval+job flow without quitting at all. The launcher's copy of the prompt doesn't offer this: it has no live Services overlay to jump to, so it shows the warning and the stop command but not theshint, rather than advertising a key it can't honor.Whether anything is "still serving" is answered by a live, on-demand read of the managed-service registry (
ManagedRegistryProbe), not a snapshot frozen at launch. This closes two real gaps: a model started mid-session through the dashboard's own Serve wizard is picked up the next time the gate is consulted (it used to get the warning but never the stop command or theshint until the session restarted), and a host whose dashboard client never connects to a daemon (Windows) still gets a correct answer for the whole session instead of only a launch-time guess. A--demo/--replaysession never shows the prompt — the data it displays is synthesized, so nothing is actually running to warn about.A dashboard or chat session opened from inside the launcher no longer asks twice: its own
q/Esc (or/quitin chat) now exits straight back to the launcher menu (since that's "back to menu", not "quit"), and it's the launcher's own gate — re-entered when control returns to it — that actually governs process exit and shows the one prompt that matters. Both of the launcher's hosted escalations — opening the full dashboard, and opening Chat — get this treatment identically, derived from the route rather than two independenttrueliterals at the call sites.The prompt reuses the existing approval seam (
ui/approval.rs) rather than introducing new overlay UI, and is wired at the same priority as the dashboard's other screen-owning overlay (the chat tool-call approval): it owns every keypress and mouse gesture ahead of any tab or manager, a chat tool call that surfaces for approval while the prompt is open is discarded rather than silently taking over input, and the footer's Esc chip reflects it instead of falling through to a generic action. The chat tool-call approval modal shares the same "owns the screen" gate, so it also swallows tab-bar and footer-chip clicks the same way the quit-confirm prompt does. The bare-rocmlauncher's in-place focused flows (Set up/Serve/Diagnose,ui::draw_focused) render the prompt too, defensively — no focused flow opens it today, but the renderer has an arm for it regardless, so a future one that did would not be left showing an invisible, key-eating prompt.The gate reads the registry once per decision rather than once per question: the title, the body, and the
shint all share a single snapshot, so a registry write landing between two reads can no longer make them disagree — it used to be possible forsto silently no-op if a service stopped between the "is anything stoppable" check and the row lookup. The prompt's serving-instance listing sorts bycontainer_id(the same key theskey's own row lookup sorts by), so the listing is stable across renders rather than ordered by whatever aHashMaphappens to yield — not thatsfocuses the Nth model named: it always focuses the first managed, still-serving row in that row lookup's own order, which can include an unmanaged (Docker- or Lemonade-discovered) entry the listing also shows.Ctrl-Cis not gated — it already exits unconditionally above every overlay, mirroring an external SIGINT, and stays that way here, except from a running job's console, where it keeps its existing meaning of "cancel this job" instead.Scope
The launcher front door (bare
rocm, before entering the dashboard) is a second, independent quit entry point from the dashboard's own — same underlying bug, same fix, same sharedrequest_quitgate (app/actions.rs). It renders the identical prompt (ui::launcher's own quit-confirm arm calls the sameui::quit_confirm_request) from its own, simpler key loop, with thes-to-manage jump out of scope there for the reason above. Chat opened from the launcher is the second hosted route through that same gate (run_chat's ownlauncher_hosted), alongside the full dashboard.The Esc-menu Quit row now has its own end-to-end coverage (
dash-34); the launcher's own Esc still relies on the same shared unit-level coverage as itsq, since it routes straight throughrequest_quitwith no menu of its own to navigate.Kept the quit-confirm logic in
app/mod.rs/ui/mod.rsrather than extracting dedicatedquit_confirm.rsfiles: theAppStatemethods involved are entangled with the general overlay system already living inapp/mod.rs(has_open_overlay/close_overlays), and theui-side functions share their test module and helpers with the rest ofui/mod.rs. A clean extraction would mean relocating that shared test infrastructure too, which felt like a separate, lower-risk-on-its-own follow-up rather than something to bundle into this round.Accepted tradeoff: a
launcher_hostedsession's ownq/Esc exits straight back to the launcher without consulting the dashboard's own gate at all (AppState::request_quitreturns immediately whenlauncher_hostedis set) — it's the launcher's own re-entered gate that actually decides whether to warn and whether to quit. That gate never offers thes-to-Services jump, so a user who quits a dashboard (or chat) session that was opened from the launcher loses that shortcut even though the full dashboard was open moments earlier, same as the launcher's own front-door prompt. Correctness here rests on an invariant held by the launcher's key loop rather than enforced by a type: every hosted route must actually return control to that loop rather than exiting on its own. A structured "how the session ended" return value threaded back throughrun/run_chatwould make that invariant explicit instead of implicit; not taken on here, to avoid widening this change's already broad surface for a design question that doesn't block the fix this PR makes.Accepted tradeoff: the gate only counts actively
ready/runningmanaged instances (LiveManagedRegistryProbe::snapshot'sis_serving()filter), notstarting/recoveringones.rocm serve --manageddetaches its supervisor, so quitting while a model is still coming up (e.g. straight out of the Serve wizard) leaves it loading unattended with no warning — the same surprise #145 is about, and arguably the likeliest moment to hit it. Kept narrow deliberately for this round: widening it means deciding what the prompt should say about a model that isn't being served yet (quit_confirm_body_withonly ever says "serving"), and the existing tests (launcher_state_does_not_warn_on_a_starting_only_registry,resolved_args_reports_no_live_managed_service_when_none_are_ready_or_running) pin the current exclusion on purpose. A follow-up, not taken on here.Accepted tradeoff: a stop already in flight still reads as serving.
stop_internal_managed_service_confirmed_withwritesstop_requested_unix_msand keeps the recordready/runningfor as long as the stop takes (around 40s by its own comment) before flipping the status; neitherrocm-dash-daemon'sServiceRecordnorLiveManagedRegistryProbelook at that field. Sos→ stop →qinside that window still asks to stop something already stopping. The hedged wording ("may still be serving") keeps the claim technically true, but the remedy it prints is already underway. Not closed here — it needs either a new field onServiceRecordor a different way to read in-flight state, which is more surface than this round's fix warrants.Test plan
cargo test -p rocm-dash-tui,cargo test -p rocm,cargo clippy -p rocm-dash-tui -p rocm --all-targets -- -D warnings,cargo test -p e2e-cucumber --test feature_namingall pass locally.qkey, the Esc-menuQuitrow, and the/quitchat command all open the prompt, and all still exit immediately with nothing serving (the regression guard for the common case); a launcher-hosted session (dashboard or chat) exits unconditionally even while a model serves; the prompt's y/n/Esc key handling, driven through the same verdict-resolution path the event loop and launcher actually call; a chat tool-call approval is discarded rather than opened while the prompt is pending; mouse drag/scrollbar-track/wheel/tab-bar/footer-chip input is swallowed while the prompt (or a chat approval) owns the screen; the startup-race fallback and the live registry read are each exercised directly, including a non-serving (Starting/Stopped/Error/Unknown) instance correctly not tripping the gate on every predicate that filters on it (has_live_instance,has_stoppable_managed_instance_with,quit_confirm_open_services_key, and the launcher's own static probe); the prompt's title and body are derived from the same "is anything still serving" decision, so the two can't print a contradiction, and both stay hedged ("may still be serving"/"may still be running") once a real daemon snapshot has landed, not only during the pre-snapshot fallback; the serving-instance listing is asserted sorted bycontainer_id, not insertion/HashMaporder; the stop command names the real service id and only appears for instances the registry actually manages, rendered through the realui::drawentry point for both the dashboard ("Press s" hint shown) and the launcher (hint absent);ui::draw_focusedrenders the prompt if opened, even though nothing opens it there today; a--demo/--replaysession never trips the gate even with a serving-looking instance in it, while--chat-mock(which only mocks the chat completion backend, not instance state) deliberately still does; andsopens the Services overlay focused on a genuinely stoppable managed row, verified by mutation.tests/e2e-cucumber/features/dash.feature:dash-26(dashboard) anddash-27(launcher) drive the prompt through a real pty — open, assert the real stop command text, decline, confirm — and assert viarocm services list --jsonthat the model's record is actuallyready/runningafterward.dash-26additionally asserts the "Press s" hint, pressess, and asserts the Services overlay opens showing the fixture's own service id;dash-27(the launcher) has noshint to assert, per the launcher's own copy of the prompt omitting it.dash-28goes one step further than the string match: it runs the actual printedrocm services stop <id> --yescommand, asserts via the same--jsonread that the service is no longerready/runningafterward, and asserts the real disposable process it pointed the record at has actually exited — not only that the registry record was rewritten (AGENTS.md §3 — a remediation command's claim is proven against the resulting state, not just its text).dash-29covers/quitin chat.dash-30covers a dashboard opened from the launcher: only one prompt appears (the launcher's), and it's the one that actually governs exit.dash-31covers the same thing for Chat opened from the launcher — the other hosted route, exercised through its own/quitslash command rather than a bareq.dash-32drives Ctrl-C through the real pty on both sides of the quit-confirm gate — beforeqhas ever been pressed, and again after it has opened the prompt — and asserts exit code 130 either way, since production routes Ctrl-C through one match arm ranked above both the approval modal and the quit-confirm prompt.dash-33covers the negative case directly, bypassing the sharedquit_and_waittest driver (whose own retry loop answers "y" to any quit-confirm prompt it finds, which would otherwise paper over a regression that opened one): with nothing serving,qmust exit on the very first keystroke, with the prompt never appearing at all.dash-34covers the Esc-menuQuitrow directly (AGENTS.md §3): Esc opens the menu, navigating to and activatingQuitasks the same prompt the other entry points do — previously relying on unit coverage alone, per the "Scope" section above before this round.ResolvedArgsand never mutates afterward, so a caching regression would otherwise pass all of them); an Esc-menuQuit-row test with nothing serving (the existing one only ever seeds aRunninginstance); a mixed serving/non-serving-row test for theskey, proving itspositionlookup'sis_serving()filter actually matters rather than the short-circuit above it doing all the work; and Esc/q/d-while-pending coverage for the launcher's own copy of the prompt.dash.feature/chat.featurescenarios open the dashboard (orrocm chat) with a model already serving, so the sharedquit_and_waittest driver needed a matching fix for both the dash and chat quit gestures: once the prompt is open,q/Qcancel it (same as any approval-style modal), so blindly resending the normal quit gesture would have flipped it open/closed forever (dash), or typed the remainder of/quitinto chat as a message (chat), instead of ever confirming it. Fixed once in the driver rather than touching each scenario.dash.featurescenario above (dash-26–dash-34) is tagged@requires-os:linuxand runs only on Linux lanes — none of them exercise the Windows-specific quit-prompt path (quit_confirm_body_with'stotal == 0branch with no daemon snapshot: ids listed without model names, the hedged trailer, noshint). That path is unit-tested only; the pty harness drivingdash.featurehas no native-Windows-console equivalent, so closing this gap would need a different harness, not just an added scenario.