Repository navigation
ROCMAI-827: start the background helper only while an automation needs it - #838
Conversation
juhovainio
left a comment
There was a problem hiding this comment.
Nothing here is broken in the code I could see, but I'm requesting changes because the PR only covers half of ROCMAI-827 and a few claims need tightening.
Scope vs the ticket. The ticket reports that rocm services stop leaves both the daemon and LemonadeServer.exe running, and accepts either stopping helpers once nothing needs them or saying plainly that they remain and how to stop them. This diff handles the daemon only. LemonadeServer.exe is untouched, services stop prints nothing about leftover helpers, and the "background processes are not stopped automatically" text in rocm uninstall is unchanged. Please either cover the remaining process / messaging, or narrow the PR description to "daemon only" and keep the ticket open for the rest.
Behavior change to confirm. With no watcher enabled (the default), the daemon no longer runs, so self-exited server records are no longer reconciled and nothing feeds the dashboard from it. Please confirm the dashboard and that reconciliation have another source, and update the rocm daemon help text, which still lists metrics and the webhook listener as unconditional (inline comment).
Race. The daemon exits on its next 5s tick after the last watcher is disabled. If a watcher is re-enabled inside that window, the already-running check still sees the old process and spawns nothing, then the daemon exits, leaving a watcher enabled with no daemon. Same shape if the daemon starts before the enabling config is saved. Worth confirming the ordering in the enable path and/or adding a re-check.
Tests. automation_loop_keeps_running_while_automations_are_enabled only proves the first tick (the interval fires immediately), so it would not catch an exit check that misbehaves later. I couldn't see a record that the new tests were shown to fail with their target broken. docs/manual-testing.md wasn't touched; if it describes the daemon lifecycle it should be updated. Also worth saying in the PR text that serve-28 only runs on the GPU lanes (still pending at the time of review) and only asserts the fresh-config case; the watcher-enabled-starts-the-helper direction is covered only by the unit test.
The automations-10 and serve-28 scenarios and docs/testing.md entries are in place, and the leak scan is clean. The PR also currently conflicts with main and needs a rebase.
| // disabled) there is no work left, so exit rather than idle | ||
| // forever as a detached orphan (ROCMAI-827). The autostart path | ||
| // starts it again when a watcher is enabled. | ||
| if !config.automation_daemon_enabled() { |
There was a problem hiding this comment.
The daemon now exits on its next tick after the last watcher is disabled. If a watcher is re-enabled inside that window, background_helper_already_running still reads the old state as running and spawns nothing, then this branch exits, leaving a watcher enabled with no daemon. Is there a grace/re-check, or does enable guarantee ordering?
There was a problem hiding this comment.
Fixed in abc0f3c. The daemon now decides to exit under the autostart lock: it re-reads the config after taking it, and only if still no watcher is enabled publishes "stopped" and clears its own spawn claim before releasing it. The autostart path no longer has a lock-free "already running" read, so an enable that lands in the window either makes the daemon stay or finds it stopped and starts a new one. enable already saves the config before ensuring the helper, so the "daemon started before the config was saved" case does not arise. The tick period and the two callbacks are injectable, so automation_loop_stays_when_a_watcher_is_enabled_inside_the_exit_window reproduces the window deterministically; the description lists which break turns each new test red.
| quiet: bool, | ||
| spawn: impl FnOnce(&AppPaths) -> Result<Result<u32>>, | ||
| ) -> Result<()> { | ||
| if !rocm_core::RocmCliConfig::load(paths)?.automation_daemon_enabled() { |
There was a problem hiding this comment.
automation_daemon_enabled() gates both the spawn and the exit. The tests set daemon_enabled = true as well as a watcher, so the predicate is not just "a watcher is enabled". Please confirm it matches the "no automation watcher is enabled" wording used in the README, doc comments and the exit log line.
There was a problem hiding this comment.
Agreed, they had drifted. automation_daemon_enabled() is now "any watcher is enabled", with the stored daemon_enabled flag no longer consulted, so the spawn gate, the exit, the "background checks" display and the README, doc-comment and log wording all say the same thing. automations enable and disable still keep the flag in step, and a test covers a config with only the flag set.
| /// - health-checks and auto-recovers managed local model servers | ||
| /// (Lemonade, vLLM) | ||
| /// (Lemonade, vLLM) when the `server-recover` watcher is enabled | ||
| /// - collects GPU thermal/VRAM metrics every 60s for the TUI dashboard |
There was a problem hiding this comment.
The list here still describes GPU metrics every 60s and the webhook listener as always-on work, but the loop now exits within seconds when no watcher is enabled (the default). Worth qualifying those too, and confirming the dashboard does not rely on the daemon being up.
There was a problem hiding this comment.
Updated: the rocm daemon help now says it runs only while a watcher is enabled, qualifies the server-recover and GPU-metrics bullets, says rocm dash does not need it, and no longer mentions a webhook listener (rocm daemon never starts one). On your second question, nothing relies on the daemon being up by default: rocm dash has its own embedded daemon and never reads the automation runtime state, and a server that exited is recorded by its own supervisor. The evidence is in the description.
nowycondro
left a comment
There was a problem hiding this comment.
Review — no blocking findings
Full review of the whole change.
Implements the daemon-orphan fix from ROCMAI-827 (the detached rocm daemon no longer outlives services stop when no automation watcher is enabled); the PR's own "Out of scope" section notes it does not touch the separate LemonadeServer.exe process named in the same ticket report (argued, independently of this review, to not be started by rocm-cli at all) nor add messaging for the still-persists-while-a-watcher-is-enabled case.
Blocking
None
Non-blocking
docs/testing.md's new nextest filter is miscounted as "the first pair" —docs/testing.md:1510,1514
cargo nextest run -p rocm -E 'test(background_helper_)'is a substring filter, andapps/rocm/src/service_launch.rscontains four test functions matchingbackground_helper_, not two: the two new ones (background_helper_not_spawned_when_no_automation_is_enabled,background_helper_spawned_when_an_automation_is_enabled) plus two pre-existing, unrelated ones (background_helper_already_running_true_for_live_pid,background_helper_already_running_false_for_dead_or_missing, testing double-spawn liveness detection). The following line calls this "the first pair," which is wrong for this command (the second command, filteringautomation_loop_, does correctly match exactly two tests). Confidence 90 · mechanical · Fix: narrow the filter to the two new test names, or reword to drop the "pair" claim and note the other two matches are pre-existing.- README.md line breaks the file's ~80-column hard-wrap convention —
README.md:833
The edited crash-restart sentence (...after a crash (with the \server-recover` automation enabled) inherits the daemon's environment...`) is 121 characters; every other line in the surrounding paragraph (and the file generally) is wrapped to 69-80 characters, including the PR's own newly-added paragraph a few lines below it (73-79 chars/line). The inserted parenthetical pushed this one sentence past the file's convention without being rewrapped. Confidence 95 · mechanical · Fix: rewrap the sentence to ~80 columns.
Decisions for the author
None
Positive signals
spawn_background_helper's process-spawn code is extracted behind an injectableimpl FnOnce(&AppPaths) -> Result<Result<u32>>closure (ensure_background_helper_running_with), letting the two new gating unit tests exercise the decision logic without ever launching a real detached process — a clean way to make a side-effecting autostart path unit-testable.- Both new unit-test pairs (
service_launch.rs's spawn-gate tests,service.rs's loop-exit tests) were traced and are not vacuous: each would fail if its corresponding gate/exit check were removed.
Deployment notes
- Behaviour change, called out in the PR itself: a
rocm daemonprocess started by an older binary is not affected by this change and keeps running until stopped by hand; only daemons started after this ships pick up the new self-exit behavior. - Running
rocm daemonby hand with no automation watcher enabled now prints its banner and exits immediately instead of idling in the foreground.
What this covered
Every file in the diff, completely, plus the surrounding code needed to judge them: crates/rocm-core/src/config.rs (AutomationsConfig, automation_daemon_enabled, watcher_config_mut), crates/rocm-core/src/automation.rs, apps/rocmd/src/watchers.rs, all call sites of ensure_background_helper_running*, all readers of AutomationRuntimeState, and the full history of the two logic-bearing files back to the original introduction of the "always spawn" behavior (504 commits reachable, non-shallow). Three independent passes were run: (1) the core gating/loop logic in apps/rocm/src/service_launch.rs, apps/rocmd/src/service.rs, apps/rocm/src/automations.rs, apps/rocm/src/main.rs; (2) the e2e feature files, step definitions, and docs (tests/e2e-cucumber/**, docs/testing.md, README.md, plus a check of docs/manual-testing.md for now-false claims — none found); (3) version history and code-comment-vs-code consistency. Verified directly: no failing CI check for this commit (all required checks, including the required E2E tests job covering automations-10, report SUCCESS; the GPU-only lanes covering serve-28 were queued, consistent with the PR's own note); the two reported findings above (exact test-name grep, exact line-length measurement); the Duration::from_mins usage against the pinned rust-version = "1.96" toolchain (compiles fine, feature stabilized in 1.91).
Investigated and refuted during the passes, below the reporting threshold: a possible TOCTOU window between the config-enabled check and the autostart lock (no production writer of watcher-enabled state exists outside the automations enable handler, which re-invokes the autostart check itself right after saving); the split automations.daemon_enabled boolean being settable independently of any individual watcher's enabled flag (true in principle, but no code path does this outside tests); lack of a dedicated scenario for "serve with a watcher already enabled still starts/keeps the helper" (the shared gating function is unit-tested for that branch and used identically by both callers, so a regression there is unlikely to go undetected, but it is a real coverage gap worth the author's attention even though it didn't meet this review's confidence bar).
Did not run: the Rust test suite itself (no cargo/rustc toolchain available in the review sandbox) — all conclusions on logic correctness are from tracing the code, not execution. Not reconciled against any other review or discussion on this PR.
c73ae26 to
abc0f3c
Compare
|
Pushed abc0f3c (squashed, rebased onto main: the rocmd refactor moved the daemon loop into
|
All points are addressed in abc0f3c and the updated description (scope narrowed to the daemon, ticket stays open for the rest); see the replies and the summary comment. A re-review is welcome.
juhovainio
left a comment
There was a problem hiding this comment.
No blocking correctness bugs found in the diff, but I'm requesting changes for a handful of things to settle before merge. The exit-race handling (re-reading the config under the autostart lock, publishing "stopped" before releasing it) looks sound.
automation_daemon_enabled()no longer consults the storeddaemon_enabledflag. Please confirm no other reader (status, chat/MCP automations tools, existing configs with only the flag set) depended on it, and say so in the PR body.FileLock::acquireblocks inside the async tick inrocmd.rocmd run/--helptext: please confirm nothing still promises a persistent supervisor, since the daemon now exits whenever no watcher is enabled.- AGENTS.md asks for
docs/manual-testing.mdto be updated alongside behavior changes; I didn't see it touched. - Two unit tests rely on fixed sleeps; minor flake risk.
- With a watcher enabled the daemon outlives
services stop, androcm uninstallstill doesn't say so. ROCMAI-827 asks to either stop helpers or say plainly what remains, so that half is still open (the PR body already says the ticket stays open). - The PR is currently conflicting and needs a rebase (the
serve-28renumber clash predicted in the PR body). E2E tests (Strix Halo, Windows, current)failed at "Run E2E tests on Strix Halo Windows"; logs weren't available yet, so please check whether it's the new daemon scenario or a lane flake. The W7900, MI300X and Strix Halo Ubuntu lanes were still pending.
Inline comments below.
| /// nothing to run. | ||
| pub fn automation_daemon_enabled(&self) -> bool { | ||
| self.automations.daemon_enabled || self.automations.watchers.values().any(|cfg| cfg.enabled) | ||
| self.automations.watchers.values().any(|cfg| cfg.enabled) |
There was a problem hiding this comment.
Dropping daemon_enabled from this predicate changes it for every caller. Please confirm no other reader (status, chat/MCP automations tools, existing configs where only the flag is set) depended on the flag alone, and note it in the PR body.
There was a problem hiding this comment.
Checked every reader, and the paragraph is now in the PR description. The stored automations.daemon_enabled flag is written only by automations enable and disable in apps/rocm/src/automations.rs, so for any config the CLI produced it already agreed with the watchers. The predicate is read by the automations status text, rocmd status, the loop exit check and the autostart gate; the chat/MCP automations tools and the Codex bridge snapshot never consulted the flag, and rocm dash never references it. Only a hand-edited config with the flag set and every watcher off behaves differently: it used to spawn a no-op daemon and now starts nothing.
| on_idle_observed(); | ||
| } | ||
| let _autostart_lock = | ||
| rocm_core::FileLock::acquire(paths.automation_autostart_lock_path())?; |
There was a problem hiding this comment.
FileLock::acquire blocks inside an async tick; if the holder stalls, this blocks a runtime worker (and the webhook task). Consider spawn_blocking or a bounded wait.
There was a problem hiding this comment.
Agreed. The daemon now takes the autostart lock with a non-blocking FileLock::try_acquire (new, in rocm-core) polled every 10 ms on the runtime. After a bounded 30 s it gives up, stays up and retries on the next tick, so a stalled holder cannot block a runtime worker or the webhook task, and the exit-race guarantee is unchanged. Two tests cover it: a task on the loop's single-threaded runtime keeps ticking while the loop waits on a held lock, and a stuck lock does not make the loop exit but a released one does.
| after a crash inherits the daemon's environment, not yours, so a forwarded | ||
| secret must also be set there. `ROCM_CLI_AIM_READY_TIMEOUT_SECS` raises the | ||
| after a crash (with the `server-recover` automation enabled) inherits the | ||
| daemon's environment, not yours, so a forwarded secret must also be set there. `ROCM_CLI_AIM_READY_TIMEOUT_SECS` raises the |
There was a problem hiding this comment.
This line is now overlong after the edit; please rewrap the paragraph.
| }); | ||
| // Let the enable path run up to the lock. Only the pass direction depends | ||
| // on this pause: a caller that skipped the lock would already be done. | ||
| std::thread::sleep(std::time::Duration::from_millis(300)); |
There was a problem hiding this comment.
Fixed 300 ms sleep to let the other thread reach the lock. Fine for the pass direction as documented, but polling until the thread is actually blocked would cut flake risk.
There was a problem hiding this comment.
Replaced with an observed event: ensure_background_helper_running_with now polls the lock and takes an on_lock_busy callback, and the test waits until the enable call has actually queued behind the lock. If a caller skipped the lock the test sees the thread finish and fails fast instead of waiting out its deadline.
| }); | ||
| // Give the loop ample time to reach the lock; if it ignored the lock it | ||
| // would be finished by now. | ||
| thread::sleep(Duration::from_millis(500)); |
There was a problem hiding this comment.
Same here: the fixed 500 ms sleep is a timing assumption; consider polling instead.
There was a problem hiding this comment.
Same change here: the test waits until the loop reports (via a new on_lock_busy hook) that it found the lock held, instead of sleeping 500 ms.
abc0f3c to
7d3a5b3
Compare
|
Pushed 7d3a5b3 (squashed, rebased onto main). Points in the review:
|
All points are addressed in 7d3a5b3 and the updated description (the LemonadeServer.exe half of the ticket stays open, as the description says); see the replies and the summary comment. A re-review is welcome.
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · 7d3a5b3
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 — no blocking findings
Full review of the whole change.
Implements ROCMAI-827 for the rocm daemon helper only. The PR's own text deliberately and explicitly leaves LemonadeServer.exe (the ticket's other named process) out of scope, with a stated reason (not started by rocm-cli) and says the ticket should stay open for that remainder. Jira currently shows ROCMAI-827 as Done, which is in tension with the PR's own "ticket should stay open" statement — worth the author or a maintainer confirming the ticket isn't meant to close on this merge.
Blocking
None
Non-blocking
- A newly introduced idle-exit check can stall the daemon's own webhook-dispatch and shutdown handling for up to 30s under lock contention —
apps/rocmd/src/daemon.rs:151(the_ = ticker.tick() =>arm),apps/rocmd/src/daemon.rs:179(acquire_autostart_lock(paths, &hooks).await?),apps/rocmd/src/daemon.rs:266-285(acquire_autostart_lock, bounded byAUTOSTART_LOCK_WAIT = Duration::from_secs(30)at line 19).
tokio::select!only polls its other arms (webhook-event receive, shutdown signal) once the chosen arm's body runs to completion; the idle-exit path now awaitsacquire_autostart_lock, which can poll for up to 30s before giving up, entirely inside the ticker arm. Confirmed via diff againstorigin/mainthat pre-PR this arm did only a quick config read/write with no comparable await, so this is new, not pre-existing. Concretely: if the autostart lock happens to be held by a concurrentserve/automations enablefor the full 30s (the code's own comment calls this "stuck"), an already-received webhook event sits undispatched and a Ctrl-C/SIGTERM is not acted on for up to 30s. The in-code comment ("a stalled lock holder cannot block a runtime worker or the webhook task") is accurate for the separate webhook-listener task, but doesn't fully hold for this loop's own webhook-dispatch and shutdown arms, which do stall. Low real-world likelihood (the lock is normally held only for a fast config-read + maybe one process spawn), and the 30s bound keeps it from hanging forever, which is presumably why it wasn't caught by the existing tests (they use injected fast ticks and don't specifically assert responsiveness during a stuck-lock window).
Confidence 85 · architectural · Fix: give the lock wait its ownselect!branch (e.g. spawn it and await a channel/oneshot) so the webhook-dispatch and shutdown arms stay pollable while it waits, or add a test that asserts shutdown/webhook responsiveness during a held lock. background_helper_remains_note's "a watcher is enabled" branch reports the helper as running without checking actual liveness, contradicting its own doc comment —apps/rocm/src/service_launch.rs:846-868.
The doc comment says the note is "derived from the same two facts the helper itself acts on: whether a watcher is enabled … and whether it is running," but theenabled-non-empty branch returns"...keeps running while an automation is enabled..."unconditionally — only the no-watcher fallback branch callsbackground_helper_already_running. If the watcher is enabled but the daemon process has died (spawn failures fromensure_background_helper_running_withare only logged, not propagated, per that function's own doc comment; nothing auto-restarts a crashed detached helper),rocm services stop/rocm uninstall --dry-runstill print "keeps running" and name the disable command, even though no such process exists right now. The existing test (stop_output_names_the_helper_that_outlives_it_only_when_it_does) only sets config state, never a runtime state, so it doesn't distinguish "watcher enabled + daemon alive" from "watcher enabled + daemon dead" — both produce the identical message today, which is arguably the intended policy-level wording, but it doesn't match the doc comment.
Confidence 72 · mechanical · Fix: either have theenabledbranch also checkbackground_helper_already_runningand vary the wording, or correct the doc comment so it no longer claims this branch is running-state-aware.
Decisions for the author
- File-lock polling (detect-and-exit) vs. a long-lived single supervisor — tradeoff. The chosen design (each tick re-reads config, decides exit under a file lock shared with the CLI-side autostart path) fits the existing detached-process architecture with a minimal diff and closes the race it sets out to close. A long-lived supervisor/OS-service model would remove the polling and the lock-contention window above entirely, at the cost of a much larger architectural change outside this PR's stated scope. Worth confirming the smaller fix is the deliberate choice rather than a deferred one.
- LemonadeServer.exe left out of scope — tradeoff, already argued in the PR text (not started by rocm-cli) and consistent with the ticket's "or say plainly what remains" acceptance criterion. Flagging only because Jira shows the ticket Done while the PR says it should stay open; confirm that's intentional bookkeeping and not an accidental early close.
Positive signals
- The race the PR sets out to close is solid: traced concretely that
automations enablealways saves config before calling the autostart gate, the gate's "already running" check is made only after taking the shared lock with no lock-free fast path, and the daemon re-reads config and publishes "stopped" before releasing that same lock — both orderings (enable wins the lock first vs. daemon wins it first) leave the system consistent, and this is exercised bybackground_helper_is_started_when_the_exiting_daemon_publishes_stoppedplus theautomation_loop_*tests inapps/rocmd/src/daemon.rs, none of which rely on fixed sleeps (they wait on injected hook callbacks instead). - The
automations-11e2e scenario genuinely drives a liverocm daemonprocess and polls its real pid/AutomationRuntimeState/rocm daemon --statusoutput rather than asserting a log line in isolation — traced the step definitions directly to confirm this. - Every behavioral claim checked against the code held: the "5 second" tick interval, the exact
helper: started background automation daemonanduninstall does not stop itstring literals, and thedocs/testing.mdnextest filter commands all match the actual code (the prior round's filter-miscount and README line-wrap findings are both fixed in this version).
Deployment notes
- Behaviour change, stated in the PR and verified: a
rocm daemonstarted by a pre-upgrade binary is unaffected and keeps running until stopped by hand; only daemons started by this binary pick up the new self-exit behavior. rocmd run --automations-enabled --local-webhook-port <port>on a default (no watcher enabled) config now exits within seconds instead of staying up — anything scripted around that invocation needs to enable a watcher first.
What this covered
Every file in the 18-file diff (6b175eee…7d3a5b3, +1119/-80) read completely, or — for the handful of very large files (apps/rocm/src/main.rs ~9.6k lines, apps/rocm/src/services_text.rs, crates/rocm-core/src/process.rs, crates/rocm-core/src/config.rs) — every touched region plus its direct callers/callees, logged per-file by each pass. Three passes ran: (1) core gating/concurrency (service_launch.rs, daemon.rs, process.rs, config.rs, automations.rs) and (2) CLI wiring/messaging (main.rs, cli.rs, services_text.rs, uninstall_plan.rs) each ran as an independent fresh-context worker; (3) docs/e2e coverage (README, docs/*.md, the two .feature files and their step definitions) could not be spawned as an independent worker — three attempts all hit the platform's 20-concurrent-subagent cap, saturated by unrelated sessions, not by any failure of the pass itself — so the driver ran that pass directly instead; it is disclosed here as not independent. The driver additionally ran impact-analysis sweeps (grep for stale "persistent daemon" claims across docs/skills — none found; completeness check of every render_service_action_result call site), a partial history pass (git log -S on daemon_enabled to rule out a legacy-upgrade hazard; diffed daemon.rs against origin/main to confirm the select!-arm finding is newly introduced, not pre-existing), and the design-question pass.
CI read live for the exact reviewed commit: all 25 check runs green (build-and-test, clippy, coverage floors, Windows build+test, Sphinx docs, lychee markdown links, commit-signatures, E2E tests + consolidated report); no GPU-hardware-lane check (Strix Halo/W7900/MI300X) appears in that set, consistent with the PR's own note that the GPU-gated serve-33 scenario runs outside it — its actual pass/fail status could not be independently confirmed here. No Rust toolchain was available in this sandbox; no test was executed directly, so conclusions on test/logic correctness are from tracing the code plus the green CI, not from a run.
Did not run: "prior changes" pass (review comments on earlier PRs to these files) — no practical way to retrieve historical review threads for these specific files within this review's scope.
Unavoidable exposure: this review's mandatory claim-check (pull_request_read method=get/get_reviews) necessarily returned the PR's own description and the full bodies of the prior human reviews on this PR before any code-level finding was reached. Nothing from that text was used to derive a finding — both findings above are not mentioned in the PR description or in any prior review on this PR. The description and ticket (ROCMAI-827, retrieved directly by key) were read for the intent line above only after findings were otherwise settled from the code. Not reconciled against the discussion on this PR.
🤖 Generated with Claude Code
juhovainio
left a comment
There was a problem hiding this comment.
Requesting changes. I found nothing that is broken in CI (all checks are green), but a few things should be settled before merge. Also note GitHub currently reports this branch as having merge conflicts, so it needs a rebase.
Ticket scope. ROCMAI-827 names both rocm daemon and LemonadeServer.exe as left running after services stop. This PR only deals with rocm daemon; nothing stops or mentions the Lemonade server process in the stop or uninstall output. Either cover it (stop it, or say plainly that it remains and how to stop it), or say in the PR body that it is split out.
Behavior changes to confirm and call out in the PR body
automation_daemon_enabled()no longer readsautomations.daemon_enabled. A saved config withdaemon_enabled=trueand no watcher enabled used to mean a persistent daemon; now it exits on its first tick. A foregroundrocm daemon/rocmd runused for debugging exits immediately unless a watcher is enabled.serveno longer starts the daemon by default. The old comment inrun_attached_servicesaid the daemon reconciles a self-exited server's record. Please state which of those duties only applied under a watcher; anything that did not is now silently lost.- Only
run_approved_service_actionand the attached Ctrl-C path print the helper note. Please confirm no other stop surface should carry it.
Messages vs. state. The "keeps running while an automation is enabled" note is printed whenever a watcher is enabled, without checking the helper is actually running, and the uninstall idle-case text contradicts itself (see inline). Per the repo's rules, derive the text from the same state the behavior depends on.
Tests
- serve-33's state check can pass vacuously before a fresh daemon publishes state (inline).
- automations-11 spawns a real detached daemon; please confirm World teardown kills it if an earlier step fails before the watcher is disabled.
- Please say that the new tests were checked by breaking their target (e.g. restore the lock-free fast path, or replace the
try_acquireloop with a blocking acquire) and went red.
Minor: the lock wait in ensure_background_helper_running_with has no deadline, unlike the daemon side (30 s). It matches the old blocking acquire, so not a regression.
The structure is good: one predicate shared by spawn, exit and display, exit decided under the autostart lock, test seams that avoid sleeps, and docs/--help updated consistently.
| .filter(|(_, watcher)| watcher.enabled) | ||
| .map(|(id, _)| id.as_str()) | ||
| .collect(); | ||
| if !enabled.is_empty() { |
There was a problem hiding this comment.
The note says the helper "keeps running" whenever a watcher is enabled, without checking that it is running. Derive the wording from background_helper_already_running as well, or soften it, so the message cannot disagree with the state.
There was a problem hiding this comment.
Fixed in 14c90e1: the note is built from both facts (watcher enabled, helper running). Enabled and running says it keeps running and names the disable command; enabled but not running says so and that the next rocm serve or rocm automations enable starts it; running with nothing enabled says it exits on its own; neither prints nothing. One test pins the exact text for all four combinations, with the running fact planted as a state file, and another checks the services stop output.
| spawn: impl FnOnce(&AppPaths) -> Result<Result<u32>>, | ||
| on_lock_busy: &dyn Fn(), | ||
| ) -> Result<()> { | ||
| if !rocm_core::RocmCliConfig::load(paths)?.automation_daemon_enabled() { |
There was a problem hiding this comment.
This config load now runs after run_attached_service has already spawned the child, and propagates with ?, whereas background_helper_remains_note uses unwrap_or_default. An unreadable config would fail serve after launch; treat it as "no watcher" here or load it earlier.
There was a problem hiding this comment.
Agreed, fixed: an unreadable config now counts as "no watcher" in ensure_background_helper_running_with, so serve cannot fail after it has launched the child. background_helper_autostart_treats_an_unreadable_config_as_no_watcher covers it, and the test goes red when the load propagates with ? again.
|
|
||
| if let Some(note) = crate::service_launch::background_helper_remains_note(paths) { | ||
| plan.warnings | ||
| .push(format!("{note}; uninstall does not stop it")); |
There was a problem hiding this comment.
For the idle case (no watcher enabled, helper about to exit) the note already says it exits on its own, so the "; uninstall does not stop it" suffix contradicts it. Only append the suffix for the enabled case.
There was a problem hiding this comment.
Right, it contradicted the idle text. "; uninstall does not stop it" is now appended only when a watcher is enabled and the helper is running; an idle helper gets only the "exits on its own" text and a helper that is not running gets no uninstall warning. A test asserts the exact strings for each case.
| ); | ||
| // The printed line is checked together with the state: the helper never | ||
| // published a runtime state, and its status reports it not running. | ||
| let state = rocm_core::AutomationRuntimeState::load(&world.app_paths()) |
There was a problem hiding this comment.
A freshly spawned daemon may not have published its runtime state yet, so this check can pass vacuously. Also assert that the autostart claim file (automation_autostart_claim_path) does not exist, since the spawn writes it synchronously.
There was a problem hiding this comment.
Done: the step also asserts the autostart claim file does not exist (the spawn writes it synchronously, and a unit test shows it exists after a spawn). This scenario only runs on the GPU lanes, so I did not run the step red locally; the unit tests cover the claim file.
7d3a5b3 to
14c90e1
Compare
|
Pushed 14c90e1 (squashed, rebased onto main; the serve scenario is now
I did not look into the Jira status of ROCMAI-827 from this PR; it should stay open for the Lemonade half. |
All points are addressed in 14c90e1 and the updated description (the Lemonade half of the ticket stays open, as stated); see the replies and the summary comment. A re-review is welcome.
juhovainio
left a comment
There was a problem hiding this comment.
Looks good; no blocking findings. Exit is decided under the autostart lock with a config re-read, the note is derived from the same state that drives behavior, and docs are in sync.
Non-blocking:
- Claim format
<pid> <ms>is parsed in rocmd and written in rocm with no shared helper, and the test hand-writes it. - An unreadable config is reported as "no automation is enabled" in the stop/uninstall note.
- New error paths in the daemon exit arm leave
running=truepublished. - CLI-side autostart lock loop has no deadline.
- The
services stopnote and uninstall warning only have unit tests; the uninstall warning looks coverable by a mock-lane scenario.
Also: ROCMAI-827 is already marked Done, but the PR says the LemonadeServer.exe half stays open, so that half has no open tracking. serve-36 (GPU lane) was not seen running in CI.
| text.split_whitespace() | ||
| .next() | ||
| .and_then(|pid| pid.parse::<u32>().ok()) | ||
| == Some(std::process::id()) |
There was a problem hiding this comment.
Claim format "<pid> <ms>" is re-parsed here separately from the writer in apps/rocm/src/service_launch.rs; a format change would make this release a silent no-op. Consider one shared helper/constant (e.g. in rocm-core) and have the test write the claim through the real writer.
| paths.ensure()?; | ||
| RocmCliConfig::default().save(&paths)?; | ||
| std::fs::write( | ||
| paths.automation_autostart_claim_path(), |
There was a problem hiding this comment.
The test hand-writes the claim instead of using the producer, so nothing ties the writer to this consumer.
| exit_attempt = None; | ||
| // `None`: the lock stayed held for the whole bounded wait. Stay | ||
| // up; the next tick decides again. | ||
| if let Some(_autostart_lock) = attempt? { |
There was a problem hiding this comment.
attempt? (and the config load just below) returns with running = true still published; the new stop note and uninstall warning read that flag. Consider recording stopped on this error path.
| /// [`helper_note_for`] over the facts on disk. An unreadable config counts as | ||
| /// "no watcher enabled". | ||
| pub(crate) fn background_helper_note(paths: &AppPaths) -> Option<HelperNote> { | ||
| let config = rocm_core::RocmCliConfig::load(paths).unwrap_or_default(); |
There was a problem hiding this comment.
unwrap_or_default() makes an unreadable config read as "no automation is enabled", and the printed note then asserts that as fact. Consider omitting or qualifying the note on a load error.
| break lock; | ||
| } | ||
| on_lock_busy(); | ||
| std::thread::sleep(std::time::Duration::from_millis(10)); |
There was a problem hiding this comment.
This CLI-side lock loop has no deadline, unlike the daemon side (30 s); a stuck lock holder would hang serve / automations enable. Intended, or worth a bound?
nowycondro
left a comment
There was a problem hiding this comment.
🤖 Automated review (Claude Code) · 14c90e1
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 — no blocking findings
Full review of the whole change.
Implements the rocm daemon (background automation helper) half of ROCMAI-827: the helper now starts only while an automation watcher is enabled and self-exits once none is, closing the "detached daemon outlives rocm services stop" orphan. The PR's own text explicitly leaves LemonadeServer.exe (the ticket's other named process) out of scope, with a stated reason (not started by rocm-cli), and says the ticket should stay open for that remainder. Jira shows ROCMAI-827 as Done (retrieved directly by key), which is in tension with the PR's own "should stay open" statement — worth a maintainer confirming that bookkeeping is intentional rather than an early close.
Blocking
None
Non-blocking
docs/testing.md:1586wraps well past the surrounding paragraph's own convention —docs/testing.md:1586
The new paragraph (lines 1571–1590) wraps prose at roughly 65–81 characters per line, and line 1586 ("bad config from failing aserveafter its launch. The user-visible side isautomations-11in") runs to 96 visible characters while every neighboring line in the same paragraph stays ≤81. Confidence 80 · mechanical · Fix: rewrap the line to match the paragraph's own width.- Stray double blank line at a scenario boundary —
tests/e2e-cucumber/features/model_serving.feature:564-565
Two consecutive blank lines separateserve-35from the newserve-36comment block; every other scenario boundary in the file uses exactly one. Harmless to Gherkin parsing, inconsistent with the file's own formatting convention. Confidence 85 · mechanical · Fix: drop the extra blank line.
Decisions for the author
- LemonadeServer.exe left out of scope — tradeoff, already argued in the PR text (not started by rocm-cli, lives in its own Startup-folder shortcut) and consistent with the ticket's "or say plainly what remains" acceptance criterion. Flagging only because Jira shows ROCMAI-827 Done while the PR says the ticket should stay open for that remainder — confirm that's intentional bookkeeping, not an accidental early close.
Positive signals
- The exit/autostart race the PR sets out to close is solid: traced that the daemon's self-exit and the CLI's autostart "already running" check share one lock (
automation_autostart_lock_path), the daemon re-reads config and publishesrunning=false(and clears its own spawn claim) strictly before releasing that lock, and the CLI side has no lock-free fast path — both orderings (anenablewinning the lock first vs. the exiting daemon winning it first) leave the system consistent, each exercised by a dedicated test (automation_loop_stays_when_a_watcher_is_enabled_inside_the_exit_window,background_helper_is_started_when_the_exiting_daemon_publishes_stopped). - The lock wait is modeled as its own
tokio::select!arm (a pinned, boxed future) rather than an inline.awaitinside the ticker arm, so a stuck lock holder cannot delay the loop's own webhook-dispatch or shutdown handling — directly exercised byautomation_loop_shuts_down_while_waiting_for_a_held_autostart_lockandautomation_loop_gives_up_a_stuck_lock_wait_and_exits_once_it_is_free, both traced to fail under the natural "inline await" or "blocking acquire" mistakes. background_helper_already_running's liveness check (state.running && process_is_running(pid)) is what every behavior-affecting caller (autostart gating, theservices stop/uninstallnote) consults, not the raw persisted flag; the note-generation tests (helper_note_follows_watcher_enabled_and_helper_running) exercise all four combinations of watcher-enabled × actually-running by writing a real runtime-state file, not just config.- New tests are deliberately non-vacuous: for every new gate/exit check, a corresponding test was traced against the reverted or mutated branch and confirmed to go red (spawn-gate removal, lock-acquire removal, claim-release removal, exit-condition-always-true, lock-free fast path restored).
- Docs (
README.md,docs/architecture.md,docs/automations.md,docs/manual-testing.md,rocm daemon --help/rocmd run --help/status text) were updated in the same change with no stale "persistent supervisor" claims left in the files checked.
Deployment notes
- Behaviour change, stated in the PR and confirmed in the code: a
rocm daemonprocess started by a pre-upgrade binary is unaffected and keeps running until stopped by hand; only daemons started by this binary pick up the new self-exit behavior. rocmd run --automations-enabled --local-webhook-port <port>on a default (no watcher enabled) config now exits within seconds instead of staying up; anything scripted around that invocation needs to enable a watcher first.
What this covered
All 19 changed files read completely at the PR head (d7faa84e…14c90e1f, fetched read-only via refs/remotes/pr/838, never checked out), plus surrounding/calling code. Three independent blind fan-out workers covered: (1) the core gating/concurrency logic (service_launch.rs, daemon.rs, process.rs, config.rs, automations.rs) — no findings ≥70; (2) CLI wiring/messaging (main.rs, services_text.rs, uninstall_plan.rs, cli.rs) — no findings ≥70; (3) docs and e2e coverage (README, docs/*.md, both .feature files and their step definitions) — the two non-blocking findings above. None of the three workers were shown the PR description, ticket, or any review discussion. I separately re-read daemon.rs and the automation_daemon_enabled()/background_helper_already_running predicate chain directly, cross-checked every claimed non-diff reader (chat.rs, mcp.rs, host_snapshot.rs, codex_bridge.rs) for the flag/predicate, and ran a git log history pass on service_launch.rs/daemon.rs (confirms this module has a recurring history of race/recovery-focused review-fix commits, consistent with the scrutiny this PR's concurrency design received).
CI verified live for this exact commit: all 25 required/listed checks (build-and-test, clippy, Windows build+test, coverage floors, E2E tests + consolidated report, commit-signatures, CodeQL, docs build, etc.) report SUCCESS; Skill checks (skillscope) is SKIPPED, which is advisory-only per this repo's own AGENTS.md and not yet a required context. No GPU-hardware lane (covering serve-36) appears in that set, consistent with the PR's own note that it runs only on gated lanes.
Unavoidable exposure, disclosed per this review's own protocol: this review's mandatory pre-work claim-check (pull_request_read get/get_reviews, required before any review effort to detect a duplicate at the live head commit) necessarily returned the PR description and the full bodies of every prior human and automated review on this PR before any code was read. To keep findings blind, every item above was either produced by a fan-out worker that never saw that text, or re-derived and verified independently against the code before being included. Several topics visible only in that already-exposed text (a prior-round finding about a select! arm stalling shutdown/webhook handling, a note-branch not checking real liveness, an unbounded CLI-side lock-wait, and a daemon error path that could leave a stale running=true record) were investigated directly against the current code: the first two are now fixed in this commit (confirmed structurally — the lock wait is its own select arm, and the note-generation now threads a real liveness-checked running bool through every branch) and are credited above as positive signals rather than restated as open findings; the latter two I could not cleanly attribute to blind derivation on my part (I had already seen them named in the exposed review text before examining that code), so rather than claim them as independent findings I am omitting them — a maintainer weighing them should rely on the discussion already on this PR, not on this review. The ticket (ROCMAI-827) was retrieved directly by key via the tracker tool for the intent line only, after findings were otherwise settled from the code. Not reconciled against the discussion on this PR.
🤖 Generated with Claude Code
14c90e1 to
547677b
Compare
|
Pushed 547677b: rebased onto main again (the GPU-lane serve scenario is now |
…s it `rocm serve` started the background automation helper (`rocm daemon`) on every launch, and the helper's loop ran until it was signalled. With every automation watcher off, which is the default, the helper has no work at all: crash recovery is the opt-in `server-recover` watcher. So a serve left a detached no-op process behind long after `rocm services stop` stopped its service, and nothing ever stopped it. - The autostart shared by `serve` and `automations enable` now spawns the helper only when an automation watcher is enabled. The predicate, automation_daemon_enabled(), is "any watcher is enabled", which is what the spawn gate, the exit and the user-facing wording all say; the stored daemon_enabled flag alone no longer counts. - The helper's loop exits on its own once no watcher is enabled, which makes disabling the last watcher the way to stop it. It exits by itself, so no process is signalled and no PID identity check is involved. - The exit is decided under the autostart lock: the helper re-reads the config after taking it, and only if still no watcher is enabled publishes "stopped" and clears its own spawn claim before releasing it. The autostart path has no lock-free "already running" read. Together with enable's existing order (save the config, then ensure the helper), an enable that lands while the helper is about to exit either makes it stay or finds it stopped and starts a new one, instead of leaving a watcher enabled with no daemon. - `rocm daemon` help no longer lists GPU metrics and the webhook listener as unconditional work. The `LemonadeServer.exe --silent` seen next to it in the report is a separate Lemonade Server install, started at logon from its own Startup shortcut. rocm-cli neither starts nor stops it. Tests: unit tests on both halves (autostart gate with an injected spawn; loop exit, persistence over several ticks, the exit window reproduced deterministically, and the lock and claim handling), mock-lane scenario automations-11 (a live helper exits after its last watcher is disabled), and GPU-lane scenario serve-40 (serving with no watcher enabled starts no helper). Review round 2: the helper takes the autostart lock with a non-blocking try-lock polled on the async runtime (bounded; it stays up if the lock is stuck) instead of a blocking acquire, and the lock tests wait on observed events instead of fixed sleeps. `rocm services stop` and the `rocm uninstall` plan say when the helper outlives them and how to end it (disable the enabled watchers). `rocmd run` help and status text no longer promise a persistent loop, docs/automations.md and docs/manual-testing.md cover the lifecycle, and the serve scenario is serve-40, the next free number on main. Review round 3: the helper note is derived from both whether a watcher is enabled and whether the helper is running (four cases, exact text pinned), the uninstall suffix is added only where it is true, an unreadable config no longer fails a serve after launch, the idle-exit lock wait is its own select arm so shutdown and webhook dispatch stay live, the serve scenario also checks the autostart claim, and the e2e World stops a helper left behind by a failed step. Signed-off-by: Eugene Volen <eugene.volen@amd.com>
547677b to
1eb574a
Compare
|
Pushed 1eb574a: ported onto main after the |
main took serve-40/41 (ROCMAI-827 #838 and the OOM-guidance scenario), so these are renumbered serve-42/43 once more; the @id: tags are unchanged and remain the stable reference. Review finding, and a real one: the "child died before its readiness wait even started" branches on both the launch and restart paths stamped `failed` without first asking whether a stop had been requested — contradicting `mark_service_failed`'s own documented precondition, and unlike every sibling branch (`finalize_managed_launch_readiness`, `record_restart_readiness`, the attached `ServerExited` arm), which all consult it. A `rocm services stop` landing in that ~200ms post-spawn window killed the child we had just spawned and was then reported as a crash. The launch branch's own comment already anticipated a concurrent stop writing its marker in "this same narrow window"; it simply never read it. Both branches now check first and report the stop instead. On the restart side the same branch discarded `update_restarting_record`'s return value, so a CAS that detected a concurrent writer (returning `None`, having deliberately written nothing) was followed by an unconditional engine-state stamp anyway — and since `refresh_from_engine_state` reads that file straight back into the manifest, the clobber the CAS had just prevented came back through the other door. The engine-state write is now gated on the manifest write having actually landed. Also adds the Windows half of the liveness coverage: `managed_child_liveness_windows_tracks_a_real_child_through_termination` spawns a real child, asserts it reads as running, terminates it, and asserts it stops reading as running and stays that way. `ManagedChildLiveness::Windows` is the crash-detection primitive on the reported platform and cannot be constructed off Windows, so the Unix test could not reach it and the scripted-engine scenarios run the Unix arm — it had nothing exercising it, while its sibling `spawn_detached_with_log` did. Signed-off-by: Roman Sirokov <roman.sirokov@amd.com>
Problem
After
rocm serveandrocm services stop, a detachedrocm daemonkept running indefinitely on native Windows, doing nothing.Scope of this PR: the daemon only. ROCMAI-827 also reports
LemonadeServer.exe --silent(run from%LOCALAPPDATA%\lemonade_server) staying alive. That process is not started by rocm-cli: a separate Lemonade install puts a Startup-folder shortcut that launches it at logon, nothing in rocm-cli references that path, rocm-cli's Lemonade lives in its own data folder, and rocm-cli neither starts nor stops it, so this PR leaves it alone and the ticket should stay open for it. The ticket's other accepted outcome, saying plainly what remains and how to stop it, is done for the daemon:rocm services stop(and the Ctrl-C stop in attachedserve) prints anote:line, and therocm uninstallplan warns, when a helper is running or a watcher is enabled.Root cause
ensure_background_helper_running_quiet(apps/rocm/src/service_launch.rs) spawnedrocm daemonon everyserve, whatever the config said, and the daemon loop (apps/rocmd/src/daemon.rs) ran until signalled. All automation watchers are off by default, so with nothing enabled the daemon had no work, and nothing ever stopped it. The daemon's own status text already describes the intended design as "on-demand by default, persistent only with background features".Change
serveandautomations enablestarts the daemon only when an automation watcher is enabled.automation_daemon_enabled()now means "any watcher is enabled", which is what the spawn gate, the exit, the "background checks: on/off" display and the wording in the README and log lines all say; the storeddaemon_enabledflag alone no longer counts. I checked every reader of that flag and of the predicate. The flag is written only byautomations enable(sets it) anddisable(clears it when no watcher remains), inapps/rocm/src/automations.rs, so for any config the CLI produced the flag and the watchers already agreed. The predicate's readers are the automations status text andrender_daemon_text(apps/rocm/src/automations.rs),rocmd statusand the loop's exit check (apps/rocmd/src/daemon.rs) and the autostart gate (apps/rocm/src/service_launch.rs). The chat and MCPautomationstools and the Codex bridge snapshot read the runtime state and event log, or serialise the raw config, so they never consulted the flag, androcm dashnever references it. Only hand-edited configs with the flag set and every watcher off behave differently: they used to spawn a no-op daemon and now start nothing.enable's existing order (save the config, then ensure the helper), an enable that lands while the daemon is about to exit either makes it stay or finds it stopped and starts a new one, instead of leaving a watcher enabled with no daemon.FileLock::try_acquirepolled every 10 ms on the runtime, not a blocking acquire inside the async tick. After a bounded 30 s wait it gives up, stays up, and retries on the next tick, so a stalled lock holder cannot block a runtime worker or the webhook task, and the exit-race guarantee still holds.select!arm, so the webhook-dispatch and shutdown arms stay live while it waits (a tick that finds the config idle starts one wait, and a tick that finds a watcher enabled abandons it);automation_loop_shuts_down_while_waiting_for_a_held_autostart_lockshuts the loop down with the lock still held.LoopHooks), so the loop tests run in milliseconds and reproduce the exit window deterministically. The two lock tests wait on an observed event (the loop found the lock held; the enable call queued behind it) instead of fixed sleeps.rocm services stop(and the Ctrl-C stop in attachedserve) prints anote:line, and therocm uninstallplan adds a warning, built from two facts: whether a watcher is enabled and whether the helper is running. Enabled and running: "keeps running ...rocm automations disable <watcher>" (the uninstall text adds "uninstall does not stop it"). Enabled but not running: it says so and that the nextrocm serveorrocm automations enablestarts it. Not enabled but running: it exits on its own (no uninstall suffix). Neither: no note. An unreadable config counts as "no watcher", soservecannot fail after it has launched the child. The assistant'sstop_servertool and the dashboard's stop action runrocm services stop, so they carry it;services restartkeeps the service,services removeandpruneonly delete records, and the MCPstop_servertool in rocmd stops in-process and cannot call therocmcrate, so adding the note there would duplicate the logic and is left as a follow-up.rocm daemon --help(no longer lists GPU metrics and the webhook listener as unconditional work; saysrocm dashdoes not need it), therocmd runflag help, banner and status text (nothing promises a persistent supervisor any more),docs/automations.md(rocmd run --automations-enabled --local-webhook-port 18080on a default config now exits within seconds, so it says to enable a watcher first), a new lifecycle section indocs/manual-testing.md,docs/architecture.md, README (Automations, and crash restarts noted as needingserver-recover), a misleading comment in the attached-serve path,docs/testing.md(filter names fixed, README sentence rewrapped).Nothing relied on the always-running daemon.
rocm dashuses its own embedded daemon (rocm-dash-daemon) and never reads the automation runtime state; the service records of a server that exited are updated by its own supervisor and refreshed whenrocm servicesor the dashboard reads them; the watcher handlers (GPU metrics,server-recover) run only for enabled watchers. The remaining readers of the daemon's state (automations status, the chat and MCP automations tools) only display it and tolerate "not running". The idle daemon also used to collect a GPU snapshot every 60 s for nothing.Behaviour changes:
automations.daemon_enabled=trueand no watcher enabled used to mean a persistent daemon. It now means nothing: the helper starts only while a watcher is enabled, and a running one exits on its first tick without one. The same goes for a foregroundrocm daemonorrocmd run --automations-enabledused for debugging, which exits within a tick unless a watcher is enabled (docs/automations.mdsays to enable one first). The flag is still written byautomations enableanddisable, so CLI-produced configs were already consistent.therock_update_due,server_recover_due,gpu_metrics_due,gpu_thermal_protect_due, andevaluate_watchers_for_eventsskips disabled watchers). The helper never reconciled self-exited records on its own, becausepersistence::load_managed_servicesin rocmd is read-only: a self-exited server's record is settled by its supervisor (supervise_servicecallsrecord_supervised_exit,apps/rocmd/src/supervise.rs) and on every read (refresh_managed_service_runtime_liveness,apps/rocm/src/managed_service_status.rs). The stale comment inrun_attached_servicesaying otherwise is replaced.rocm daemonby hand with no watcher enabled now prints its banner and exits instead of idling. Daemons already started by an older binary keep running until ended by hand.Tests
rocm,rocmd): the spawn gate in both directions; the loop persisting over several ticks while a watcher is enabled and exiting on the tick after it is disabled; a watcher enabled inside the exit window keeps the loop alive; the exit waits for the autostart lock and clears its claim; and an enable that finds the exiting daemon already stopped starts a new one.automation_loop_stays_when_a_watcher_is_enabled_inside_the_exit_windowautomation_loop_exit_waits_for_the_autostart_lock_and_clears_its_claimautomation_loop_keeps_running_while_automations_are_enabledand the exit-window testbackground_helper_is_started_when_the_exiting_daemon_publishes_stoppedbackground_helper_not_spawned_when_no_automation_is_enabledbackground_helper_not_spawned_when_no_automation_is_enabledautomations-11(@id:automations-helper-stops-when-no-watcher-is-enabled, mock lane, runs in the requiredE2E testsjob; the harness teardown now stops a helper it left behind if an earlier step fails, verified by making the scenario fail while the helper was alive): enabling a watcher starts a live daemon and prints thehelper:line; disabling it makes that process exit androcm daemon --statusreports it not running. This is the only scenario for the watcher-enabled-starts-the-helper direction.serve-40(@id:serve-stop-leaves-no-background-helper,@requires-gpu @requires-engine:lemonade): serving with a fresh config starts no daemon, prints no helper line, and leaves no autostart claim file (the spawn writes it synchronously, so the check cannot pass before a fresh daemon has published state). It runs only on the GPU lanes and asserts only the no-watcher case.cargo nextest run -p rocm -p rocmd(1444 passed), clippy forrocm,rocmdande2e-cucumber(including--test e2e),cargo fmt --check, the automations scenarios against debug builds: pass.Notes
serve-40is the next free number on main (afterserve-36..serve-39), so a later merge may renumber it.automations-10is taken on main by the torn-audit-tail scenario, henceautomations-11here.E2E tests (Strix Halo, Ubuntu, …)lanes ran out at 45 minutes with no logs on this PR, and the same two lanes fail the same way on several unrelated open PRs.Not covered by a scenario
The
services stopnote has unit tests for the message and the state but no scenario, because it needs a real service record.