Repository navigation
Conversation
d1e9c4e to
0bebf62
Compare
r0x0r
left a comment
There was a problem hiding this comment.
Solid, well-scoped fix for EAI-8014. Placement is correct (after the dry-run/empty-plan early return and the confirm gate, before removal), the abort is fail-closed, and it reuses the verified process-tree termination path so a service only counts as stopped once every recorded process is confirmed gone. CI is fully green including the GPU/Strix Halo/Windows E2E lanes.
Leaving a few resolvable threads: one meaningful test gap on the abort path plus a couple of minor nits. None are blocking.
…t stops them (EAI-8014) `rocm uninstall` reported completion while a managed model server — including a publicly-bound, GPU-holding vLLM endpoint — kept running, then deleted the binaries and service records needed to stop it, leaving only a manual PID kill. Stop every live managed service before removing anything. If any cannot be confirmed stopped, abort non-zero and remove nothing so the recovery tooling stays in place. Reuses the verified process-tree termination path used by `rocm services stop`. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
0bebf62 to
e2015fb
Compare
michaelroy-amd
left a comment
There was a problem hiding this comment.
Reviewed at e2015fb1a9393a6cc3e757805c02dfc1ed09b81b. The stop-before-remove ordering and the new removal gate are sound, and all 17 required checks pass. Two blocking gaps remain:
-
load_managed_servicessilently skips a service record whenserde_json::from_slicefails. That lets uninstall proceed without attempting to stop a live service represented by a corrupt manifest, which contradicts this change's fail-closed guarantee and can recreate the reported outcome. Propagate the parse error with the manifest path and add a regression test proving uninstall removes nothing on malformed service state. -
The user-visible uninstall contract has no Cucumber scenario in this PR. The cited
uninstall-stops-what-it-managesscenario is only in still-open PR #241, so it does not cover this branch. Add the scenario here, or explicitly make this PR depend on #241 and rebase after it lands.
Please address both and re-request review.
rominf
left a comment
There was a problem hiding this comment.
I went through this at e2015fb1. The direction is right and the two helpers are cleanly separated so the abort branch is unit-testable — that's a good call. All checks are green and it's MERGEABLE.
I agree with both blocking points already raised: load_managed_services still does if let Ok(mut record) = serde_json::from_slice(...) at main.rs:15001, so a corrupt manifest for a live service is still invisible to the new gate — which directly contradicts the fail-closed doc comment you added at :13891-13895; and there's still no Cucumber scenario, with the cited uninstall-stops-what-it-manages id present nowhere in the tree (I grepped) and #241 still open and now CONFLICTING, so depending on it means waiting for a rebase there first. I won't repeat the detail on those.
What I want to add is that the abort is not the clean no-op its message claims, and this is worse than the wording nit already noted. Beyond that: the stop pass runs even when the uninstall isn't removing the tooling, the plan warning overcounts, and the .ok() swallows the diagnosis. Details inline.
One I looked at and decided isn't worth a comment on its own: refresh_managed_service_runtime_liveness demoting a record to stopped on dead tracked PIDs while an engine grandchild keeps serving. On Linux that's mostly covered — collect_process_tree walks /proc and signal_process_scope gets tree: true, so descendants do get signalled on the stop path. The Windows gap is real though, and I've folded it into the comment on the liveness skip.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · e2015fb
Summary
Stops live managed services before rocm uninstall deletes the binaries and service records needed to stop them, aborting the uninstall with nothing removed when any service cannot be confirmed stopped. The fix itself is sound and fails closed in the right direction — Needs work, because the ordering guarantee it introduces has no test that pins it. Verified: the ordering is guaranteed, not incidental (uninstall.rs:45-48 runs before the for entry in &plan.actions removal loop at uninstall.rs:50, and both calls use ?, so a failed stop returns Err and the loop never executes — nothing is removed); a partial failure is recoverable and re-runnable (services confirmed stopped are written back as status = "stopped", so managed_service_is_live skips them on the next attempt and only the failed ones are retried); already-dead services are correctly skipped rather than counted as failures; but on the revert question, all five new tests still compile and pass if the four wiring lines in uninstall.rs are deleted — I confirmed by grep that uninstall::uninstall() is called only from CLI dispatch (main.rs:23, main.rs:1972) and from no test. I read the code and the caller-supplied CI state; I did not build or run the suite. Blocking: 1 · Non-blocking: 5.
🚫 Blocking (must fix before merge)
apps/rocm/src/main.rs:26777-26887,apps/rocm/src/uninstall.rs:45-50— nothing tests the thing the PR fixes. The five new tests exercise the two new helpers in isolation; none callsuninstall(), so none proves that stopping happens before removal or that a failed stop actually prevents removal. Delete thestop_managed_services_before_uninstall/uninstall_removal_gatecalls fromuninstall.rsand the entire test suite stays green — the ordering guarantee rests on code inspection alone. One test is weaker still:uninstall_skips_already_dead_managed_service(main.rs:26809) asserts only thatstoppedandfailedare both empty, which is exactly what astop_managed_services_before_uninstallgutted toOk(ManagedServiceStopReport::default())returns — it cannot distinguish "correctly skipped a dead service" from "did nothing". This also runs intoAGENTS.md§3: the change alters user-observable behavior (new abort with a new error message, newstopped N managed service(s) before removalline, servers killed during uninstall), and §3 states plainly that a unit test on an internal helper does not discharge the requirement for a Gherkin scenario intests/e2e-cucumber/features/— I checked, and no scenario there registers a managed service (install_lifecycle.feature:115-124is the only uninstall scenario and would pass identically with or without this fix). Concrete fix: makeuninstall()(or an extracted, injectable inner function) testable and add a test that seeds a plan with a real file plus a service whose stop cannot be confirmed, then asserts the call returnsErrand the file still exists — that single assertion is what pins the ordering. Add or extend a lifecycle scenario for the observable path, or, if it can only run on a gated lane, name the lane in the PR text per §3. I could not read the PR body from the checkout, so if it already names an@idor states the gap, treat that half as satisfied.
Non-blocking
apps/rocm/src/main.rs:13928-13938— a service that can never be confirmed stopped (EPERM on a process owned by another user, or a GPU worker wedged in uninterruptible sleep where SIGKILL does not land) makesrocm uninstallpermanently impossible: there is no override inUninstallOptions(main.rs:16114), and the suggested recoveryrocm services stop <id> --yesroutes through the samestop_internal_managed_servicewith the same outcome. Refusing is the right default, but consider a documented override flag and mentioning the privilege/manual case in the message.apps/rocm/src/main.rs:13903-13908—stop_internal_managed_service(...).ok()discards the underlying error, so the abort message names the service but never says why the stop failed; carrying the error string intofailedwould make the dead end diagnosable.apps/rocm/src/main.rs:13891-13895vs15001— the doc comment claims fail-closed discovery, butload_managed_servicessilently skips any record whose JSON fails to parse (if let Ok(mut record) = ...), so a corrupt manifest for a live, GPU-holding server is invisible to the gate and uninstall proceeds — the exact failure this PR closes.apps/rocm/src/main.rs:13749-13798— the stop is serial and unbounded in aggregate: ~10 s grace twice per PID, up to two PIDs per service, plus the engine-side stop, so uninstall can sit silent for roughly a minute per live service after the confirmation prompt; printingstopping <id>…before each would stop it reading as a hang.apps/rocm/src/main.rs:16193— the plan warning now promises "their servers will be stopped before removal" for every record, including ones alreadystopped/failedthat the gate skips entirely; scoping the count to live records would keep the plan truthful.
…rify what is still serving (EAI-8014) Review follow-up on the managed-service stop gate: - Extract the stop-then-remove step so the ordering guarantee is testable, and cover it: an unconfirmed stop (or a stop pass that cannot run) now has a test proving the planned paths are still on disk after the abort. Deleting the stop call fails those tests. - Add the user-visible scenario the change was missing: a lifecycle Gherkin scenario plants a running local server and asserts uninstall reports completion and the server is gone. - Confirm stops against reality, not only recorded PIDs: any recorded endpoint that still accepts connections after the stop fails the gate. This covers the Windows case where a surviving engine grandchild keeps the port while the record reads stopped. - Fail the gate on a service manifest that cannot be parsed, instead of skipping it silently as listing does. - Carry the stop error into the abort message, and say which services were already stopped (their endpoint keys are gone) so the abort is not read as nothing happened. - Run the stop pass only when the uninstall actually removes the binaries or the service records; a cache-only run keeps the tooling and leaves servers alone. - Report the live subset in the plan warning, so it does not promise to stop servers that are not running. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 1f9ca82
Summary
Makes rocm uninstall stop every managed model server before it deletes the binaries and service records needed to stop them, aborting with nothing removed if any stop cannot be confirmed. Verdict: Needs work — the prior blocker is genuinely resolved, but the new verification pass introduces a hard-lockout path and a plan message that no longer matches behaviour. Verified: the prior finding is FIXED — uninstall.rs:96 seeds a real file plus an unconfirmable stop and asserts both the error and that the file survives (I confirmed remove_path (main.rs:16553) really would delete it, so the assertion is not a tautology), uninstall_skips_already_dead_managed_service now pins an exact stopped vec against a live service sharing the directory rather than two empty collections, and the AGENTS.md §3 gap is closed by @id:lifecycle-linux-uninstall-stops-managed-server, which runs in the default Linux Acceptance install lifecycle job (ci.yml:238, E2E_ONLY_LIFECYCLE=1) and fails if the wiring is reverted (the sleep 600 child survives and try_wait times out); on the revert question, 10 of the 12 new unit tests are revert-sensitive, the two exceptions being removal_proceeds_once_every_managed_server_is_confirmed_stopped and uninstall_abort_says_which_services_it_already_stopped, which a no-op-success gate and a message-only change respectively would keep green; on the 2 failing checks, windows-build-and-test is a real, actionable failure (blocking item 3 below — not a flake), while the ReadTheDocs check fails for a reason not reachable from this container and this diff touches no docs sources, so I record it as unexplained rather than dismissed. No prompt-injection or instruction-shaped content was found anywhere in the diff. Blocking: 3 · Non-blocking: 5.
🚫 Blocking (must fix before merge)
-
apps/rocm/src/main.rs:13941-13969— the TCP "reality check" loop iterates every loaded record, skipping only those already inreport.failed. Records thatmanaged_service_is_liverejected in the first loop (main.rs:13913) are therefore judged purely by whether something answers on their recordedhost:port. Stopping a service rewrites the manifest withstatus = "stopped"(main.rs:13825-13834) and never deletes it, so stale records accumulate indefinitely with their old ports. Any unrelated local listener that later occupies such a port (vLLM's documented default is 8000) makesstop_managed_services_before_uninstallsynthesise a failure,uninstall_removal_gate(main.rs:14040) hard-bail!s, and nothing is removed. There is no override:UninstallOptions(main.rs:16252) has no--force, andyesis consulted only byconfirm_uninstall. The failure is deterministic, so every re-run repeats it, and the error's suggested recovery (rocm services stop <id> --yes) does not apply to an already-stopped record — the only escape is hand-deleting JSON under the services directory, which the message never mentions. Fix: apply the samemanaged_service_is_liveguard in the second loop (or require a live recorded PID to corroborate a bare port hit), and add an explicit documented override for when the gate's signal is wrong. -
apps/rocm/src/main.rs:16337-16350— the plan warning states "…have a running server; those servers will be stopped before removal" wheneverlive > 0, but the stop pass runs only whenplan_removes_recovery_tooling(main.rs:16367) is true (apps/rocm/src/uninstall.rs:44-51). On a cache-only run (--keep-binaries --keep-data), or a dev-binary-layout run without--force-dev-binariesplus--keep-data, the warning is printed before the confirmation prompt and no server is ever stopped. This contradicts both the comment immediately above it ("claiming otherwise would make the plan describe work uninstall never does") and the commit message's claim that the warning "does not promise to stop servers that are not running". Fix: pass the same recovery-tooling predicate into the warning and word it accordingly when the plan leaves the tooling in place. -
CI:
windows-build-and-testis red and this PR must resolve it. The job checks out the PR-merged-into-main commit, which includestests/e2e-cucumber/tests/feature_naming.rs(added upstream after this branch diverged, absent from this checkout). Itsscenario_names_are_indexed_sequentially_per_featurerequires every scenario ininstall_lifecycle.featureto be namedlifecycle-NN - …; the newScenario: Linux - uninstall stops the local server it manageshas no such prefix. Fix: rebase onto currentmainand renumber the new scenario into the sequence. (Verified from the CI log and by confirming the guard test's absence from this branch; theTest (affected crates)job pins the raw head SHA, which is why it stays green and why this cannot be dismissed as unrelated.)
Non-blocking
- No unit test drives
uninstall()itself — deleting thestop_managed_services_then_removecall and inlining the old loop is caught only by the e2e scenario; a thin test overuninstall()would close the last of the original finding. uninstall_refuses_when_a_service_record_cannot_be_parsed(main.rs) is#[cfg(target_os = "linux")]despite touching no PID or process state — it would run fine everywhere and the corrupt-manifest gate is not Linux-specific.ManagedServiceRecord::write(rocm-core lib.rs:7168) does a plain non-atomicfs::write; a concurrent read during overwrite can make a healthy record look unparseable and spuriously abort the gate. Write-temp-then-rename would close it.build_uninstall_planusesload_managed_services(...).unwrap_or_default()(main.rs:16328) while the real gate propagates that error, so the plan/dry-run can show "no managed services" for a run that will in fact abort.- The ReadTheDocs check is failing and unexplained; the diff touches no docs sources, but that should be confirmed rather than assumed before merge.
…s-success-while-leaving-a Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
main adopted a per-feature sequential scenario index (`lifecycle-NN - `) enforced by tests/e2e-cucumber/tests/feature_naming.rs, which this branch predated. The new uninstall scenario carried no index, so the merged PR ref failed windows-build-and-test. Place it after the existing uninstall scenario as lifecycle-10 (leaving main's 01-09 untouched) and shift the Windows block to 11-23 to keep the indexes sequential in declaration order. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
69aa6c6 to
6b697a3
Compare
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 6b697a3
Summary
This PR makes rocm uninstall stop the managed servers it manages before deleting the binaries and service records needed to stop them, and adds unit, integration and e2e coverage for that ordering. Verified: finding 1 (hard lockout) STILL OPEN and byte-identical to head 1f9ca82f; finding 2 (plan warning promises unperformed work) STILL OPEN and byte-identical; finding 3 (Windows scenario-naming guard) FIXED — I ran cargo test -p e2e-cucumber --test feature_naming, 4/4 pass, and a sweep found no stale references to the renumbered scenarios; the only work since the held review was a merge of main plus the renumbering commit, so nothing new was introduced, and every added test fails or stops compiling if the production change is reverted (details below) — except that one of them locks in the finding-1 defect as intended behaviour. Blocking: 2 · Non-blocking: 4.
🚫 Blocking (must fix before merge)
1. apps/rocm/src/main.rs:15570-15591 — the port-reachability loop still has no liveness guard; a recycled port deterministically bricks uninstall.
The second pass in stop_managed_services_before_uninstall iterates every loaded record, skipping only ones already in report.failed, then synthesises a failure for any whose recorded host:port accepts a connection. Verified unchanged since the held review (same code, same comment, only shifted by the merge).
Why it bites: managed_service_is_live (main.rs:16576) is a pure status check — ready|running|starting|recovering. load_managed_services (main.rs:16748) returns every *.json under the services dir with no age pruning, and refresh_managed_service_runtime_liveness (main.rs:16715) demotes a record to a non-live status when its recorded pids are gone while persisting it. services stop likewise writes the record back rather than deleting it (main.rs:15446-15456), and rocm services list --all exists precisely to show "failed, stopped, and old service records". So stale records carrying their old ports are the normal steady state, and there is no services remove/prune subcommand to clear them (ServicesCommand is List|Logs|Stop|Restart, main.rs:798). Any unrelated process that later binds a recycled port makes the gate bail!, and UninstallOptions (main.rs:17878) has no override (force_dev_binaries is unrelated). The abort tells the operator to run rocm services stop <id> --yes — which for an already-stopped record neither changes the record nor frees the foreign port, so every retry fails identically.
Fix: apply managed_service_is_live(record) in the second loop as well (or require a live recorded pid to corroborate a bare port hit), and add a documented override flag so an operator is never locked out. Note that main.rs:30729 (uninstall_refuses_while_a_recorded_endpoint_still_accepts_connections) deliberately writes a status = "stopped" record with a live listener and asserts the gate fails — that test currently encodes the lockout as the contract and must be reworked alongside the fix.
2. apps/rocm/src/main.rs:17970-17971 vs apps/rocm/src/uninstall.rs:44-50 — the plan warning promises a stop that will not happen.
build_uninstall_plan pushes "…have a running server; those servers will be stopped before removal" whenever any record is live, with no reference to what the plan actually removes. uninstall() prints the plan at line 25 and prompts at line 35, both before line 44 computes plan_removes_recovery_tooling and skips the stop pass entirely on a false. Verified unchanged since the held review. So rocm uninstall --keep-binaries --keep-data shows the operator a promise to stop their live GPU-holding server, they confirm, and nothing is stopped. Fix: gate the warning on plan_removes_recovery_tooling(&plan, &paths) (the predicate already exists at main.rs:17993) and word the ungated case as informational.
This gap is exactly what the tests miss: only_an_uninstall_that_removes_the_recovery_tooling_stops_servers (main.rs:30777) asserts the predicate in isolation, and nothing asserts the warning text agrees with it.
Non-blocking
- Revert check, per test: the four
uninstall.rstests inject the stop closure intostop_managed_services_then_remove, so removing the stop call is a compile error — genuinely tied. Themain.rsgate/report tests callstop_managed_services_before_uninstall/uninstall_removal_gate/plan_removes_recovery_toolingdirectly, so a revert deletes the symbols — tied. The e2e scenario plants a realsleep 600plus a service record and pollstry_wait()on its own child, so a revert leaves it running and the assertion fails — genuinely tied, not a pass-either-way test. apps/rocm/src/uninstall.rstesta_live_managed_server_is_stopped_before_the_planned_paths_are_removedhardcodes port 9, and the e2e step hardcodes 59999; anything listening there on the runner flips these from pass to fail (via the very second loop in blocking item 1). Prefer a bound-then-dropped ephemeral port.probe_host(main.rs:15607) maps only0.0.0.0,::,[::],""; other wildcard spellings a record could carry fall through and are probed literally.- The e2e step comment says liveness "falls through to the recorded process" because nothing listens on the chosen port — accurate today, but it depends on the same unguarded port probe that blocking item 1 asks you to change; revisit it with that fix.
…port Review follow-up on the stop-before-remove gate. The port-reachability check added in the previous commit iterated every loaded record, so a long-stopped service was judged by whoever holds its old port now. Stopped records keep their manifest and port forever (nothing prunes them, and there is no `services remove`), so any unrelated process that later bound one made uninstall abort deterministically, with no override and no recovery -- `rocm services stop` cannot help an already-stopped record. The probe now covers only the services this pass actually attempted to stop, which keeps the Windows grandchild case it was added for while making the failure self-clearing. The plan warning also promised a stop the run would not perform: it was built from record liveness alone, but the stop pass is skipped when the plan keeps the recovery tooling. It is now built after the actions are final and gated on the same predicate, so a cache-only run says the servers are left running. Also from the review: - Drive the whole command in a test (`uninstall_with_paths`), so the wiring between plan, gate and removal is covered rather than assumed. - Pin the warning text to the predicate; asserting the predicate alone could not catch the two drifting apart. - Make `ManagedServiceRecord::write` atomic (temp file + rename), so a concurrent reader cannot see a half-written record and mistake it for a corrupt one the gate must refuse. - Report a services-directory read failure in the plan instead of showing "no managed services" for a run that will abort on it. - Widen `probe_host` to the other wildcard spellings a record can carry. - Replace the hardcoded test ports with bound-then-dropped ephemeral ones. - Drop a `cfg(target_os = "linux")` from a test that touches no process state. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 451a8ee
Summary
Adds a stop-before-remove gate to rocm uninstall (stop every live managed service, abort with nothing removed if any stop cannot be confirmed), plus a port reality-check, plan-warning rework, an atomic service-record write, and unit + e2e coverage. Verdict: Needs work — the core gate is well-designed and genuinely tested, but the remediation commit introduces a Windows-risky third copy of an atomic-write helper, and two fail-direction defects remain. Verified: compiled a standalone probe confirming ("[::1]", port).to_socket_addrs() and whitespace-padded hosts fail resolution (so the port probe silently reads "nothing serving"); cargo test -p rocm and the feature-naming suite pass, and reverting the for record in attempted line makes a_stale_record_whose_old_port_was_reused_does_not_block_uninstall fail, so that test is real regression coverage; confirmed by reading source that stop_internal_managed_service does record.write()?, that apps/rocm/src/therock.rs:3042 and apps/rocmd/src/lib.rs:1139 already implement a hardened publish_temp_file, and that only *.json manifests live directly under services_dir. CI for this head: 18 success, 1 skipped, no failures. Blocking: 3 · Non-blocking: 4.
Prior review status (judged from the current code, not from the earlier report):
- Stale records judged by their recorded port → permanent, unrecoverable uninstall block: FIXED (probe restricted to services this pass attempted; pinned by a test verified to fail on revert).
- Plan warning promising a stop the run would not perform: FIXED (warning built after actions are final and gated on the same predicate, pinned by
the_plan_warning_only_promises_a_stop_the_uninstall_will_perform). - Wiring between plan / gate / removal untested: FIXED (
uninstall_with_pathsnow driven end to end). probe_hostnot covering all wildcard spellings: PARTIALLY FIXED — see Blocking 2; the non-wildcard branch still discards the normalization.- "Fail closed with no override and no recovery" as a class: STILL PRESENT for the unparseable-manifest path — see Blocking 3. The same reasoning that justified fixing the recycled-port case applies here and was not carried over.
🚫 Blocking (must fix before merge)
1. crates/rocm-core/src/lib.rs:7317-7357 — the new atomic write is a third, weaker copy of a helper this repo already hardened, and it now sits on the uninstall critical path.
ManagedServiceRecord::write was changed from fs::write to temp-file + bare fs::rename. This project already has that pattern twice — apps/rocm/src/therock.rs:2925-3080 and apps/rocmd/src/lib.rs:1053-1163 — and both wrap the publish step in publish_temp_file, which on Windows deliberately routes through ReplaceFileW (replace_file_windows) whenever the destination exists, precisely because a plain rename-over-existing is not reliable enough here. Both copies also carry a collision-avoidance loop and tests that a failed publish leaves no scratch file. The new implementation has none of that, and manifests are overwritten on every status transition. It matters more than usual because the failure propagates: apps/rocm/src/main.rs:15456 is record.write()?, so a publish failure inside stop_internal_managed_service becomes a failed entry in the stop report and hard-aborts the uninstall this PR is trying to make safe. I could not run Windows here, so I am stating the platform semantics from the repo's own prior art rather than from a live repro — but the prior art is unambiguous that this codebase does not consider bare rename sufficient. Fix: hoist write_file_atomically/stage_file_for_atomic_publish/publish_temp_file into rocm-core and call it from ManagedServiceRecord::write (which also removes the existing duplication between therock.rs and rocmd); note crates/rocm-core/Cargo.toml:31 currently lacks the Win32_Storage_FileSystem feature that apps/rocm/Cargo.toml:54 has.
2. apps/rocm/src/main.rs:15614-15624 — probe_host computes a normalized host and then throws it away, so the port reality-check fails open.
The function builds normalized (trimmed, bracket-stripped, lowercased) to classify the host, then the default arm returns host.to_owned() — the raw string. loopback_tcp_port_is_reachable (main.rs:13359) does (host, port).to_socket_addrs(), which I confirmed by compiling a standalone probe rejects both "[::1]" and " 127.0.0.1 ". A resolution failure returns false, and the caller at main.rs:15585 treats false as "nothing is serving" and continues. So for a record whose host is a bracketed IPv6 literal or carries stray whitespace, the only check that can catch a surviving engine grandchild — the documented reason this probe exists — silently disappears, and uninstall removes the tooling while the endpoint is live. This is representable: main.rs:10232 (loopback_host_key) already normalizes "[::1]", so the codebase itself expects bracketed spellings in records, and --host is a free-form string. Fix: _ => normalized in the default arm, and add a bracketed non-wildcard IPv6 case to every_wildcard_bind_spelling_is_probed_on_loopback — that test currently only exercises unbracketed literals, which is why this slipped through.
3. apps/rocm/src/main.rs:15674-15716 (with 15638-15667) — an unparseable manifest blocks uninstall permanently and the abort message prescribes a fix that cannot work.
unreadable_service_manifests pushes the filename as the service_id (main.rs:15660-15665), and the gate then tells the operator: "Stop them with rocm services stop <id> --yes, then re-run uninstall." For this failure mode that command cannot succeed — rocm services stop loads the same record and fails on the same JSON — so every retry aborts identically, with no flag to override and no mention anywhere (message, doc comments, docs) that the real remedy is to inspect or remove the file on disk. This is the same "no override, no recovery" property that was correctly judged unacceptable for the recycled-port case in this very commit; it should be judged the same way here. Minimum fix: include the full manifest path and state the actual remedy for the unparseable case (repair or delete the record), distinct from the still-serving case; better, add an explicit override for "I have verified nothing is serving".
Non-blocking
tests/e2e-cucumber/features/install_lifecycle.feature:129— the new scenario is@requires-os:linuxonly, so the gate has no Windows e2e coverage even though the Windows grandchild case is exactly why the port probe exists.crates/rocm-core/src/lib.rs:7344-7352— a crash between write and rename leaves a hidden.<name>.<pid>.<ms>.tmpfile that nothing ever sweeps; harmless to readers (all scanners filter on thejsonextension) but it accumulates.apps/rocm/src/uninstall.rs(a_live_managed_server_is_stopped_before_the_planned_paths_are_removed,the_uninstall_command_itself_stops_a_managed_server_before_removing_anything) andtests/e2e-cucumber/tests/e2e/lifecycle_steps.rs:485— the bind-then-drop ephemeral-port trick can race on a busy runner;uninstall_refuses_while_a_recorded_endpoint_still_accepts_connectionsshows the safer shape (hold the listener open).apps/rocm/src/uninstall.rstests — temp roots and spawned children are cleaned only on the success path, so a failing assertion leaks a directory; matches existing convention in this file, so only worth tightening opportunistically.
Superseded: this change request was left at an earlier head. The findings it raised are re-judged against 451a8ee in the current round — the stale-record port block, the plan-warning overclaim and the untested wiring are all fixed there. A fresh change request stands at the current head for the remaining items.
…escapable abort Review follow-ups on the EAI-8014 stop-before-remove gate. - ManagedServiceRecord::write no longer carries its own rename: the publish step moves to rocm-core, where the Windows ReplaceFileW path already lived in therock.rs and rocmd, and both now call it. A record is rewritten on every status transition, including inside the uninstall stop pass, where a failed publish is reported as a service that could not be stopped and aborts the whole uninstall. - probe_host returned the raw host after normalizing it, so a bracketed IPv6 literal or a padded host failed to resolve and read as "nothing is serving" - the fail-open direction for the only check that catches a surviving engine grandchild. - An unparseable service record told the operator to run 'rocm services stop', which loads the same file and fails the same way, so every retry aborted identically. Each failure class now carries the remedy that can clear it, and the unparseable one names the file to repair or delete. - Tests that need a dead endpoint use a port nothing can serve on instead of binding and dropping an ephemeral one. - Adds the Windows half of the uninstall-stops-the-server scenario, which is where the grandchild case the probe exists for actually happens. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…mon first Two holes the earlier fix left open, both in the direction that removes the tooling while a server keeps the GPU. - A stop persists status=stopped before the port probe runs, so the record that failed the gate reads as not-live on the next run. Probing only the services this pass stopped meant the retry the abort message asks for sailed through. Every record is probed again; what differs is the evidence: a service stopped by this pass fails on any listener, while one already recorded stopped has to identify itself as serving that record's own model, so an unrelated process on a recycled port still cannot block uninstall. An unidentifiable listener warns instead of aborting. - rocmd respawns a managed service whose endpoint stops answering, which is exactly the state the stop pass creates before it writes the record back. The background helper is now stopped first. Also: the port-probe failure carries its own remedy (the recorded processes are gone, so 'rocm services stop' cannot help - find what holds the port); the plan warning says 'recorded as running' rather than asserting liveness it did not check; and the atomic write syncs before publishing, so a crash cannot leave the zero-length manifest the gate would refuse to remove. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
… uses it Both users are Linux-only tests, so on Windows and macOS the constant was dead code, which -D warnings makes a build failure. The Linux container gate cannot see this class of break. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Withdrawing this change request as superseded: all three blocking points from 451a8ee are genuinely fixed at 4db3a7f — the atomic-write helper is single-sourced, the port probe keeps its normalization, and each abort class now names a remedy that can actually clear it. A fresh review at the current head is being filed separately, raising a new concern introduced by the latest commits; only that newer one is operative.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 4db3a7f
This automation posts comments only. It never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Adds a stop-before-remove gate to rocm uninstall (stop every live managed service, abort with nothing removed if any stop cannot be confirmed), and in the latest commits consolidates the atomic-write helper, makes the port probe resolvable, gives each abort class a remedy that actually works, re-probes every record so a retry stays honest, and stops the rocmd background helper first. Outcome: Needs work — all three of our prior blocking points are genuinely fixed, but the newest commit adds a destructive path we have not reviewed before and that this PR does not test. Verified: cargo test -p rocm-core --lib passes (333/333) on this checkout, and targeted cargo test -p rocm --bin rocm uninstall_removal_gate passes (3/3); confirmed by scratch-revert that the uninstall-ordering and uninstall_removal_gate tests fail when their production lines are reverted; confirmed by reading source that probe_host's default arm now returns normalized, that publish_temp_file/replace_file_windows moved into rocm-core byte-for-byte with Win32_Storage_FileSystem following them and no Windows FFI left in the app crates, that unreadable_service_manifests now pushes the full path with a RepairTheRecord remedy, that ProcessIdentity::new(pid, None) makes identity_state return Matches unconditionally, and that terminate_verified(..., Tree, .., force=true) escalates to SIGKILL across the whole tree. Blocking: 2 · Non-blocking: 5.
Prior objection status
- 1 — atomic write was a third, weaker copy of a hardened helper: RESOLVED.
publish_temp_fileandreplace_file_windows(bothcfgarms, thetry_exists→ReplaceFileW→ rename-race fallback, the SAFETY comment) are now single-sourced incrates/rocm-core/src/lib.rs:7634-7692;therock.rs:3038androcmd/src/lib.rs:1135delegate to it,ManagedServiceRecord::writegoes through the newwrite_file_atomically(collision loop,sync_all, cleanup-on-failure), and the windows crate feature moved tocrates/rocm-core/Cargo.toml:31with no direct FFI left behind in the app crates. - 2 —
probe_hostdiscarded its normalization and failed open: RESOLVED.apps/rocm/src/main.rs:15763is now_ => normalized, andevery_wildcard_bind_spelling_is_probed_on_loopbackgained[::1]," 127.0.0.1 "and[FE80::1]cases that assert both the string andto_socket_addrs().is_ok()— all three fail on revert, so it is real regression coverage. - 3 — unparseable manifest was a permanent block with impossible advice: RESOLVED.
main.rs:15733now records the full path, the remedy isRepairTheRecord, and the gate's text (main.rs:15877-15891) tells the operator to check/stop the server, then repair or delete the named file — with a test asserting the message names the file and does not sayrocm services stop. No override flag was added, and none is needed now that each failure class names a remedy that can clear it. - Prior non-blocking Windows e2e gap: also resolved —
lifecycle-24adds the@requires-os:windowssibling of the Linux scenario.
🚫 Blocking (must fix before merge)
1. apps/rocm/src/main.rs:15557-15591 — rocm uninstall can now SIGKILL an entire process tree at a PID it cannot prove is rocmd.
stop_background_helper_before_uninstall builds ProcessIdentity::new(state.daemon_pid, None) (line 15573) and passes it to terminate_verified(&identity, KillScope::Tree, MANAGED_STOP_GRACE, true). With start_ticks: None, identity_state (crates/rocm-core/src/proc_lifecycle.rs:87-104) takes the (None, _) => Matches arm — the "legacy state file, best-effort proceed" path — so Recycled and Indeterminate can never be returned for this PID. force = true then means SIGTERM followed by SIGKILL across process_tree_pids(pid). This is the only kill in the codebase with no recorded identity: the pre-existing managed-service call site (main.rs:15394) passes real ticks, and ProcessIdentity's own doc comment says it exists to be "robust to PID recycling". Nothing invalidates daemon_pid: crates/rocm-core/src/lib.rs:5878-5892 only reads/writes automations/runtime-state.json, never deletes it, and running = false is written only on a clean shutdown (apps/rocmd/src/lib.rs:3053) — after a crash, OOM-kill or reboot the file survives with a stale PID. The guard at main.rs:15564-15567 checks only pid != 0, pid != self, and process_is_running, and (unlike the repo's own background_helper_already_running, main.rs:5963-5966) does not consult state.running. So the reachable sequence is: rocmd dies uncleanly → machine reboots → the OS reissues that low PID to an unrelated process → the user runs rocm uninstall → that process and every descendant are killed. That is exactly the "uninstall destroys something the user did not install" case, and it is new in this PR. Fix: persist the daemon's start ticks alongside daemon_pid in AutomationRuntimeState (capture with ProcessIdentity::capture at spawn, as services already do) and pass them here, so Recycled/Indeterminate are detected; when identity cannot be verified, do not signal — record a failed entry with the existing StopTheDaemon remedy (which already tells the operator to kill the pid themselves) rather than killing on a guess.
2. apps/rocm/src/main.rs:15557 — the new daemon-stop behaviour has no test at all, in a PR where every other new behaviour is revert-pinned.
Grepping the unit tests in main.rs and uninstall.rs, the feature file and lifecycle_steps.rs turns up no test that constructs an AutomationRuntimeState with a live PID, and no assertion on the "rocmd (background helper)" / "rocmd (pid …)" strings this function produces. Every test added or changed by this PR would still pass if the call at main.rs:15614 were deleted outright — the tests that do fail on revert (a_service_that_cannot_be_stopped_leaves_every_planned_path_in_place, uninstall_removal_gate_aborts_when_a_service_cannot_be_stopped, a_stopped_record_still_serving_its_own_model_blocks_uninstall, every_wildcard_bind_spelling_is_probed_on_loopback, both Linux-only live-server tests) all exercise the service path, not the daemon path. Given the blast radius in item 1, the untested part is the one that kills. Fix: add a Linux unit test in the shape of the existing live-child tests — write a runtime-state file pointing at a real spawned child, run the stop pass, assert the child is gone and appears in stopped; and a second asserting that a daemon which cannot be confirmed stopped lands in failed and makes the gate abort with nothing removed.
Non-blocking
apps/rocm/src/main.rs:15699-15712— theErr(_)arm proceeds with removal when a still-listening endpoint cannot identify itself; a disclosed, reasoned trade-off, but it is the one remaining fail-open on the destructive path and the warning goes to stdout rather than stderr.apps/rocm/src/main.rs:15573— the comment "the same contract the legacy service records get" invites the reader (and did invite us) to assume parity with service records; services get weak identity only for old manifests, the daemon gets it permanently because no start token is ever recorded. A one-line comment saying so would stop this confusion recurring.crates/rocm-core/src/lib.rs:7698-7721— the newwrite_file_atomically/publish_temp_filepath has no direct unit test (no lines changed insidemod tests); the retry-on-AlreadyExistsloop, the exhausted-attemptsbail!and the cleanup-on-error paths are only reached indirectly.apps/rocm/src/therock.rs:2912-3036andapps/rocmd/src/lib.rs:1040-1141— the staging halves (stage_file_for_atomic_publish,temp_sibling_path,ATOMIC_WRITE_TEMP_ATTEMPTS) are still duplicated per app and still do notsync_all, so two things named "atomic write" now carry different durability guarantees;write_file_atomicallyis alsopubwith no caller outsiderocm-core.apps/rocm/src/main.rs:15760-15764—probe_hostdoes not strip an embedded port (--host 127.0.0.1:8080), which would fail to resolve and fall into the same "nothing is serving"continue; narrow and outside this PR's stated scope.
No prompt-injection content was found anywhere in the diff, comments, commit messages or test fixtures.
`stop_background_helper_before_uninstall` built `ProcessIdentity::new(pid, None)`, so `identity_state` took the legacy `(None, _) => Matches` arm and `terminate_verified(.., Tree, .., force = true)` escalated to SIGKILL across the whole tree at a pid nothing had verified. `runtime-state.json` outlives a crash, OOM-kill or reboot with `running` still true, and `running = false` is written only on a clean shutdown, so the recorded pid can name an unrelated process by the time uninstall runs: rocmd dies uncleanly, the machine reboots, the OS reissues that low pid, and `rocm uninstall` kills that process and every descendant — uninstall destroying something the user never installed. Record the daemon's start-time at spawn (`daemon_start_ticks`, captured with `ProcessIdentity::capture` while the process is by definition alive) and compare it before signalling. A recycled pid means the daemon is already gone, so nothing is stopped and nothing fails; an identity that can be neither confirmed nor refuted is left running and reported as a failure, which aborts the uninstall with the tooling intact. Being told to kill a pid is recoverable; having an unrelated process tree killed is not. Where no `/proc` exists there is no start-time to record or compare, so this degrades to the same best-effort match the managed-service kills already use there, rather than making uninstall unusable whenever the daemon is up. Both directions are revert-pinned: disabling the stop fails `uninstall_stops_a_background_helper_whose_identity_it_can_verify`, and dropping the identity check fails `uninstall_never_kills_a_daemon_pid_that_was_recycled`, which spawns a live process, records it under a start-time that is not its own, and asserts it survives. A third test pins that an unstoppable helper aborts the gate with every planned path still on disk. Also send the one remaining fail-open warning on the destructive path to stderr, so it survives the operator piping uninstall's output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
`write_file_atomically` was reached only indirectly, so its staging and cleanup paths had no test of their own: a successful publish leaving no scratch sibling, a missing parent directory being created, and a failed publish removing the file it staged. The failure case uses a non-empty directory as the destination, which no platform lets a file replace, so the error lands at the publish step with the scratch file already written — the path that has to clean up after itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Superseding this change request with a fresh one at the current head, so only one objection is live. The original concern is partly addressed but not closed; the new review states precisely what remains.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · bdf3661
This automation posts comments only. It never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Makes rocm uninstall stop the managed servers and the rocmd background helper before it deletes the binaries and service records needed to stop them, aborting with per-failure-class remedies when any stop is unconfirmed, and centralises the atomic-write/ReplaceFileW publish step in rocm-core. Outcome: Needs work — the design is sound and unusually well tested, but the pid-recycling objection is only partly closed and one new fail-open remains on the destructive path. Verified: ran cargo test -p rocm-core --lib write_file_atomically (3 passed); read the source to confirm identity_state's (None, _) => Matches fallback, that process_start_ticks returns Some only on Linux, that process_tree_pids collapses to the root pid off Linux, that AutomationRuntimeState::load returns Err (not Ok(None)) on a corrupt/unreadable state file while its write is still a non-atomic fs::write, that managed_service_endpoint_model_ready probes record.endpoint_url built from the un-normalised host, that OsStrExt is already in scope for the moved Windows FFI, and that both new e2e Given steps resolve to existing step definitions. Blocking: 2 · Non-blocking: 5.
Standing objection — partially resolved, not resolved. We were right, and the fix is real but incomplete. The new stop_background_helper_before_uninstall does gate the kill on ProcessIdentity, and on Linux with a state file written by this PR's rocmd a recycled pid is correctly detected and never signalled (uninstall_never_kills_a_daemon_pid_that_was_recycled genuinely pins that). But the gate is inert whenever daemon_start_ticks is None — see blocking #1. The PR's own field doc says the two causes of None "must not be conflated"; the only caller conflates them.
🚫 Blocking (must fix before merge)
1. apps/rocm/src/main.rs:15592-15612 — the daemon is still force-killed on a pid it cannot prove is rocmd, whenever no start-time was recorded.
identity_state (crates/rocm-core/src/proc_lifecycle.rs:91-104) maps (None, _) => IdentityState::Matches, not Indeterminate. So the match falls through to terminate_verified(&identity, KillScope::Tree, MANAGED_STOP_GRACE, /* force */ true) — SIGTERM then SIGKILL — for every runtime-state.json that carries no daemon_start_ticks. That is: (a) any Linux state file written before this PR, i.e. the ordinary upgrade path for existing users, where collect_process_tree is available and the kill is the whole tree of a possibly-unrelated process; and (b) every macOS and Windows install, permanently, since process_start_ticks is #[cfg(not(target_os = "linux"))] -> None (proc_lifecycle.rs:312-316) — blast radius there is the single root pid, since process_tree_pids returns vec![root] off /proc, but it is still an unrelated process killed by an uninstall. This PR introduces the kill site; before it, uninstall signalled nothing. The stale-file scenario the code comment itself describes ("survives a crash, OOM-kill or reboot with running still true") is exactly the case a pre-upgrade state file represents.
Also note the new code never checks state.running, while the pre-existing double-spawn guard at apps/rocm/src/main.rs:5965 does — so even a cleanly shut-down daemon's leftover pid is a kill candidate.
Fix: distinguish "no identity recorded" from "identity verified" at this call site rather than relying on the best-effort fallback. Concretely, before signalling: if rocm_core::process_start_ticks(state.daemon_pid).is_some() && state.daemon_start_ticks.is_none(), this platform can verify and the record simply predates the field — treat it as Indeterminate (push StopFailureRemedy::StopTheDaemon, abort, leave the tooling in place) instead of killing. That closes the Linux upgrade hole outright and leaves the documented macOS/Windows best-effort degradation as the only remaining gap, which should then be stated in the daemon_start_ticks doc at crates/rocm-core/src/lib.rs:5869-5880 rather than only in a call-site comment.
2. apps/rocm/src/main.rs:15574 — an unreadable or corrupt runtime-state.json silently skips the daemon stop entirely.
let Ok(Some(state)) = AutomationRuntimeState::load(paths) else { return; }; collapses three outcomes into one. load (crates/rocm-core/src/lib.rs:5890-5901) returns Ok(None) only when the file is absent; a permission error or a truncated/unparseable file returns Err, and that Err is discarded. Nothing is pushed to report.failed, nothing is printed, and uninstall proceeds to delete the binaries and every service record while a live rocmd — which respawns a managed service whose endpoint stops answering — may still be running. This is the precise defect the PR exists to close, and it is the opposite of how the PR treats the equivalent service-side case (unreadable_service_manifests, apps/rocm/src/main.rs:15772-15781, deliberately turns an unparseable manifest into a hard failure with the RepairTheRecord remedy). It is reachable in practice because AutomationRuntimeState::write (crates/rocm-core/src/lib.rs:5903-5912) is still a plain non-atomic fs::write executed on every daemon tick, so a crash mid-write leaves exactly this file.
Fix: match explicitly — Ok(None) => return, Err(error) => report.failed.push(FailedManagedServiceStop { service_id: "rocmd (runtime state unreadable)", reason: format!("{error:#}"), remedy: StopFailureRemedy::StopTheDaemon }), Ok(Some(state)) => ….
Non-blocking
apps/rocm/src/main.rs:15729— the model-identity probe is handed the rawrecord, whoseendpoint_urlis built from the un-normalised host (format_http_base_url,crates/rocm-core/src/lib.rs:124), so a0.0.0.0/::bind is connected to literally — the exact non-portabilityprobe_hostwas written three lines earlier to avoid; the fallout is a warned-but-fail-open pass on a wildcard-bound orphaned engine.crates/rocm-core/src/lib.rs:5903—AutomationRuntimeState::writewas left on non-atomicfs::writewhileManagedServiceRecord::writemoved towrite_file_atomically; that is the file blocking #2 depends on being intact.apps/rocm/src/therock.rs:2925andapps/rocmd/src/lib.rs:1053— both keep their own fullwrite_file_atomicallystaging pipelines and only call into the shared publish primitive, so neither picked up the newsync_all()-before-publish that the PR added specifically to prevent a zero-length file after a crash; three same-named functions now exist in the workspace.apps/rocm/src/main.rs:984-1043(a_stopped_record_still_serving_its_own_model_blocks_uninstall) —drop(serving)only detaches theJoinHandle; the thread stays blocked inaccept()holding an ephemeral port for the life of the test binary, and none of thesleep-child tests use a kill-on-drop guard, so a genuine regression panics before the reaping line and leaks a 60s child plus its temp root (the e2e suite already models the right pattern inLifecycleState::drop).crates/rocm-core/src/lib.rs:7711—write_file_atomicallycreates its temp file with default mode and replaces the target inode, so an existing file's permission bits do not survive (a change from thefs::writeit replaces, and a landmine for any future permission-sensitive caller); the doc's crash framing also overstates durability, since the parent directory is never fsynced.
Test audit (standing focus a)
All 24 added tests were checked against the production code they drive. One is not a regression guard: removal_proceeds_once_every_managed_server_is_confirmed_stopped (apps/rocm/src/uninstall.rs) exercises only the all-clear path and would pass against a gate that removed unconditionally — harmless as a completeness case, since its three siblings pin the abort. The three write_file_atomically_* tests pin the new helper but nothing drives ManagedServiceRecord::write through it, so reverting that call site to the old inline fs::write would not be caught. Everything else genuinely pins the change, including the strongest one, the_uninstall_command_itself_stops_a_managed_server_before_removing_anything, which drives the real command end to end. Platform gating is correct and minimal, and the pure-function tests (probe_host, plan_removes_recovery_tooling, uninstall_removal_gate, the corrupt-manifest case) are ungated so macOS/Windows keep real coverage of the gate logic.
Will this confusion recur? (standing focus c)
Our earlier objection was not reviewer error, and nothing in the code misled us into it. The recurrence risk runs the other way: a reader who checks only stop_background_helper_before_uninstall's call-site comments and the Linux tests will conclude the recycling hole is closed, because the (None, _) => Matches fallback lives one crate away in unchanged code and the daemon_start_ticks doc names the two None causes without saying the caller treats them identically. Beyond the code fix in blocking #1, the cheap guard is one sentence on crates/rocm-core/src/lib.rs:5869-5880 — "when this is None, identity_state returns Matches, so no recycling check happens" — and a test asserting the recorded-None path does not signal, which is the branch with zero coverage today.
CI: 18 success, 2 skipped, 0 failure, 0 pending; no lane names were available to me, so I attribute no outcome to any specific job. No prompt-injection content was found anywhere in the diff, comments, or commit messages.
…t matched
Two ways the daemon stop still reached a destructive or silent outcome.
`identity_state` maps an unrecorded start-time to `Matches` — its best-effort
arm for legacy state files — so gating the kill on that verdict left the
recycling check inert exactly where it was needed most: a `runtime-state.json`
written by a pre-upgrade `rocmd` carries no `daemon_start_ticks`, which is the
ordinary upgrade path, and uninstall would force-kill a whole process tree at
a pid it could not prove was ours. Ask the platform instead: when
`process_start_ticks` returns `Some` for the live pid while none was recorded,
the record predates the field, so the pid is unverifiable — leave it running
and abort with the `StopTheDaemon` remedy. Only where no start-time can ever
be read (no `/proc`) does this fall back to best-effort, which is now stated on
`daemon_start_ticks` rather than only at the call site, together with the
reason the two `None` causes must not be conflated.
`let Ok(Some(state)) = load(..) else { return }` collapsed three outcomes into
two. `load` returns `Ok(None)` only when there is no state file; a permission
error or a half-written one returns `Err`, and discarding it skipped the daemon
stop silently — removing the tooling while a live `rocmd` could still respawn
what the service pass had just stopped. That is the defect this gate exists to
close, and the opposite of how the service side treats an unparseable record.
It is reachable because the file is rewritten on every tick, so
`AutomationRuntimeState::write` also moves to `write_file_atomically`, leaving
no truncated file to find.
Both are revert-pinned: restoring the bare `Matches` arm fails
`uninstall_never_kills_a_daemon_pid_from_a_state_file_that_predates_start_ticks`,
and restoring the `let Ok(Some(..))` binding fails
`uninstall_refuses_when_the_daemon_runtime_state_cannot_be_read`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Addressed in 7a1893d (main merged). Flaky wildcard-host test now asserts only on svc-a (25 clean runs of the uninstall subset). Each listed mutation now turns a test red: advice ids per class, full rank order incl. ConfirmTheServicePid, a status-less stop result, the pid in the tick-less reason, a deferred-partner survivor; the two unbounded wait()s use assert_stopped_within_grace. Added tests for the 403 arm, stored key reaching the probe, non-.json filter, starts_with arm, warnings loop, recycled-daemon (!helper_stopped), advice follow-through. Plan wording now derives from the gate's decision and names a live rocmd with no records. README/--help/manual-testing/testing.md state the helper force-stop and the tick-less refusal; lifecycle 10/24/26/27 assert removal state. Comments fixed; shared recorded_pid_identities helper; localhost names both loopback families. Not done: write_file_atomically write/sync-failure and retry-exhaustion branches need a test seam that does not exist. The recovery-writer and engine_start_ticks gaps are covered by main's #499 tests. |
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
01ca985 to
4541c89
Compare
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 4541c89
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 note (full review of the whole change at 4541c899). This round replaces the change request on 3124f2ef. Every blocking item is marked new or standing.
- Your reply on the
df51a02ereport (issue comment 6167706877) was read and recorded with a reaction; it asked nothing, so this round is the answer. Your "no test seam exists" point on the atomic writer is weighed under that item below. - Dropped as resolved (each put to the reviewer after its report was frozen; anchored check, consistent with the blind pass, most confirmed by a mutation run):
- All five "further branches that stay green when broken" (remedy filter,
unwrap_or("unknown"), deferred partner probe,starts_witharm, stored keys): each mutation now turns a test red. uninstall_never_kills_a_daemon_pid_that_was_recycled: the fall-through mutation now fails it.- The background-helper stop is now in README,
--help,docs/manual-testing.mdanddocs/testing.md, and a liverocmdwith no records gets a plan line. - The plan no longer promises a stop for a live tick-less record; it predicts the refusal.
- The intermittent wildcard-host test: it now asserts only on
svc-a; about 23 full-suite runs and 10 subset runs passed. - From the standing test items: the
.jsonfilter and the 403 arm are now pinned; the rank check covers all eight classes; the tick-less test asserts the id; the unboundedwait()is nowassert_stopped_within_grace.
- All five "further branches that stay green when broken" (remedy filter,
- Would not gate on their own (marked below): the description finding (the squash body is built from commit messages), and the new untested-branch and missing-scenario items. Each is reported in full; the floor for a new test-strength gap is a test that claims coverage and cannot fail.
- Carried to the next round's scope as observations (not findings): whether
a_deferred_stopped_record_is_judged_once_its_live_partner_has_stoppedshares the ephemeral-port class that made the wildcard test flaky; whether anything proves the stop-pass warnings print before the abort line. - CI at
4541c899: every reported hosted check passes. No self-hosted GPU or Windows lane appears in the rollup; that is missing evidence, not a defect.
Review — needs work
Full review of the whole change.
Implements EAI-8014 (now ROCMAI-290): uninstall stops what it manages before removing the tooling, or aborts and keeps everything. The ticket also expects "verify their … process trees are gone", which Windows does not do (see Blocking). The description names #241 as an open, unmerged dependency, which is correct.
Blocking
-
The plan says "those servers will be stopped before removal" for a run the gate will abort (new; the tick-less case of the earlier plan objection is resolved, this is a different case) —
apps/rocm/src/main.rs:13748-13803,apps/rocm/src/uninstall_gate.rs:289-292,apps/rocm/src/uninstall_gate.rs:562-575build_uninstall_planreads records throughload_managed_services, which skips a manifest that does not parse without saying so (main.rs:12477). The plan never callsunreadable_service_manifests, whose only other caller is a test.- Run: a temporary Linux test with one live ticked record plus
services/bad.json={. The plan's only service line was "1 of 1 … those servers will be stopped before removal", and the helper note said "it will be stopped". The real stop pass then returnedfailed=[bad.json RepairTheRecord],stopped=[], and the server was still alive. - An already-stopped record whose port still answers as our model is also an abort the plan does not predict. The plan prints "none is recorded as running" for it (
main.rs:13770). - This is the last line read before confirming. AGENTS.md §3 forbids an outcome claim that is false.
- Confidence 100 · logic · Fix: one non-destructive pre-flight function (manifests, unprovable pids, already-stopped probes, helper identity). The plan renders its result, and the stop pass calls it again just before acting.
-
The gate picks what to stop with
managed_service_is_live, so a record readingfailedwhose recorded engine pid is still alive is never stopped (new) —apps/rocm/src/uninstall_gate.rs:581-583,:610,:637,:641,:654; plan atmain.rs:13748-13760refresh_from_engine_stateadoptsfailedfrom the engine's state file. vLLM writesfailedafter a best-effortlet _ = terminate_process_tree(…)(engines/vllm/src/process.rs:162-164). rocmd writesfailedwhen a wrapper exits non-zero (apps/rocmd/src/service.rs:822).refresh_managed_service_runtime_livenessreturns early for non-live statuses without looking at the pids (main.rs:12364).- Result: the record is only port-probed. An engine that is still loading, or that holds the GPU with no port, passes the gate and the tooling is removed. If it is listening, the advice says "Every process recorded … is gone" (
uninstall_gate.rs:92-98), which is false;rocm services stopwould still work. - The base already has the right predicate.
managed_service_record_is_in_use(main.rs:4147-4152) checks the recorded pids too, and its doc comment describes exactly this case.services removeuses it; the gate uses the weaker twin. - Confidence 85 · logic · Fix: select stop and pre-check candidates with
managed_service_record_is_in_use, and use it in the plan too.
-
A service counts as stopped on its
statusstring, not on the confirmed-stop boolean (standing, re-found blind) —apps/rocm/src/uninstall_gate.rs:662-676; doc claim:521-523stop_internal_managed_servicedrops theall_stoppedbool and re-readsstatusfrom disk. The doc ofstop_internal_managed_service_confirmed_with(main.rs:11070-11073) says a record that already readsstopped"keeps that status through a stop that confirmed nothing".- A supervisor's clean-exit write (
rocmd/src/service.rs:822) followed by aTimedOutgrandchild therefore lands inreport.stopped. Only a survivor that is listening is caught by the later probe. - The gate's doc ("only counted stopped when … confirms every recorded process is gone") states the opposite. The code governs, because it is what runs. The restart path already uses the bool (
main.rs:11296). - Confidence 85 · logic · Fix: call
stop_internal_managed_service_confirmed_withand gate on the bool.
-
"Whole process tree is force-stopped" is printed and documented, but is false on Windows (new) —
apps/rocm/src/uninstall_gate.rs:290-291(plan note),apps/rocm/src/main.rs:654(--help),README.md:952-953,docs/manual-testing.md:630- On Windows
process_tree_pidsreturnsvec, andsend_signalignores_tree(proc_lifecycle.rs:393-396). - The PR's own comments admit this (
uninstall_gate.rs:686-688,install_lifecycle.feature:292-294). - Confidence 85 · mechanical · Fix: word the claim per platform, or derive it from the platform, or implement descendant enumeration on Windows.
- On Windows
-
Five new branches are broken by a single mutation with every test still green (1237 tests ran per mutation) (the first is standing, re-found blind; the other four are new and would not gate on their own)
uninstall_gate.rs:200: dropping theprocess_is_runningfilter inunprovable_live_pidsmeans a dead tickless pid refuses the whole uninstall.main.rs:13776: the partial-refusal suffix "; the others will be stopped before removal" is never asserted.uninstall_gate.rs:255: the unreadableruntime-state.jsonplan warning can be removed (Err arm →None).uninstall_gate.rs:288:NothingOfOurscan be made to print "will be stopped".uninstall_gate.rs:271: the!state.runningguard can be dropped, so the plan promises to stop a helper the stop pass leaves alone.- Confidence 100 · logic · Fix: one test per branch that asserts the plan line together with the stop-pass outcome.
-
A test name claims the advice "works", but the test only pins strings (standing, re-found blind) —
main.rs:30347the_tickless_pid_advice_names_ids_and_a_manual_action_that_works- It asserts that the text contains
kill <pid>/Stop-Process -Id <pid>. No test follows theConfirmTheServicePid,ConfirmTheDaemonPid,RepairTheRecordorStopWhatHoldsThePortadvice and shows that a re-run then proceeds. - AGENTS.md §3: named remediation must actually clear the condition.
removing_the_stopped_record_as_advised_lets_the_retry_proceed(main.rs:29585) is the pattern to copy. - Confidence 85 · logic · Fix: rename the test to what it pins, and add follow-the-advice retry tests, at least for the tickless-pid and daemon-pid cases.
- It asserts that the text contains
-
No scenario covers the refusal of a live server or helper recorded without a start time (new; would not gate on its own) —
tests/e2e-cucumber/features/install_lifecycle.feature- README,
--helpanddocs/manual-testing.mddescribe this refusal: plan line, non-zero exit, nothing removed. A grep offeatures/finds no scenario for it, and none for an unreadableruntime-state.json. - AGENTS.md §3: output and exit-code behaviour needs a Gherkin scenario, and a unit test does not discharge that.
- Confidence 85 · logic · Fix: a lifecycle scenario that plants a tickless live record and asserts the plan line, the non-zero exit, the files kept and the server alive.
- README,
-
The description misstates the change (standing in part; would not gate on its own, see round note)
- It claims "
write_active_runtime_markernow publishes through the atomic helper instead of remove-then-rename". The diff does not touch it; that code is already on the base (apps/rocm/src/runtimes_cmd.rs:1780). - It says the
--keep-binaries --keep-dataexemption and the helper stop are "covered by unit tests only, not by a scenario". lifecycle-26 and lifecycle-27 cover both. - It does not mention the changed relaunch advice in
service_launch.rsandrocmd/src/common.rs, or the partial-removal report inuninstall.rs. - Confidence 85 · mechanical · Fix: correct the description. The code is fine on these points.
- It claims "
The items below are standing objections from the previous round that this round's blind review did not re-find. After its report was frozen, each was put to the reviewer separately, with your defence where you gave one. Those verdicts are anchored: the reviewer was told what to look for. Only the part that still holds is listed.
- The stop loop keeps stopping services after one fails, and the abort message calls those later stops "before the failure" (standing; anchored check: holds, by reading) —
apps/rocm/src/uninstall_gate.rs:653-684,:1095-1104,:545-552- The loop still continues after a failure, and
uninstall_removal_gatestill renders everyreport.stoppedentry as "stopped before the failure and stay stopped". The comment at:545-552("decided BEFORE anything is stopped", "pure cost") still contradicts the loop. - Fix:
breakon the first failed stop, or keep the loop and say "stopped during this run".
- The loop still continues after a failure, and
- The fail-open warnings tell the operator to act "first", but removal goes ahead straight after them (standing; anchored check: holds, by reading) —
apps/rocm/src/uninstall_gate.rs:764-771,:818-833,apps/rocm/src/uninstall.rs:99- "stop it before re-running uninstall" and "stop whatever holds that port first" reach stderr only after confirmation, removal then follows with no prompt, and a full uninstall leaves no
rocmto re-run. They never appear in the plan or a dry run. - Your defence (proceeding on an inconclusive probe is deliberate and stated in the warning text) covers the decision to proceed, which is listed as a tradeoff. It does not cover wording that names an action the operator has no window to take.
- Fix: reword as after-the-fact disclosures, or move these lines into the plan before confirmation.
- "stop it before re-running uninstall" and "stop whatever holds that port first" reach stderr only after confirmation, removal then follows with no prompt, and a full uninstall leaves no
- Nothing proves
uninstallprints the stop-pass warnings (standing in part; anchored check: holds for this part, confirmed by a run) —apps/rocm/src/uninstall.rs:99- Replacing
write_stop_warnings(&mut std::io::stderr(), &stop_report)with a no-op left every test green. The helper itself is tested; the call site is not.
- Replacing
- The "id-carrying remedies must name their ids" loop cannot fail (standing in part; anchored check: holds for this part, by reading) —
apps/rocm/src/main.rs:27749-27762inevery_failure_class_carries_its_own_recovery_advice- Every id it checks is a
service_idinfailed, and the gate's detail line prints every{service_id} ({reason})(uninstall_gate.rs:1084-1089), so the loop passes whatever the advice says. Per-class naming is pinned separately byeach_advice_names_only_the_ids_of_its_own_failure_class, so delete the loop or check the advice text alone.
- Every id it checks is a
write_file_atomically's retry and exhausted-attempts branches are untested (standing; anchored check: holds; your defence accepted for the sync-failure branch only) —crates/rocm-core/src/lib.rs- Nothing covers the write/sync-failure cleanup, the
AlreadyExistsretry, or the exhausted-attemptsbail!. - "No test seam exists" is fair for a
sync_allfailure. For the retry and exhaustion branches the workspace already has the seam: the twinwrite_file_atomically_withinapps/rocmd/src/sandbox.rstakessuffix_for_attempt. Splitting the temp name out the same way lets a test pre-create the names and drive both branches deterministically.
- Nothing covers the write/sync-failure cleanup, the
- Fail-closed branches no test exercises (standing in part; anchored check: holds in part)
- The production
KillScope::Treeargument is outside the injected seam (uninstall_gate.rs:398-405), and lifecycle-26's planted helper has no descendants, so Tree and Single are indistinguishable there. - Making
host_port_resolvesalways return false left the whole suite green (confirmed by a run). - "Publish order" from the earlier wording could not be checked; it is dropped rather than carried.
- The production
Non-blocking
-
The abort message under-reports what was already changed, and the gate's comments contradict its loop —
uninstall_gate.rs:545-552,:653-684,:853-855,:1095-1117- The comment says everything that can doom the run is "decided BEFORE anything is stopped" and that any earlier stop is "pure cost". But the helper and service stops, the post-stop probe and the partner probe all abort after stops have happened, and the loop keeps stopping after the first failure.
- A service dropped from
stoppedby the post-stop probe has already lost its key (main.rs:11131-11133), and the message does not say so. - On Linux the helper's tree kill takes down rocmd-supervised servers, but a
TimedOuthelper abort still says only "No files were removed." - Confidence 85 · logic.
-
A stranger serving our model on an old port gets advice that steers the operator to kill it —
uninstall_gate.rs:92-98,:853-880,:990-992BlockServingOurModelon an already-stopped record getsStopWhatHoldsThePort("outlived its supervisor").rocm services remove <id> --yeswould clear it, and the 401/403 arm already offers that (RemoveTheStoppedRecord).- Related: a stopped record on a specific host is not paired with a live
0.0.0.0partner byhosts_name_one_listener. - Confidence 85 · logic.
-
The gate's relaunch advice is a third copy and disagrees with the two twins —
uninstall_gate.rs:1095-1106vsservice_launch.rs:198-222androcmd/src/common.rs:335-355- It omits
--require-api-keyfor loopback key-protected services. - "(add
--api-key <key>)" reads as required; the flag is optional, and its help prefersROCM_SERVE_API_KEYto keep the key out of shell history. - The two twins agree with each other and match the
Serveclap definitions, so they should govern. - Confidence 85 · mechanical · Fix: one shared advice builder.
- It omits
-
The helper note gives the wrong reason in the
Indeterminatecase —uninstall_gate.rs:293-298- It says "no start time was recorded" when a start time was recorded but cannot be read now (e.g.
OpenProcessfails on Windows). - Confidence 85 · mechanical.
- It says "no start time was recorded" when a start time was recorded but cannot be read now (e.g.
-
The stop pass loads the service records twice —
uninstall_gate.rs:580,:602- The unprovable-pid pre-check runs on the first load and the stop acts on the second. A record that turns live in between reaches
terminate_verifiedwithticks=Noneand is force-killed on an unproven pid. - Each load also re-runs the readiness probes.
- Confidence 70 · logic · Fix: load once.
- The unprovable-pid pre-check runs on the first load and the stop acts on the second. A record that turns live in between reaches
-
Stale or inaccurate comments — in each case the code governs:
- Start-ticks still described as Linux-only after this PR added Windows support:
uninstall.rs:613,:690;proc_lifecycle.rs:50("clock ticks since boot");crates/rocm-core/src/lib.rs:1004-1005;uninstall_gate.rs:463; the expect message inapps/rocmd/src/service.rs:1026. uninstall.rs:104-105says "binaries before config, data and cache", but the actions are sorted by path (main.rs:13723).main.rs:27871-27872is garbled and says "there is noservices remove", which is false.docs/architecture.md:55-56still callsuninstall.rsa mechanical relocation, and callsuninstall_gate.rsa "full domain extraction" although its plan half lives inmain.rs.- Confidence 85 · mechanical.
- Start-ticks still described as Linux-only after this PR added Windows support:
-
"Cache-only" is not what
--keep-binaries --keep-datadoes, and the scenario runs different flags- Locations:
main.rs:13811-13812,README.md:957-958,docs/manual-testing.md:648. - Those two flags also remove config. lifecycle-27 adds
--keep-config(lifecycle_steps.rs:1083-1093), so the documented combination is never run. - Confidence 85 · mechanical.
- Locations:
-
The e2e success steps never check the exit code —
lifecycle_steps.rs:1353then_removal_complete(used by lifecycle-10, -24, -26, -27)last_rcis captured but read only by the refusal step.- In lifecycle-27,
contains("left running")(lifecycle_steps.rs:1474-1480) also matches the refusal text. - Confidence 70 · mechanical.
-
Liveness assertions that cannot fail —
main.rs:29051,:29054process_is_runningon an unreaped child iskill(pid, 0), which succeeds on a zombie. Only the report fields catch a wrong kill (mutations M05 and M11).- Confidence 85 · mechanical · Fix: use
try_wait()?.is_none(), as sibling tests do.
-
Five gate tests are Linux-only with no stated reason —
main.rs:28499,:28539,:28653,:28781,:28891- They use loopback HTTP and injected stops, like their ungated 401 sibling. As a result the 403 path, the stored-key path and the deferred-partner path run on no Windows lane.
- AGENTS.md §6 asks for a comment stating why a test is skipped.
- Confidence 70 · mechanical.
-
Plan claims pinned without the resulting state or a cross-reference —
main.rs:29634,:29483- The behaviour is proven elsewhere (e.g.
uninstall.rsa_cache_only_uninstall_leaves_a_live_server_running). AGENTS.md §3 asks the pin to name that test. - Confidence 70 · mechanical.
- The behaviour is proven elsewhere (e.g.
-
Three copies of stage-and-publish —
crates/rocm-core/src/lib.rs:2022-2186,apps/rocm/src/therock.rs:4708-4844,apps/rocmd/src/sandbox.rs:652-753- Only the publish step was single-sourced. The two app copies skip
sync_all, and their doc comments say so. - Confidence 85 · architectural.
- Only the publish step was single-sourced. The two app copies skip
Decisions for the author
- No
--forceon uninstall — tradeoff- For: deleting the record or state file is already a force, just with more friction and more room for mistakes.
- Against: a flag would bring back exactly the EAI-8014 defect.
- Fine either way, provided every refusal's advice names the file path or the
services removealternative.
- Keep stopping services after the first failure, or stop early — tradeoff
- Continuing gives a full report and takes down servers the operator wanted down anyway.
- Stopping early matches the "pure cost" rationale in the comment.
- Pick one and make the comment match.
- Windows descendant handling — non-blocking-improvement
- Enumerate descendants with Toolhelp32 (parent pid + creation time) in
process_tree_pids, or use a Job Object at spawn. - The post-stop port probe misses a launcher's child that is still loading or holds the GPU with no port.
- Enumerate descendants with Toolhelp32 (parent pid + creation time) in
- Test placement — non-blocking-improvement
- About 2,880 lines of tests for
uninstall_gate.rswere added tomain.rs's test module. - Child modules can see crate-root items, and test helpers can be exposed as
service_launch::tests::runtime_statealready is. The gate itself notes the cost atuninstall_gate.rs:808-809.
- About 2,880 lines of tests for
- Scope — non-blocking-improvement
- 66 commits and about 6.8k changed lines. The atomic-write single-sourcing and the serve relaunch-advice change could each have been reviewed on their own.
Positive signals
StopFailureRemedy::advice/advice_rankandStoppedRecordVerdict::blocksare exhaustive matches, so a new variant cannot silently default to proceeding or to no advice.stop_then_remove_withinjects both the stop and the removal. The ordering guarantee and the "removed / failed and possibly partial / not attempted" report are asserted against files on disk, not return values.assert_stopped_within_graceis a boundedtry_waitpoll, so a missing kill actually fails instead of waiting out the stand-in.identity_state_from_probestakes the probes as arguments, which pins the order "liveness before reading the start time".probe_hoststries both loopback families for wildcard binds, and the identity probe reuses whichever host answered.
Deployment notes
- After upgrading, a live server or
rocmdstarted by an older version has no start time recorded, androcm uninstallrefuses until the user stops it by hand. This is documented, but expect support questions. runtime-state.jsongains an optionaldaemon_start_ticks. On Windows, start ticks are FILETIME values (about 1.3e17, above 2^53), so a JSON consumer that parses numbers as doubles loses precision. No in-repo consumer is affected.ManagedServiceRecord::writeandAutomationRuntimeState::writenow replace the file instead of writing through it: a symlink at the path is replaced and the old file's mode is not kept.http_get_text_with_autherror text changed to "returned HTTP". No caller matches on the old text.
What this covered
- Reviewed:
prw-base 21be0034 (merge-base 666cb34a) … 4541c899.
- Read completely:
uninstall_gate.rs, uninstall.rs, every non-code and core hunk with its surrounding functions, and both e2e files' diffs.
main.rs: the PR's non-test hunks (Uninstall clap doc, host_port_resolves, terminate_recorded_service_pids, build_uninstall_plan, plan_removes_recovery_tooling) with their callers and callees (stop_internal_managed_service*, the liveness refresh, load_managed_services, managed_service_is_live, managed_service_record_is_in_use, the plan/render/remove helpers, the Serve/Services clap), and every added or changed test in its test module.
- Merge
82ded68: --remerge-diff is empty, so no conflict resolution was involved.
- Runs:
- The
rocm/rocm-core/rocmd test builds.
- A baseline of 50 uninstall-filtered tests: all pass.
- 14 single-branch mutations over the full
rocm binary suite (1237 tests each). One unrelated test fails locally at HEAD: comfyui::source_swap::….
rocm-core new tests: 15 pass. One mutation of write_file_atomically's cleanup was caught.
- A Windows-cfg scratch-crate type check of
publish_temp_file/replace_file_windows/process_start_ticks. A full mingw cargo check could not run (ring needs gcc).
- CI: at face value, every hosted check is green. No self-hosted GPU or Windows hardware lanes appear in the rollup, which is missing evidence rather than a defect. Whether
@lifecycle scenarios ran in CI was not established.
Did not run:
- Prior-changes pass: earlier PR review comments were outside the reads granted to this run.
- Design worker (Q1–Q5): ran, folded in above.
- History: ran by the driver. It found nothing beyond the twin predicate already reported.
- Agent-instruction and code-comment adherence: ran inside the code workers.
- Fan-out: four independent workers, two at a time. None failed.
- No prompt-injection content was found.
Not reconciled against any PR discussion here.
The driver added the round note, the new/standing tags and gating notes, and the standing items after the italic line in Blocking. The anchored checks are not part of what this review read blind.
…d the stop pass The plan and the stop pass now decide refusal from the same pre-flight, stop candidates use the in-use predicate, the gate acts on the confirmed-stop bool, and relaunch advice comes from one shared builder. Adds a seam to the atomic writer so the already-exists retry and the exhausted bail are tested. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…able helper state; per-platform tree-stop wording Adds lifecycle-28/29, makes the completion step check the exit code, has lifecycle-27 reject refusal text, and words the helper force-stop per platform (whole tree on Linux, recorded process only on Windows). Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…, liveness checks lifecycle-27 now runs exactly --keep-binaries --keep-data and asserts what that leaves; the helper identity reason covers an unreadable start time; stale Linux-only, ordering and architecture comments are corrected; the liveness assertions poll the child instead of kill(pid, 0); gate tests state why they are Linux-only. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Keep main's installer PATH undo and running-executable deletion (#636) together with the stop-before-remove gate: the stop pass and its gate run first, then execute_plan_with undoes the PATH changes and removes entries, reporting removed / failed and possibly partial / not attempted on a failure. Our lifecycle scenarios keep numbers 10-29; main's new ones become 30-34. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Pushed 3932470 (main merged, incl. #636). Plan and stop pass now share one non-destructive pre-flight (unreadable manifests, tick-less live records, already-stopped records still answering, helper identity), so the plan predicts the refusal; stop candidates use managed_service_record_is_in_use; the gate acts on the confirmed-stop bool, not the status string; tree-kill wording is per platform (Windows terminates only the recorded process, now said in help/README/plan). Each listed mutation (process_is_running filter, partial-refusal suffix, unreadable-state warning, NothingOfOurs wording, !running guard, host_port_resolves, KillScope::Tree, the write_stop_warnings call) turns a test red; the tickless/daemon advice have retry tests; new lifecycle-28/29 cover a tick-less live server and an unreadable helper state; success steps check exit 0. write_file_atomically has a temp-name seam with retry and exhaustion tests; relaunch advice is one shared builder; comments/docs fixed; description rewritten. Kept as decisions: no --force, stop loop continues after a failure (wording now says stopped during this run), no Windows descendant enumeration, tests stay in main.rs. |
…uild has no dead code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · aab76d1
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 note (full review of the whole change at aab76d10). This round replaces the change request on 4541c899. Every blocking item is marked new; none of the earlier ones still stands.
- Your reply (issue comment 6189391308) was read and recorded with a reaction. It reported fixes and asked nothing, so this round is the answer.
- Every standing blocker from
4541c899is dropped. For each one, the blind full review did not re-find it. After its report was frozen, the objection was put to the reviewer with your defence. These are anchored checks, and most were confirmed by mutation runs over the 1290-testrocmsuite:- The plan now predicts the refusal for an unparseable record and for a stopped record still serving.
- Candidate selection uses
record_needs_stopping, which includesmanaged_service_record_is_in_use. Reverting it fails a test. - The gate acts on the confirmed-stop bool. Ignoring it fails a test.
- The tree-kill wording is per platform in the plan,
--help, README and manual-testing. Only internal comments still say "whole process tree" unqualified. - Mutations (a)–(f) and the
host_port_resolvesandKillScopemutations now turn a test red. The(b)suffix is gone. - The ids loop is gone, and id naming is pinned elsewhere.
- The atomic writer has a temp-id seam with retry and exhaustion tests.
- The abort says "stopped during this run".
- The fail-open warnings now read as after-the-fact disclosures.
- The tickless and daemon advice have follow-the-advice retry tests.
- Residue of those objections, not gating, carried as observations:
- No follow-the-advice test exists for
RepairTheRecordorStopWhatHoldsThePort. - No test shows a helper descendant actually dying (also below as non-blocking).
- The fail-open warnings still reach stderr only after confirmation (also below).
- A record whose status is literally
stoppedwhile its pid is alive is only port-probed, and its "Every process recorded … is gone" advice was not re-verified. - The Unix test seam
stop_managed_services_withstill infers "confirmed" fromstatus == "stopped".
- No follow-the-advice test exists for
- What gates this round: four new items, marked below.
- The wildcard dual-family fail-open.
- The helper plan line that can promise a stop on an aborting run while the tests stay green.
- A test message claiming a regression it cannot catch.
- The shared-builder claim. Commit
14f17e6's message says "relaunch advice comes from one shared builder", and a squash carries that onto main.
- Would not gate on their own (each marked): the other new test-coverage items. For a new test-strength gap the floor is a test that claims coverage and cannot fail.
- CI at
aab76d10: every reported hosted check passes. Self-hosted GPU and Windows lanes are not in the rollup; that is missing evidence, not a defect. - Coverage line corrected at the reviewer's request: in What this covered, the reviewer asked for its garbled range to be corrected to
12342b3…aab76d10, and that is the only change made to its report text.
Review — needs work
Full review of the whole change.
Implements what EAI-8014 asks for (the tracker now files it as ROCMAI-290): stop managed services before removing the tooling, abort non-zero if they cannot be stopped, and keep the recovery state. One fail-open below breaks the ticket's "never report uninstall complete while a managed endpoint is still serving". The description overstates one shared-builder claim.
Blocking
-
A wildcard record whose own model still serves on
::1passes the gate silently when an unrelated listener holds127.0.0.1on the same port (new; gates) —apps/rocm/src/uninstall_gate.rs:847-850probe_records_for_survivorsusescandidates.iter().find(..)and identity-probes only the first loopback family that accepts a connection.- The
probe_hostsdoc says the caller blocks if either family answers, and the description says "A wildcard bind is probed on both loopback families". Neither holds for the identity probe. - A temporary test bound
::1:Pserving the record's model and127.0.0.1:Pserving another model, with a stopped record on host::. The report came back with nothing failed and no warnings, so the removal proceeds. - The same mechanism makes
an_ipv6_only_wildcard_engine_is_not_waved_through(main.rs ~28150) depend on no concurrent test binding the same v4 port. - Confidence 100 · logic · Fix: identity-probe every reachable candidate, block if any of them blocks, and add the dual-family test.
-
The plan's helper line can promise a stop the run will not perform, and three of its branches are untested (new; gates: the helper-refusal tests stay green while the plan says "will be stopped before removal" on a run that aborts) —
apps/rocm/src/uninstall_gate.rs:361-362,:374-377,apps/rocm/src/main.rs:13744- Each mutation below left the full
rocmbin suite green (1284 passed):- deleting the
(HelperDecision::Stop { .. }, true) if run_refusesarm; - dropping
|| services_read_error.is_some()fromrun_refuses; - making the
(UnreadableState | Unverifiable, true)arm return "will be stopped before removal".
- deleting the
- In each case the plan, which is the last thing read before confirming, says the helper "will be stopped before removal" on a run that aborts first.
- AGENTS.md §3 requires a printed claim about what the CLI will do to be asserted together with the behaviour.
- Confidence 100 · logic · Fix:
- Stage a verified live helper plus a refusing record (once with an unreadable services directory, once with a tickless pid).
- Assert that the plan says "left running" and never "will be stopped", and that the helper is still alive after the stop pass.
- In the two unverifiable/unreadable-helper tests, add
!plan.warnings.iter().any(|w| w.contains("will be stopped")).
- Each mutation below left the full
-
The test that claims a doomed run stops nothing cannot fail on the helper-failure branch (new; would not gate on its own) —
apps/rocm/src/uninstall_gate.rs~743- This is the early
returnafter a failedstop_background_helper_before_uninstall. Deleting it left every uninstall test green. a_doomed_run_stops_nothing_on_its_way_to_the_abortreaches the abort only through the pre-flight (unreadable state).- The helper termination cannot be injected through
stop_managed_services_confirmed_with, so no test can stage this path. - Confidence 100 · logic · Fix: make the helper termination injectable in the confirmed stop pass, return
TimedOut, and assert the per-service stop is never called.
- This is the early
-
The "Not attempted: …" part of the partial-removal error is never produced by any test (new; would not gate on its own) —
apps/rocm/src/uninstall.rs:230-238,:271-275- Forcing
untouched_partto always be empty left the suite green (1284 passed). - Every partial-removal test fails on the last or only step, so
untouchedis always empty and theself_delete"(the running program)" entry is never seen. a_directory_removal_that_deletes_some_children_then_fails_is_reported_as_partialasserts!contains("Still present"), a string no code produces (uninstall.rs:1690).- Confidence 100 · logic · Fix: a three-entry plan that fails in the middle; assert that the later entries are named under "Not attempted" and still exist on disk.
- Forcing
-
The claim that endpoint keys were dropped is pinned by wording only (new; would not gate on its own) —
apps/rocm/src/uninstall_gate.rs:961-983,:1213-1225- Forcing
key_droppedto false left the suite green. - The abort note ("Stopping them dropped their endpoint keys…") is asserted only against a hand-built report (
uninstall_abort_says_which_services_it_already_stopped). No test checks that the key is actually gone after a real stop followed by an abort. - This is the AGENTS.md §3 outcome-claim rule.
- Confidence 85 · logic · Fix: stop a keyed service for real, force the abort, and assert both the message and that
endpoint_keys::endpoint_api_keyreturnsNone.
- Forcing
-
The lifecycle scenarios that stop a server never assert the lines saying it will be or was stopped (new; would not gate on its own) —
tests/e2e-cucumber/features/install_lifecycle.feature:143-153,:309-319;tests/e2e-cucumber/tests/e2e/lifecycle_steps.rs:1509- lifecycle-10 and lifecycle-24 do not assert the plan's "those servers will be stopped before removal" (main.rs:13781) or the result "stopped 1 managed service(s) before removal" (uninstall_gate.rs:1295).
- lifecycle-26 does not assert "stopped the background helper (rocmd)".
- These lines are pinned only by unit tests on hand-built reports, and those tests carry no comment pointing at a behaviour test, which §3 requires when wording is pinned alone.
- Confidence 85 · mechanical · Fix: add Then steps asserting those lines next to the existing process-exit and file-state steps.
-
A test message claims a regression it cannot catch (new; gates) —
crates/rocm-core/src/proc_lifecycle.rs:110,:563-598a_dead_process_is_never_read_for_a_start_timedrives the privateidentity_state_from_probeswith stub closures. Its message says it fails if the read "is back in argument position" inidentity_state.- Rewriting
identity_stateto read the start time eagerly (let early = process_start_ticks(id.pid); move || early) leftcargo test -p rocm-core --lib -- proc_lifecyclegreen (18 passed). - A check claiming a branch it cannot fail on is worse than no check. The production code itself is correct.
- Confidence 100 · mechanical · Fix: reword the test and the doc comments to claim only the helper's internal ordering, or route the public
identity_statethrough a seam the test can drive.
-
The description and the protocol doc claim the uninstall gate uses the shared relaunch builder; it does not (new; gates: commit
14f17e6's message makes the same claim and lands on main) —apps/rocm/src/uninstall_gate.rs:1216-1223,crates/rocm-engine-protocol/src/lib.rs:55,apps/rocmd/src/common.rs:336-351- The description says the builder is "used by the gate,
rocm serveandrocmd".relaunch_command's doc says "One builder for the three places that print it, so they cannot drift". - In fact the gate hand-writes
rocm serve <model> --host <host> --port <port>…and shares onlyRELAUNCH_KEY_CHOICE. - There are four builder call sites (two in
service_launch.rs, two inrocmd/common.rs). The rocmd copy omitsRELAUNCH_KEY_CHOICE, so its message already differs from therocm serveone. - The rocmd public-host branch test asserts only "without authentication", so swapping that branch's
require_api_keyargument would stay green (traced, not run). - This governs both ways: the code is what ships. Either route the gate through the builder or correct the description and the doc comment.
- Confidence 85 · architectural · Fix: build one relaunch line per stopped service from its record with
relaunch_command(host, requires_key), and assert the public-branch command in rocmd.
- The description says the builder is "used by the gate,
Non-blocking
-
The plan discards the fail-open warnings the pre-flight computes.
apps/rocm/src/main.rs:13735-13736,apps/rocm/src/uninstall_gate.rs:669-670- The pre-flight's doc says the plan gets "the fail-open disclosures it would make".
build_uninstall_planuses onlypreflight_refusal_lineand dropsreport.warnings. - So warnings such as "does not resolve", "empty model list" and "unidentified listener" reach stderr only after the operator has confirmed, and they say "this uninstall does not wait for you to".
- Confidence 85 · logic · Fix: add
report.warningstoplan.warnings, or correct the doc.
- The pre-flight's doc says the plan gets "the fail-open disclosures it would make".
-
The plan omits an unverifiable or unreadable helper when an earlier pre-flight group refuses first.
apps/rocm/src/uninstall_gate.rs:361-362,:631-662- The comment says these cases are "Named by the refusal line", but
preflight_with_recordsreturns before reachinghelper_refusalwhen it finds a bad manifest, a tickless pid or a survivor. - The operator learns about the helper only on the retry.
- Confidence 80 · logic · Fix: collect the helper refusal whatever earlier group refused, or emit the note when the refusal line lacks it.
- The comment says these cases are "Named by the refusal line", but
-
Tests the comments call platform-neutral are gated to Unix, so the required Windows lane runs no unit test where the real stop kills a live server.
apps/rocm/src/uninstall_gate.rs:718, plus five tests carrying "nothing here is Linux-specific … but no Windows lane has run it yet"- Those five are
a_forbidden_answer_on_a_stopped_records_port_blocks_like_an_unauthorized_one,the_stored_endpoint_key_reaches_the_identity_probe,serving_the_same_model_again_after_a_stop_does_not_doom_the_uninstall,a_stopped_record_spelled_with_a_wildcard_host_still_waits_for_its_live_partneranda_failed_live_partner_is_not_named_twice_through_its_stopped_twin. stop_managed_services_withis#[cfg(all(test, unix))]with no stated reason.- lifecycle-24 is the only Windows exercise of the stop. No Windows twin exists for the helper scenario (26) or the tickless scenario (28).
- Confidence 80 · mechanical · Fix: ungate the HTTP-only tests and the seam, use a Windows stand-in process, and add a Windows twin of lifecycle-26.
- Those five are
-
The kill-breadth wording is not asserted.
apps/rocm/src/uninstall_gate.rs:250-257tree_scope_words()produces "its whole process tree" / "only the recorded process", the same claim--helpmakes. Swapping the Linux string for the Windows one left the suite green.the_helper_is_stopped_with_the_whole_tree_scopepins only theKillScopevalue, so no test shows a descendant dying.- Confidence 85 · mechanical · Fix: assert the words appear in the plan note, and add a Linux test with a grandchild (
sh -c 'sleep 60 & wait') that must die.
-
Remediation commands are pinned as text, not parsed.
- The
rocm serve … --allow-public-bind --require-api-keyadvice androcm services stop|remove <id> --yesexist (traced against clap), but no test parses them throughCli::try_parse_from. removing_the_stopped_record_as_advised_lets_the_retry_proceedcalls the function directly rather than going through the CLI.- Confidence 85 · mechanical.
- The
-
a_deferred_stopped_record_is_judged_once_its_live_partner_has_stoppedis timing-sensitive (main.rs ~28420).- It asserts
stopped == ["svc-b"], which depends on nothing binding::1:P. Its sibling avoids that assertion for exactly this reason. - Confidence 70 · mechanical.
- It asserts
-
The plan's pre-flight
Errarm is effectively unreachable and untested (main.rs:13734-13740).services_read_erroris already set by the same read failures, so the arm runs only if the two reads race. Neutering it left the suite green.- Confidence 70 · mechanical · Fix: drop the arm or fold it into the read-error path.
-
The atomic-write staging now exists three times and the copies differ.
crates/rocm-core/src/lib.rs:2023,:2144;apps/rocm/src/therock.rs:4708,:4760;apps/rocmd/src/sandbox.rs:652,:700- Only the publish step was single-sourced. The rocm-core copy calls
sync_alland retries 128 temp names; the other two do neither. The new doc comments state the difference. - Nothing reaches the
write_all/sync_allerror-cleanup arms (lib.rs:2174-2187). - Reverting vLLM
write_state(engines/vllm/src/state.rs:195) to plainfs::writeleftrocm-engine-vllmgreen (98 passed). - The
ReplaceFileWmove itself was checked to be identical to base logic. - Confidence 75 · architectural · Fix: give the rocm-core helper the
suffix_for_attempt/before_publishseams and delete the local copies.
- Only the publish step was single-sourced. The rocm-core copy calls
-
The expected-model selection is duplicated.
crates/rocm-core/src/net.rs:881-897vs:858-874- The fallback to
model_refand the empty-URLbailin the new copy are untested. The bail is unreachable from the only caller, which always rebuilds the URL. - Confidence 75 · mechanical.
- The fallback to
-
A Windows test leaves a
ping -n 30grandchild running afterchild.kill()killscmd(crates/rocm-core/src/proc_lifecycle.rs:541-556).- Confidence 70 · mechanical · Fix: spawn
pingdirectly.
- Confidence 70 · mechanical · Fix: spawn
-
Docs disagree with the code and with each other.
docs/manual-testing.md:660quotes "will be force-stopped", which the CLI never prints. The real line reads "will be stopped before removal, and the kill covers …".- README.md:973-985,
--help(main.rs:659-668) and manual-testing do not mention the refusal on an unreadable helper state. - The same surfaces say "nothing is removed" on refusal without saying that an abort after the stops leaves those services and the helper stopped with their keys dropped.
docs/architecture.md:55-56givesuninstall_gate.rsno extraction type, and saysstorage.rsreusesbuild_uninstall_plan, which it does not.- Confidence 85 · mechanical.
-
Smaller test-hygiene points in
lifecycle_steps.rs.:1679— the "left running" match also matches the helper notes; assert "so those servers are left running".:1651— cites a scenario by number in the same PR that renumbered them; cite the@idinstead.:618— the stand-ins inherit the harness's stdio; useStdio::null().:687,:1700— the start time is spliced intoe2e-mock.jsonafter it is written instead of being an option field.- Confidence 70 · mechanical.
-
The pull request does several separable things. Beyond the stop gate, it:
- single-sources the atomic-write publish and moves vLLM state writes onto it;
- changes how
http_get_with_authreports a response status; - adds Windows
GetProcessTimesstart times, which changes every Windowsrocm services stop; - changes the
rocm serve/rocmdrelaunch wording.
All of it is described, but at roughly 7,900 added lines the uninstall gate cannot be reviewed apart from them. Splitting out the supporting work would make each piece reviewable.
Decisions for the author
- A stop pass that aborts partway is not transactional — tradeoff
- Services stopped before the abort stay stopped, and their endpoint keys are gone.
- The other order would remove tooling while servers run, which is the bug. The abort message says so, and the pre-flight shrinks the window.
- Confirm that is the intended contract, and say it in README/
--help(see the docs item above).
- The survivor probe sends the stored endpoint key to whatever now listens on a stopped record's old port, including during
--dry-run— tradeoff- It makes an auth refusal provable rather than guessed. Its own comments say that port can belong to a stranger, and the key is the one a restart reuses.
- Probing without credentials first, and sending the key only on a 401, would narrow that.
- Records are loaded once, in the pre-flight — non-blocking-improvement
- A service started while the live helper is still up during the stop (up to its 10 s grace) is never stopped, and its record is then deleted with the data directory.
- Re-loading after the stops, and failing on any record that needs stopping but was not attempted, would close that.
Positive signals
- The plan and the stop pass share one pre-flight function, so a run that refuses has stopped nothing before it aborts.
StopFailureRemedy::advice/advice_rankandStoppedRecordVerdict::blocksare exhaustive matches, so a new variant cannot silently default to "proceed".assert_stopped_within_gracereplaces an unboundedwait()that passed whether or not anything killed the child.- No test signals a process it did not spawn.
- The lifecycle steps assert real state (live child via
try_wait, files present or absent), and aDropguard cleans up. - The advice-following tests carry out the printed remedy and re-run uninstall.
Deployment notes
- Live managed services recorded by an older version have no start time. On Windows that is every one, since start times were not read there before. Uninstall refuses them until they are stopped by hand:
rocm services stopstill works for them through the legacy best-effort path. - A
rocmdstarted by an older version has nodaemon_start_ticks, so uninstall refuses until that helper is confirmed and stopped. Old state files still deserialize (#[serde(default)]). - On Windows,
rocm services stopcan now returnRecycled/Indeterminatewhere it always matched before. A process whose creation time cannot be opened (for example an elevated one) is left alone rather than killed.
What this covered
Read:
- Every file in the change,
12342b3…aab76d10(the merge-base withprw-basefee201c, through head). uninstall_gate.rs,uninstall.rsand the main.rs diff completely.service_launch.rsandtherock.rsthrough their diffs and surrounding code. The rocm-core, rocmd, protocol and vLLM changes completely. The feature, steps and docs completely.- Fan-out was four independent workers in two waves of two.
Ran:
- The
rocmbin suite at head (1284 passed) and about 13 single-branch mutations acrossrocm,rocm-coreandrocm-engine-vllm. - The Linux lifecycle e2e (19/19 passed, plus a mutation run).
feature_naming, and a scratch-crate type and clippy check of thecfg(windows)start-time andReplaceFileWcode.- CI at head is green on every reported check, including
windows-build-and-testandE2E tests. The self-hosted GPU/Windows lanes are not in the rollup, which means missing evidence, not a defect.
Did not run, and why:
- The full Windows-target
cargo check/clippy:ringneedsx86_64-w64-mingw32-gcc, which is not installed. - The Windows-only unit tests and the Windows lifecycle scenarios (11-24, 31): Linux host.
- The design review (Step 7) was investigated by the driver from the workers' results, not by separate workers, because of the two-worker cap and the time budget.
- The history pass was light. The prior-changes pass covered only #636's review threads.
- Ticket EAI-8014 was read by its ID (now ROCMAI-290; title, body, status).
Not reconciled against this PR's review discussion.
The driver added the round note and the new/gating tags, and corrected the coverage range at the reviewer's request. The standing-objection verdicts in the round note are anchored checks, not part of what this review read blind.
Identity-probe every reachable loopback candidate for a survivor, collect the helper refusal in the shared pre-flight so the plan names it even when an earlier group refuses, make helper termination injectable and assert the plan line against the stop pass outcome, derive the abort's relaunch advice from the shared relaunch_command builder (rocmd copy carries the key choice too), reword the proc_lifecycle ordering test to what it proves, and update docs. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…lifecycle plan/result lines Three-entry partial removal asserts the Not attempted entries and that they still exist; a real keyed stop outlived by a survivor asserts the abort note and that the key is gone; lifecycle scenarios for the server and helper stop assert the plan and result lines; stand-ins no longer inherit stdio. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Pushed c6e3107 (main merged). Gating: (1) the survivor identity probe now probes every reachable loopback host and blocks if any does; dual-family test added (probing only the first host turns it red, as does the IPv6-only wildcard test, which no longer needs a free v4 port). (2) The plan's helper line says 'left running' on aborting runs and 'will be stopped' only on proceeding runs; the helper refusal is recorded even when an earlier group refuses; helper termination is injectable, with tests for the three branches (each mutation red) and a timed-out helper stop asserting no per-service stop runs. (3) proc_lifecycle test reworded to claim only the helper ordering. (4) The abort's relaunch line now goes through relaunch_command per stopped service, and rocmd's copy carries RELAUNCH_KEY_CHOICE with the public-host command asserted. Also: 'Not attempted' covered by a three-entry middle-failure test; a real stop plus abort test asserts the key is gone; lifecycle-10/24/26 assert the plan and result lines; plan carries pre-flight warnings and names the helper; docs updated. Decisions: the abort is not transactional by design (services stopped before it stay stopped with keys dropped; now stated); the stored-key probe and load-once points are noted for a follow-up; atomic-write staging dedup, expected-model dedup and PR splitting are deferred. |
Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Status of the automated Blocking
Non-blocking taken: stale Windows start-time comments ( Not taken, deliberately: splitting out the CI: the only red check is the Windows E2E |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · f88d72c
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 note (full review of the whole change at f88d72cb). This change request replaces the one on aab76d10. Every blocking item below is new; none of the earlier ones still stands.
- Your two replies (issue comments 6193133024 and 6229851028) were read and recorded with a reaction. Both reported fixes and asked nothing, so this round is the answer. The CI claim in 6229851028 was checked and holds (see CI below).
- Every standing blocker from
aab76d10is dropped. The blind full review did not re-find any of them. After its report was frozen, each objection was put to the reviewer together with your defence. These are anchored checks, settled by mutation runs:- Dual-family probe.
probe_records_for_survivorsnow identity-probes every reachable loopback address. Probing only the first one turnsa_stranger_on_one_loopback_family_does_not_hide_the_record_serving_on_the_otherandan_ipv6_only_wildcard_engine_is_not_waved_throughred. - Helper plan line. Each of the three named mutations now fails at least one test.
proc_lifecycletest message. It now claims only the helper's ordering, and no test claims the eager-read regression.- Shared relaunch builder. The gate,
rocm serveand rocmd all build throughrelaunch_command, so the claim in14f17e6and9c40b3bis true of what lands. Swapping rocmd's public-host argument turns a test red.
- Dual-family probe.
- Residue, carried as observations, not gating:
- Both dual-family tests skip and pass on a host without IPv6.
- The helper-refusal append at
uninstall_gate.rs:637-640is fixed in code but untested. It is also listed below. every_wildcard_bind_spelling_is_probed_on_both_loopback_familiespins onlyprobe_hosts.
- What gates this round: two new items, marked below.
- Three em dashes in new user-facing literals. Main's ASCII-output contract rejects them.
- The abort message says each relaunch command has "its own model and port filled in", but it prints
<model>/<port>placeholders. The only test reads back the list it built itself.
- Would not gate on their own (each marked):
- The merge conflict. GitHub already refuses to merge a dirty branch, but a rebase and a fresh CI run are needed.
- The untested branches. No test claims to cover them. For a new test-strength gap, the floor for gating is a test that claims coverage and cannot fail.
- The stale rocmd Windows comment.
- The
Co-Authored-Bytrailers. These are an attribution footer rather than a false claim, and whoever merges can drop them from the squash body.
- CI at
f88d72cb:- The only red check is
E2E tests (Strix Halo, Windows), with one unexpected failure,diagnose-38. Three more failures are expected xfails. - Main failed the same scenario the same way (run 39220155843, 2026-10-19) until main fixed it. This branch predates that fix, so it does not gate.
Regenerate manifestsandSkill checks (skillscope)were skipped.- Every check ran against the old base. A rebase will re-run them.
- The only red check is
Review — needs work
Full review of the whole change.
EAI-8014 now resolves to ROCMAI-290 ("rocm uninstall reports success while leaving a publicly-bound GPU-holding server running…"), status In Progress. This change implements that ticket's three expectations, with fail-open exceptions for records that were already stopped. The PR does not claim to close the ticket.
Blocking
-
The branch does not merge with current main (new; would not gate on its own: GitHub already blocks merging a dirty branch) —
apps/rocm/src/main.rs,crates/rocm-core/src/lib.rs- GitHub reports
mergeable_state: dirty. git merge-tree prw-base HEADshows content conflicts in both files.- On main, fcc646e (#666) moved
ManagedServiceRecordandAutomationRuntimeStateout oflib.rs. The newdaemon_start_ticksfield and the atomicwrite()bodies must be re-applied in the new modules. A resolution that keeps main's side silently loses the atomic writes. - Every green check is against the old base 03cdbcc, 23 commits behind main.
- Confidence 100 · architectural · Fix: rebase onto main. Then grep that the moved
write()methods still callrocm_core::write_file_atomically, and re-run CI.
- GitHub reports
-
Three new user-facing messages contain em dashes, which main's ASCII-output contract rejects (new; gates) —
apps/rocm/src/uninstall_gate.rs:95,:983,:994- Main added
xtask/src/ascii_output_contract.rsin f0d6ce3 (#683). - I ran that test against this tree. It names exactly these three literals: the
StopWhatHoldsThePortadvice and the two "already recorded stopped — proceeding" warnings. - Everything else it flagged comes from main-side conversions this branch predates. These three are new in this change, so they remain after a rebase and will turn the check red.
- Confidence 100 · mechanical · Fix: replace
—with ASCII (-or;) in all three.
- Main added
-
The abort message says each relaunch command has "its own model and port filled in", but it prints placeholders (new; gates: a printed remediation claim that is false, pinned only by a test that reads back its own input) —
apps/rocm/src/uninstall_gate.rs:1284-1291,:796-800,crates/rocm-engine-protocol/src/lib.rs:61-73relaunch_command(host, require_api_key)returnsrocm serve <model> --host {host} --port <port>…. The record's model and port are never passed in.- The PR description repeats the claim: "now names a runnable command with the model and port".
- Nothing tests this per-record construction. When I mutated
:799to ignore the record's host and key, the fullrocmbinary stayed green. the_stopped_service_advice_names_a_serve_command_that_restores_a_public_bind(main.rs:28722) builds therelaunchlist itself and reads its own input back, so its name claims more than it checks.- This is AGENTS.md §3: remediation advice pinned as a string, never checked against behaviour.
- Confidence 85 · mechanical · Fix: either pass
record.model_refandrecord.port(and--enginewhere needed) into the builder, or drop "filled in" from the message and the description. Then drive a real stop with a public, keyed record and assert the abort carries--allow-public-bind --require-api-key.
-
Five branches the change introduces have no test (new; would not gate on its own: no test claims to cover them). Mutating each one alone left the full
-p rocm --bin rocmbinary green (1327 tests), and the e2e lifecycle steps would not catch them either.uninstall_gate.rs:637-640: the pre-flight adds the helper refusal. Dropping it removesrocmdfrom the plan whenever a run already refuses for a service reason.uninstall_gate.rs:183-186:recorded_pid_identitiesprefers a known start time when the supervisor and engine share a pid. Without it, both the gate andterminate_recorded_service_pids(main.rs:10572) refuse that service.main.rs:13358: theself_deletearm ofplan_removes_recovery_tooling(Windows).uninstall.rs:234: the running executable in the "Not attempted" list (Windows).main.rs:13276: the plan line for a pre-flight error.- More broadly, 52 of the 97 new uninstall tests are
cfg(unix)or Linux-only. Lifecycle-25 to 29 are@requires-os:linux. On Windows the gate is covered only by lifecycle-24, the happy path, which uses a PowerShell sleep as a stand-in server. - These Windows-reachable behaviours are untested on Windows: the tickless refusal (now live there through
GetProcessTimes), the root-only helper-stop wording (tree_scope_words), and the survivor-after-stop arm, whose own comment says it exists because of Windows. - Confidence 100 · logic · Fix: a unit test for each arm above. Put the
self_deleteand running-exe cases in platform-neutral tests, which means un-gating thestop_gate_testsrunhelper.
-
A comment this change made false: rocmd's Windows stop says start times are unavailable there (new; would not gate on its own) —
apps/rocmd/src/service.rs:309-317- It says
process_start_ticks"has no/procto read there and returnsNone… Closing it needsrocm_coreto read a Windows process creation time". - This change adds exactly that (
GetProcessTimes), so theidentity_stategate below the comment can now refuse a recycled PID on Windows. - The same stale claim is in the test docs at
service.rs:1611and aroundcrates/rocm-core/src/lib.rs:2808("Noneelsewhere"). - AGENTS.md §5 requires that behaviour claims stay in sync with the code.
- Confidence 85 · mechanical · Fix: update all three. Optionally widen the refuted-PID rocmd tests to Windows, which can now set up a refuted identity.
- It says
-
41 of the 75 commits carry
Co-Authored-By: Claude <noreply@anthropic.com>(new; would not gate on its own: an attribution footer, not a false claim, and the merger edits the squash body)- The repository builds the squash body from the commit messages, so these trailers ship to main.
- AGENTS.md §11: "avoid AI-generated boilerplate footers".
- Confidence 85 · mechanical · Fix: strip the trailers when rewriting the history or the squash body. Consider writing a clean squash body anyway, since the 75-commit trail includes "address review of aab76d1".
Non-blocking
-
The help text and README promise a command that brings the helper back, but none is printed —
main.rs:666-668, README hunk,uninstall_gate.rs:1293-1295- "those stay stopped… the error lists the commands that bring them back" covers "services (or the helper)".
helper_noteonly says the helper stays stopped "until it is started again".- Confidence 70 · Fix: name the command, or narrow the wording.
-
partial_removal_noteis fixed text —uninstall.rs:265-283- A failure on config or cache, which run before the binaries, still says the binaries and service records may be gone and that
rocm services stopmay no longer work. - Confidence 70 · Fix: derive that clause from
removed.
- A failure on config or cache, which run before the binaries, still says the binaries and service records may be gone and that
-
The PATH-undo failure still says "Nothing else was removed…" —
uninstall.rs:200-204- It now fires after services and the helper were stopped and their endpoint keys dropped, and it prints no relaunch commands.
- The PATH edit could be checked in the pre-flight, before anything is stopped.
- Confidence 70.
-
False refusal when a stopped record and a live wildcard-bound record share a port under different host spellings —
uninstall_gate.rs:585-597,:1071-1074- Example: a stopped record on a LAN IP and a live record on
0.0.0.0. - The wildcard expands only to loopback addresses, so the two records are not paired. The stopped record is probed in the pre-flight and finds the live server answering with the same model.
- The run refuses with
StopWhatHoldsThePortinstead of stopping the live server itself. - Confidence 70.
- Example: a stopped record on a LAN IP and a live record on
-
Two refusals give advice that does not clear the condition
RemoveTheStoppedRecord(uninstall_gate.rs:115-121) says "if it is [yours], stop it first", but names no way to do that.rocm services stophas nothing left to kill there.- When
services_dircannot be enumerated (uninstall.rs:153), the run exits with a bare anyhow error, without the "uninstall aborted… nothing removed" context the help text promises. - Confidence 70.
-
The gate reads the start time before checking the pid is alive —
uninstall_gate.rs:312-316- It calls
process_start_ticksand thenidentity_state_with_observed, the exact order that function's new doc warns can yieldMatchesafter the pid exits and is reused. - It cannot cause a wrong kill, because
terminate_verifiedre-checks. It does mislabel the outcome, and it is the only caller of a new public API that invites misuse. - Confidence 85 · Fix: use
identity_stateand makeidentity_state_with_observedprivate.
- It calls
-
On Windows, a failed
ReplaceFileWcan delete both the record and its replacement —crates/rocm-core/src/lib.rs:2074,:2191- On
ERROR_UNABLE_TO_MOVE_REPLACEMENTthe target is already gone, and the cleanup then deletes the temp file holding the new bytes. - Service and runtime-state records now go through this path, and a lost record leaves uninstall unaware of a live server.
- Confidence 70 (from the Win32 contract; not run).
- On
-
On Windows the uninstall stop kills only the recorded root process —
main.rs:10570-10603,crates/rocm-core/src/process.rs:290- The rocmd twin of this stop uses
taskkill /T /F, because "the engine runs one level below the recorded PID" (apps/rocmd/src/service.rs:303-308). - If a child survives and has already bound its port, the run aborts after the keys are dropped. If it has not bound yet, removal proceeds under it.
- Confidence 70 (argued from the twin; not run on Windows).
- The rocmd twin of this stop uses
-
The vLLM state comment overclaims —
engines/vllm/src/state.rs:196- It says readers "never see a torn document". The same files are still rewritten in place by
rocm_core::merge_json_state_file(net.rs:~666), and lemonade'swrite_state(engines/lemonade/src/state.rs:205) also writes in place. - Confidence 70.
- It says readers "never see a torn document". The same files are still rewritten in place by
-
Gate code and tests live in
main.rs—main.rs:8501-8512,main.rstest hunk at 26874host_port_resolvesis used only byuninstall_gate.rsbut was added tomain.rs.- About 3,660 lines of gate tests went into
main.rs'smod tests; the gate file itself notes they are "far from here" (uninstall_gate.rs:959-962). - AGENTS.md §6 asks for new subsystems in their own file rather than growth in
main.rs. - Confidence 70.
-
Test quality
- Several message-only pins lack the §3 comment naming the test that proves the behaviour:
the_tickless_pid_advice_text…,a_helper_only_stop_prints_a_success_line,a_stopped_helper_is_reported_apart…. The proving tests do exist. a_directory_removal_that_deletes_some_children_then_fails_is_reported_as_partial(uninstall.rs:1715) asserts on what its own stub deleted.- Confidence 70.
- Several message-only pins lack the §3 comment naming the test that proves the behaviour:
Decisions for the author
-
Auto-stop plus a heuristic gate, versus refuse-and-advise — tradeoff
- For auto-stop: it gives the one-command uninstall the ticket asks for, and it reuses the
rocm services stopprimitive. - Against: roughly half the 1,376-line gate exists only because it kills: helper identity, the partial-stop/relaunch reporting, deferred partners, and the plan's refusal matrix.
- Against: the paths that proceed without proving the port is free can fail closed cheaply only before anything is stopped. These are an
ListsNoModelslistener (a loading engine),ProceedUnidentified, an unresolvable host, and a silently refused port. Under refuse-and-advise they could all fail closed. With auto-stop they fire after keys are dropped, which pushes the design toward failing open. - Confirm the choice was deliberate.
- For auto-stop: it gives the one-command uninstall the ticket asks for, and it reuses the
-
Splitting the change — non-blocking-improvement
- About 8,500 lines bundle several independent changes:
- the atomic-write consolidation and the
windows-sysremoval - vLLM atomic state writes
- Windows
GetProcessTimes, which changesidentity_statefor every Windows caller - the
rocm serveand rocmd refusal-text fixes http_get_with_authstatus parsing- partial-removal reporting
- renumbering about 20 existing lifecycle scenarios instead of appending the new ones
- the atomic-write consolidation and the
- AGENTS.md §11 asks for one logical change per PR, and several of these could land first on their own.
- About 8,500 lines bundle several independent changes:
-
The deferred-partner probe (
uninstall_gate.rs:865) — non-blocking-improvement- It mostly repeats the partner's own survivor probe, and only adds the other loopback family.
- Narrowing it to the addresses the partner's probe does not cover would remove a post-stop abort path.
Positive signals
- The plan and the stop pass share one non-destructive pre-flight (
preflight_with_records), so the plan predicts a refusal instead of promising a stop the run then refuses. - The stop pass is injected into
stop_then_remove_with, so the ordering can be tested: reversing stop and remove turns 7 tests red. daemon_identity_outcometreats a missing start time as unverifiable rather than a match, so the gate never force-kills a process tree on an unproven pid.- The
StopFailureRemedy::adviceandStoppedRecordVerdict::blocksmatches are exhaustive, so a new variant cannot default to "proceed" or go unprinted. - The new record fields use
#[serde(default, skip_serializing_if)], so old and new binaries read each other's files.
Deployment notes
- Older records refuse. A live server or helper started by a version that predates
supervisor_start_ticks/daemon_start_ticksmakes uninstall refuse rather than stop it. Users upgrading with servers running must stop them by hand before uninstalling. - Windows stop behaviour changes. Managed-service stops on Windows (in
rocm services stop, rocmd and lemonade) now verify the process start time, and can returnRecycledorIndeterminatewhere they always matched before.
What this covered
Every file in the change was read completely, split across four independent workers. Their scopes were:
- uninstall production code
- core, rocmd and engines
- the tests, with mutation checks
- design questions
I also read the callees of the changed symbols and the rocmd twin of the stop path. The diff read was 03cdbcc…f88d72cb; the merge preview and the ASCII check were against main at c7a0d65.
What ran:
- Unit tests: the full
rocmbinary passed at HEAD (1327 passed, 7 ignored), as didrocm-core(735),rocmd(154),rocm-engine-vllm(98) and the protocol crate (23). - Mutations: 16 single-branch mutations, plus several more, against the full
rocmbinary. - Main's ASCII contract: run against this tree.
CI: the only failing check, "E2E tests (Strix Halo, Windows)", fails on diagnose-fix-path-prepends-with-apostrophe-intact. Main failed the same way (run 39220155843, 2026-10-19) until #710, which this branch predates, so this change did not cause it. The lifecycle-10 and 24–29 scenarios ran and passed in build-and-test (19/19) and windows-build-and-test (15/15).
Did not run:
- The e2e lifecycle scenarios locally; they were read only.
- Any Windows-gated test, and a full Windows cross-check:
ringneeds mingw gcc, which is not installed. The two Windows functions were clippy-checked in a scratch crate instead. - Mutations of the probe and verdict logic (
stopped_record_verdict,probe_records_for_survivors). - The prior-changes pass was light: only #636's review comments were consulted.
Not reconciled against any discussion on this PR here.
The driver added the round note and the new/gating tags. The review paused overnight with the session and then resumed. The standing-objection verdicts in the round note are anchored checks, not part of what this review read blind.
juhovainio
left a comment
There was a problem hiding this comment.
This closes the primary failure mode in the linked ticket: uninstall_gate.rs stops rocmd and every managed service first, independently re-probes all managed records for survivors (TCP reachability + HTTP identity check, with explicit fail-open disclosure for unresolvable hosts and wildcard binds), and aborts via bail! before any deletion runs if a stop can't be confirmed. That ordering and the "abort leaves everything in place" guarantee are backed by dedicated tests (e.g. a_service_that_cannot_be_stopped_leaves_every_planned_path_in_place), and the coverage is convincing.
The directory-recreation race from the follow-up nightly report ("Directory not empty" after the live service kept rewriting files mid-delete) looks substantially addressed by that same ordering change: deletion no longer starts until liveness is independently confirmed gone, so the window that caused the race should mostly be closed. partial_removal_note reads as a diagnostic fallback for a partial removal that still happens for an unrelated reason, not the mechanism that itself prevents the race — worth confirming that's the intended split.
Requesting changes mainly because:
- The PR currently shows as
CONFLICTING/DIRTYagainstmain— needs a rebase before this can land.
Non-blocking, worth a second look before merging:
- Process-identity verification (
daemon_start_ticks/IdentityState) fails closed onIndeterminate/tickless pids, which is the right default, but I didn't trace the exact window between reading start-ticks and acting on them end-to-end — worth a quick confirm there's no realistic staleness window there. - CI: one failing check, Strix Halo Windows E2E (
diagnose-fix-path-prepends-with-apostrophe-intact) — this is the known PATH-apostrophe flake that fails identically on main, not a regression from this diff. Everything else, including clippy with--workspace --all-targets -D warnings, is clean.
Nice test coverage on the gate logic overall — the fail-open disclosures in probe_records_for_survivors show real thought about the actual failure modes (wildcard binds, unresolvable hostnames, stale endpoint keys, already-dropped endpoint keys).
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
fredespi
left a comment
There was a problem hiding this comment.
Review - needs work
The stop-before-remove gate addresses the original uninstall failure: it stops the helper before managed services and refuses removal when a stop cannot be confirmed. One remaining issue needs fixing: the error promises recovery commands with the model and port filled in, but prints placeholders. See the inline comment.
Delivered as a COMMENT because the available posting identity is the PR author and GitHub prohibits requesting changes on your own PR; this does not make the issue advisory. Recommendation: fix the recovery advice before merging.
Reviewed f53815d...4dbdb2f and surrounding stop, process-identity, atomic-publication, and lifecycle-test code. This was a code-only review; Linux/Windows tests and runtime behavior were not executed.
| " These services were stopped during this run and stay stopped: {}. Stopping \ | ||
| them dropped their endpoint keys, so a service that was publicly bound, or \ | ||
| launched with `--require-api-key`, has to be served again; the command for \ | ||
| each, with its own model and port filled in: {} {}.", |
There was a problem hiding this comment.
The abort promises a command for each stopped service with its own model and port filled in, but the production construction at lines 796-800 calls relaunch_command(&record.host, record.requires_api_key). That builder always returns rocm serve <model> --host ... --port <port>...: neither the model nor the port is passed. After a partial stop the operator is given placeholders, not the runnable recovery command this message and the PR description promise. The test the_stopped_service_advice_names_a_serve_command_that_restores_a_public_bind constructs report.relaunch itself, so it cannot catch this production defect. Populate the command from the stopped record (including any required engine selection), or explicitly describe it as a template instead of claiming the values are filled in. Test the production stop-report construction with a specific model and non-default port and assert the resulting abort advice.
There was a problem hiding this comment.
Fixed in 01f6510 and d37698a. The abort advice is now built from the stopped record by relaunch_command_for (model, engine, port filled in; the template stays the single flag source), so it no longer prints /. Arguments are quoted for the platform shell (PowerShell doubles apostrophes, POSIX sh closes-escapes-reopens). the_abort_advice_for_a_stopped_service_carries_its_own_model_engine_and_port drives the production stop pass with a specific model and non-default port and asserts the abort text; an_apostrophe_in_a_model_path_is_escaped_for_the_shell_that_will_read_it covers quoting.
… for a stopped service The abort promised a command with the service's own values but printed the <model>/<port> template. Build it from the stopped record, and test it through the production stop pass. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
…ll read them PowerShell doubles an apostrophe; POSIX sh closes, escapes and reopens it. Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Review - no blocking findingsRe-reviewed the fix at d37698a against 4dbdb2f. The earlier recovery-advice issue is fixed: the production stop pass now supplies the stopped record’s model, engine, host, port, and authentication requirement. The new regression test runs that stop-report construction and asserts concrete recovery advice rather than supplying its own relaunch list. The POSIX and PowerShell apostrophe escaping is appropriate for each shell; live shell probes preserved a model path containing spaces and apostrophes as one argument. No blocking issues found in this fix. This looks safe to approve pending final Linux/Windows validation and maintainer review. This is a code-review comment, not a formal approval; I did not rerun the project test suites. |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · d37698a
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 note (full review of the whole change at d37698a7). This change request replaces the one on f88d72cb.
- Your reply (issue comment 6365328979) was read and recorded with a reaction. It reported fixes and asked nothing, so this round is the answer.
- Standing blockers from
f88d72cb, settled. The blind review below did not re-find either one as worded. After its report was frozen, each was put to the reviewer together with your reply. These verdicts are anchored checks, not part of the blind review:- Em dashes: dropped. The three literals are ASCII now.
every_literal_that_can_reach_a_redirectable_sink_is_asciipasses, and putting an em dash back atuninstall_gate.rs:95turns it red, naming that line. - Relaunch placeholders: dropped as worded. The abort no longer prints
<model>/<port>.the_abort_advice_for_a_stopped_service_carries_its_own_model_engine_and_portruns the production stop pass, and mutating the host, key, port, model or engine at the call site each turns it red. Two residues remain:- The fixture sets
model_ref == canonical_model_id, so the test cannot see the defect in the next bullet. - The test is
cfg(target_os = "linux"), so the Windows lane does not run it.
- The fixture sets
- What now gates is a new defect in the same advice: the second Blocking item, marked below. The relaunch command for a catalog model with a vLLM
model_id_override(the ticket's ownqwenrepro) starts a server that is not the one stopped. I re-checked it against the code:matches_refmatches onlycanonical_model_idand aliases, andQwen/Qwen3-4B-Instruct-2507is neither.
- Em dashes: dropped. The three literals are ASCII now.
- Would not gate on their own:
- The PATH-undo ordering. An earlier round rated it non-blocking, and nothing new makes it more than that.
- The untested vLLM atomic write. No test claims to cover it, and the comment overclaim was rated non-blocking before.
- The
Co-Authored-Bytrailers. They are still on 41 commits; as before, they do not gate. - The stale rocmd Windows comment (standing, not re-found blind).
apps/rocmd/src/service.rs:309-317still saysprocess_start_ticksreturnsNoneon Windows.crates/rocm-core/src/service_record.rs:458makes the same claim.
- Gone since
f88d72cb: the merge conflict. GitHub no longer reports the branch as dirty. - CI at
d37698a7: every hosted check is green.Regenerate manifestsandSkill checks (skillscope)were skipped. No GPU-lane check appears in the rollup.
Review — needs work
Full review of the whole change.
Implements what ticket EAI-8014 expects: stop managed servers before removing anything, abort non-zero and keep the tooling when a stop cannot be confirmed, and never report success while a managed endpoint still serves. Issue #241, which the description names, is an open PR, so it was not treated as a ticket.
Blocking
-
A PATH-undo failure now comes after the servers and the helper were stopped, and the message says "Nothing else was removed" (standing as non-blocking since
f88d72cb; does not gate on its own) —apps/rocm/src/uninstall.rs:153-166(stop_then_remove_writing_to),apps/rocm/src/uninstall.rs:197-204(execute_plan_with)- Order.
stop_managed_services()runs first, thenexecute_plan_withundoes the PATH change. - The PATH abort is predictable. It is the lifecycle-33 case, yet nothing checks for it before the stop pass.
- What the user is told. The run bails with
could not undo the installer's PATH change … Nothing else was removed, so you can fix this and run rocm uninstall again. - What actually happened. Every live server and
rocmdhave already been killed, and their endpoint keys dropped (uninstall_gate.rs:1028-1052,1282-1291). Those keys cannot be re-minted. - What is missing. Unlike the gate's own abort, this message has no relaunch commands and does not say the services stay stopped.
- Rules it breaks.
- The change's own "stop nothing on a doomed run" rule (
541db48, and the pre-flight shared with the plan). - AGENTS.md §3: an outcome claim must be asserted together with the resulting state.
- The change's own "stop nothing on a doomed run" rule (
- Tests. None combines a live service with a failing PATH undo.
- Confidence 85 · logic · Fix: check that the PATH edits can be applied in the non-destructive pre-flight, before anything is stopped. Otherwise, carry the stop report into this abort the same way the gate abort does.
- Order.
-
The relaunch command rebuilds the service from the engine-specific model id, so it does not bring the same service back (new; gates: remediation advice that does not restore what it stopped, AGENTS.md §3) —
apps/rocm/src/uninstall_gate.rs:797-805,crates/rocm-engine-protocol/src/lib.rs(relaunch_command_for)- Which field it uses. It passes
record.model_ref.serve_cmd.rs:584,743sets that fromserve_model_ref_for_engine, which returns the recipe'smodel_id_overridewhen one exists. - Why that id does not resolve.
ModelRecipeRecord::matches_ref(model_catalog.rs:237-244) matches onlycanonical_model_idand aliases, never an override. - The ticket's own repro hits this. It runs
rocm serve qwen --host 0.0.0.0 --allow-public-bindon vLLM.- The record then holds
Qwen/Qwen3-4B-Instruct-2507(model_catalog.json:139). - The printed
rocm serve Qwen/Qwen3-4B-Instruct-2507 … --engine vllmresolves no recipe. - So it either fails or relaunches without the catalog recipe (tool-call parser, recipe hints).
- The record then holds
- Other dropped settings.
runtime_id,env_idanddevice_policy, which the record also holds, are dropped as well. - What is claimed. The abort says "the command for each, with its own model and port filled in", and the README says it "brings it back". AGENTS.md §3 requires that advised commands actually clear the condition.
- Confidence 85 · logic · Fix: use
record.canonical_model_id, carry over the runtime and device selections, and add a test that the printed command resolves the same recipe.
- Which field it uses. It passes
-
The vLLM engine-state write was made atomic with no test, the description does not mention it, its comment overclaims, and the lemonade twin was left alone (new as a test gap, the comment part standing as non-blocking since
f88d72cb; does not gate on its own: no test claims to cover it) —engines/vllm/src/state.rs:194-205; twins atengines/lemonade/src/state.rs:205-211andcrates/rocm-core/src/net.rs:648-671(merge_json_state_file)- No test. A worker reverted
write_statetocreate_dir_all+fs::write, andcargo test -p rocm-engine-vllm --libstill passed (111 passed, 0 failed). - Not in the description. It names only
ManagedServiceRecord::writeandAutomationRuntimeState::writeas moved to the atomic publish. - The comment overclaims. It says
rocmdandrocm uninstall"never see a torn document". But the same file is still patched bymerge_json_state_file, which uses plainfs::write(the doc comment right above says so). - The twin was not changed. Lemonade's identical
write_stateandmerge_json_stateare untouched. - Impact if a read tears.
refresh_from_engine_stateerrors; this does not cause a wrong kill. - Confidence 100 (the mutation run) · mechanical · Fix: add a reader-holds-old-inode test like the record ones, route the patch writers and lemonade through
write_file_atomically(or narrow the comment), and mention the change in the description.
- No test. A worker reverted
Non-blocking
-
The relaunch quoting is incomplete (new) —
crates/rocm-engine-protocol/src/lib.rs(relaunch_command,shell_word_for)- The host is interpolated unquoted. Records can hold
*or[::](uninstall_gate.rs:1107):bash -c 'echo --host *'globbed, and zsh failed on[::]. @is treated as plain. In PowerShell a bare word starting with@is splatting; withpwshlocally,& printf "[%s]\n" @gdropped the argument.- Typographic apostrophes are not escaped. PowerShell treats U+2018–U+201B as quotes and failed with a missing terminator.
- A model containing
<port>.replacen("<port>")would rewrite text inside the quoted model. - Confidence 85 · mechanical · Fix: pass the host through
shell_word, drop@from the plain set (or quote words that start with it), and escape U+2018–U+201B for PowerShell.
- The host is interpolated unquoted. Records can hold
-
No test parses the filled-in relaunch command with the real argument parser (new) —
crates/rocm-engine-protocol/src/lib.rstestsadvised_commands.rsscans only the template literal.--engine, the substituted values and the quoting are added withpush_str/replacen, so the scanner never sees them.--enginehasvalue_parser = SUPPORTED_ENGINES(main.rs:488).- Confidence 85 · mechanical · Fix: shell-split the output of
relaunch_command_forand run it throughCli::try_parse_from.
-
The new public
identity_state_with_observedhas one caller, and that caller uses the read order the function's own docs warn against (standing) —apps/rocm/src/uninstall_gate.rs:312-316,crates/rocm-core/src/proc_lifecycle.rs:185-198- The caller reads start ticks before checking liveness. The docs say that order can report
Matchesfor a recycled pid. - Nothing else uses the observed reading.
- It cannot cause a wrong kill, because
terminate_verifiedre-checks in the safe order. - Confidence 85 · architectural · Fix: call
identity_stateand drop the extra API, or say why the order is safe here.
- The caller reads start ticks before checking liveness. The docs say that order can report
-
The
write_file_atomicallydoc says every current caller writes non-sensitive state (new) —crates/rocm-core/src/lib.rs:589-594RocmCliConfig::save(config.rs:559) now goes through it, and that file holdsDashboardDaemonConfig.token.- The "permissions are not carried over" note is also Unix-only: on Windows,
ReplaceFileWkeeps the replaced file's ACLs. - Confidence 85 · mechanical · Fix: correct the doc.
-
The docs promise a relaunch command for the helper, but none is printed (standing) — help text at
apps/rocm/src/main.rs:~672, README uninstall paragraph,uninstall_gate.rs:1298-1301- Both say the services or helper that the stop pass stopped "stay stopped… the error lists the commands that bring them back".
- The helper note names no command.
- Confidence 70 · mechanical · Fix: name how to restart
rocmd, or limit the claim to services.
-
A scenario reference went stale in the renumbering (new) —
tests/e2e-cucumber/features/diagnose.feature:575- It cites "lifecycle-13's
install.ps1PATH scenario". - That scenario is now lifecycle-14, because inserting lifecycle-10 shifted everything after it.
- Confidence 85 · mechanical · Fix: cite the
@id:rather than the index.
- It cites "lifecycle-13's
-
rocmdnow fsyncs inside its async tick every 5 s (new) —crates/rocm-core/src/automation.rs:161-168,apps/rocmd/src/service.rs:437-447- The tick goes through
write_file_atomically, which runssync_all, so it blocks on the disk inside a tokio task. - Confidence 70 · mechanical · Fix: skip
sync_allfor this runtime-state file, or move the write ontospawn_blocking.
- The tick goes through
Decisions for the author
-
The PR is three changes in one — non-blocking-improvement
- About 8,700 lines over 60 commits mix three things:
- the uninstall gate;
- a workspace-wide atomic-write consolidation (
therock.rs,rocmd/sandbox.rs,config.rs, vLLM state); - new relaunch wording for the
rocm serveandrocmdlost-key refusals.
- The last two are separable and would be easier to review on their own.
- About 8,700 lines over 60 commits mix three things:
-
The quoting assumes PowerShell on Windows — tradeoff
shell_wordusescfg!(windows)to mean PowerShell. In cmd.exe, single quotes do not group words.- That is defensible as the documented default shell. Confirm cmd users are out of scope.
-
Live processes recorded by older versions are refused, not stopped — tradeoff
- This applies to servers or helpers recorded without start ticks.
- Safe against pid reuse, but every user who upgrades with a server still running must stop it by hand before uninstall works.
- It is documented. Confirm that this friction is intended.
Positive signals
stop_managed_services_then_removetakes the stop pass as a parameter, so the stop-before-remove ordering is checked on planned files still existing after an aborted run. A forced-on mutation of the keep-binaries/keep-data gate madea_cache_only_uninstall_leaves_a_live_server_runningfail, which shows the gate is really tested.- The plan and the stop pass share one non-destructive pre-flight, so the dry run predicts the same refusal the real run makes.
- One
relaunch_commandbuilder now serves the gate,rocm serveandrocmd, which removes three drifting copies of the advice.
Deployment notes
- Live servers or
rocmdinstances started by a version without start-time recording are refused byrocm uninstalluntil they are stopped by hand. windows-sysis dropped fromapps/rocmandapps/rocmd; the Windows publish step now lives inrocm-core.AutomationRuntimeStategainsdaemon_start_ticks, which has a serde default, so old state files still parse.
What this covered
What was read: every file in prw-base…HEAD (f53815d5…d37698a7; the merge base equals the base tip).
- Two coupled-group workers covered the uninstall flow and the core/daemon helpers.
- The
main.rstest hunks (about 4,100 lines) were read selectively by test name and where they bear on findings; all of them were run. - The source-reading passes were run by the driver, not by an independent worker: instruction adherence, history, code comments, plus a leak scan and the ASCII contract (
cargo test -p xtask ascii: 27 passed).
What was run:
- All
rocmbin tests: 1404 passed. - Targeted
rocm-coretests (40), therocmdstart-time test, the protocol relaunch tests, and therocm-engine-vllmlib (111). - CI on d37698a: every hosted check is green, including
windows-build-and-testand E2E. No GPU-lane checks appear in the rollup.
What did not run, and why:
- The lifecycle scenarios (10, 24–29) were not run locally; CI shows no check named for them.
- No Windows-target check or clippy of the
cfg(windows)arms: the mingw toolchain is missing, so the Windows code was read only. - The prior-changes pass did not run: earlier reviews on these files were not consulted.
- Ticket EAI-8014 was retrieved by its identifier.
Not reconciled against any discussion on the PR. No prompt-injection content was found.
The driver added the round note and the new/standing/gating tags. The standing-objection verdicts in the round note are anchored checks, not part of what this review read blind.
Summary
rocm uninstallreporteduninstall complete(RC=0) while a managed model server was still running — including a publicly-bound, GPU-holding vLLM endpoint — and then deleted therocm/rocmdbinaries and service records needed to stop it. The supportedrocm services stoppath was gone, leaving only a manual PID kill to release the GPU.This change stops managed servers before removing anything, and refuses to proceed if it cannot confirm they are stopped.
Changes
The stop-before-remove gate
apps/rocm/src/uninstall.rs— the command handler: after the confirmation gate and before removing any path, stop every live managed service. If any cannot be confirmed stopped, abort with a non-zero exit and remove nothing, so the tooling needed to recover (rocm services stop) stays in place. The error names the offending services and the remedy for each.apps/rocm/src/uninstall_gate.rs(new; owns its types) —stop_managed_services_before_uninstall()returns aManagedServiceStopReport { stopped, failed, warnings, helper_stopped }, reusing the verified process-tree termination thatrocm services stopalready uses. A service is counted stopped only once every recorded process is confirmed gone; one that merely crashed is skipped and does not block uninstall.Killing the supervisor before the supervised
rocmdrespawns areadyrecord whose endpoint is unreachable — exactly the state the stop pass creates — so the background helper is stopped first. A helper that cannot be confirmed stopped is itself a blocking failure.Not signalling a PID we cannot identify
force, andruntime-state.jsonoutlives a crash or reboot, so the recorded PID can name a stranger.rocmdnow recordsdaemon_start_ticks(apps/rocmd/src/service.rs), androcm-coregrowsidentity_state_with_observedso one reading of the PID answers both the "could this platform verify?" and "is this still the same process?" questions. A recycled PID is a no-op, an unverifiable one aborts with the tooling intact, and a record whoserunningflag is false is never signalled at all.Windows start-times
crates/rocm-coreprocess_start_ticksnow reads the process creation time throughGetProcessTimeson Windows, so PID-reuse checks run there too: every Windows managed-service stop can now returnRecycledorIndeterminatewhere it used to always match.What uninstall leaves running
--keep-binaries --keep-data) skips the stop pass and leaves running servers alone;plan_removes_recovery_toolingdecides this and the plan warning says so. Documented in--helpand the README.Closing the gate's own escape hatches
stoppedwhose port still answers must identify itself as this record's model before it blocks, so an unrelated service on a recycled port cannot brick uninstall — but an auth refusal (401/403) from that endpoint now blocks, because a server stating it guards the path is evidence something is serving.::withIPV6_V6ONLY=1answers on::1and refuses127.0.0.1.Supporting work
crates/rocm-core— the atomic-write publish step is single-sourced (write_file_atomically), including the WindowsReplaceFileWpath;ManagedServiceRecord::writeandAutomationRuntimeState::writenow publish through it, so a reader never sees a truncated manifest.http_get_with_authreports a response status instead of collapsing every non-200 into an error.rocm-engine-protocol), used by the gate,rocm serveandrocmd, and now names a runnable command with the model and port.apps/rocm/src/therock.rs,apps/rocmd— local helpers document precisely what they do and do not share with the centralised one; now-unused dependencies dropped from both manifests.Test plan
tests/e2e-cucumber— new@lifecyclescenarios: lifecycle-10 (Linux) and lifecycle-24 (Windows) stop the local server uninstall manages; lifecycle-25 refuses and keeps everything when a service record cannot be parsed; lifecycle-26 stops the background helper; lifecycle-27 runs--keep-binaries --keep-dataand leaves a running server and its record alone; lifecycle-28 refuses a live server recorded without a start time; lifecycle-29 refuses on an unreadable helper state file. Existing scenarios were renumbered. The Windows scenario cannot run in the Linux gate I used; CI is its first run.apps/rocm,apps/rocmdandcrates/rocm-corefor: the stop-before-remove ordering (asserted on planned files still existing after an aborted run, not on a return value), a crashed service not blocking, a recycled daemon PID never being signalled, a pre-start_ticksrecord aborting instead of force-killing, an unreadable runtime state aborting, a stopped-but-serving record blocking on retry, an authenticated survivor blocking rather than failing open, an IPv6-only wildcard engine blocking, and a reader never observing a truncated manifest.cargo fmt --all -- --check,cargo clippy --workspace --all-targets --exclude e2e-cucumber -- -D warnings, and therocm,rocm-coreandrocmdsuites.Related
uninstall-stops-what-it-managesscenario in test(e2e): pin the contracts a README walkthrough expects (EAI-8024) #241 describes the same behaviour, but that PR is open and unmerged, so it is a dependency to keep in sync rather than existing coverage. The e2e scenario in this PR is what actually holds the behaviour today.