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>
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 824a3eb
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 (round 10, full review of the whole change). This round files a change request again. It replaces the one at 1edde9cb as this account's whole position. Each finding is tagged new or standing. This also answers your reply about 824a3eb5. What changed since 1edde9cb:
- Settled, by putting each to the reviewer after its report was frozen (anchored):
- The helper listed as a stopped service.
helper_stoppedis its own field, and the abort words the helper separately. Left for the next round: the test asserts text on hand-built reports only, and it does not assert the "nothing supervises" consequence. - The
ProceedUnidentifiedwarning. Dropping that push now turnsan_endpoint_that_answers_without_a_usable_listing_discloses_the_fail_openred.
- The helper listed as a stopped service.
- Fixed in production, not yet in the test: the doomed-run ordering. Already-stopped records are now probed before anything is stopped. The test half is still open; see the third bullet of "Three branches…" below.
- Still standing: the conflict is back.
mainmoved after this review's base: #540 merged at0a021785. - Still standing: most of the untested fail-closed branches, the plan warning, and the description. Your note says those tests were not done this round.
- The red
E2E tests (MI300X)lane is not this PR's doing. The same scenarios (bench-06,chat-06/07,serve-01/02/05) fail with the same vLLMOutOfMemoryErroronmain's own push run at2c37f5bc(run 37835680511). The reviewer lists it below because a red required check blocks merge. It is not a defect in this diff and is not a reason for this change request. It needs a re-run once the GPU is healthy.
Review — needs work
Full review of the whole change.
Implements the expected behaviour of EAI-8014 (the tracker now returns it as ROCMAI-290): stop managed services before removing the tooling, abort non-zero and keep that tooling if a stop can't be confirmed. Runs with --keep-binaries --keep-data are deliberately exempt from the stop pass (see Decisions).
Blocking
-
Reusing a port for the same model makes uninstall refuse, with false advice (new) —
apps/rocm/src/uninstall_gate.rs:452-457(probe_records_for_survivors(paths, &already_stopped, &[], …)then the early return)
generate_service_idcreates a new id on every launch. Soserve M→services stop→serve Magain leaves record A "stopped" and record B "ready" on the same host:port. The probe of already-stopped records runs before any live service is stopped. It finds B answering with A's model, or a 401 when B requires a key, and blocks A withStopWhatHoldsThePort. B is never stopped.
The abort tells the operator that "the engine outlived its supervisor, sorocm services stophas nothing left to kill". Both claims are false here. It then tells them to find and kill whatever holds the port, which is their own healthy managed server.
A temporary unit test usingstop_managed_services_withreproduced this. Both variants (no auth, and B requiring auth) failed with A infailedandstop_calls=[](2 tests ran, 2 passed).
Confidence 100 · logic · Fix: leave out of the pre-stop probe any already-stopped record whose normalized host:port belongs to a live record. Probe those after the live stops instead. Add the sequence above as a test. -
The
--allow-public-bindadvice cannot restore a public service (new) —apps/rocm/src/uninstall_gate.rs:885-890
The abort says a publicly bound service "has to be served again withrocm serve --allow-public-bindto return". Without--host,rocm servebinds loopback (validate_bind_host,main.rs:6884). The existing restart error atmain.rs:7034gives the working form:rocm serve --host {host} --allow-public-bind. AGENTS.md §3 requires a named remedy command to accept those flags and actually clear the condition. The only test (main.rs:39681) just checks that the string contains--allow-public-bind.
Confidence 85 · mechanical · Fix: name--host <host> --allow-public-bind(and the key options) asmain.rs:7034does, and assert the advice against whatvalidate_bind_hostaccepts. -
A plan warning promises a refusal that some runs never perform (standing) —
apps/rocm/src/main.rs:21085
Whenload_managed_servicesfails, the plan prints "Uninstall will refuse to remove anything until they can be read". On a--keep-binaries --keep-datarun,plan_removes_recovery_toolingis false, the stop pass is skipped (uninstall.rs:55-61), and removal goes ahead. No test asserts either the message or the behaviour; it is not referenced anywhere else.
Confidence 85 · logic · Fix: base the sentence onplan_removes_recovery_tooling(&plan, paths), as the live-records warning just below already does, and add a test. -
Three branches this change introduces stay green when broken on their own (standing in part: the first bullet is the open test half of the doomed-run fix, the third was raised at
1edde9cb, the second is new) —apps/rocm/src/uninstall_gate.rs:458,crates/rocm-core/src/proc_lifecycle.rs(identity_state_with_observed),crates/rocm-core/src/lib.rs:~2927(AutomationRuntimeState::write)
Each was mutated alone and the targeted tests rerun (102 rocm and 26 rocm-core tests):- Deleting the early return after the already-stopped probe left everything green. Without it, a run that is going to abort still force-kills a verified live
rocmdfirst.a_survivor_behind_an_already_stopped_record_dooms_the_run_before_anything_is_stoppedclaims to cover this but has no daemon in its fixture, so it cannot fail on it. - Deleting the liveness check in
identity_state_with_observedleft everything green, although its doc promises a pid that has exited yieldsGone. - Reverting
AutomationRuntimeState::writetofs::writeleft everything green. The same change toManagedServiceRecord::writeis caught bya_service_record_write_never_shows_a_reader_a_truncated_manifest.
Confidence 100 · logic · Fix: add a live verified daemon to the doomed-run fixture and assert it was never signalled. Add a directGonecase foridentity_state_with_observed, and a truncated-read test for the runtime-state file.
- Deleting the early return after the already-stopped probe left everything green. Without it, a run that is going to abort still force-kills a verified live
-
The description misstates the change in both directions (standing in part) — PR description
- It names the wrong files.
stop_managed_services_before_uninstallis inapps/rocm/src/uninstall_gate.rs, notmain.rs.daemon_start_ticksis recorded inapps/rocmd/src/{service,watchers,persistence}.rs;apps/rocmd/src/lib.rsis not in the diff. - It does not mention three changes:
- Windows start-time reading (
GetProcessTimes) inproc_lifecycle.rs. Every Windows managed-service stop can now returnRecycledorIndeterminatewhere it used to always returnMatches. - The
--keep-binaries --keep-dataexemption from the stop pass. write_active_runtime_markerswitching to the atomic helper.
Confidence 85 · mechanical · Fix: update the description to match the diff.
- Windows start-time reading (
- It names the wrong files.
-
The required check
E2E tests (MI300X)is failing on this head (new; see the round note: the same failure is onmain, so this PR did not cause it) — CI
The failure was in "a model is being served on GPU" and "a model is served in the background". vLLM ran out of memory: "144.15 GiB is allocated by PyTorch… 46.88 GiB is free". That points to an infrastructure cause (a busy shared GPU) rather than this diff, but the check is red and needs a rerun to green.
Confidence 100 · mechanical · Fix: re-run the lane, or check with the lane owner whether the GPU was busy. -
The branch conflicts with the base again (standing: checked by the driver against the current base, not by the blind review, which read against
db11e474, where it merged cleanly) —apps/rocm/src/main.rs,docs/architecture.md
mainis now at0a021785. #540 moved driver-install, engines and serve out ofmain.rsthere.git merge-tree --write-tree 0a021785 824a3eb5reportsCONFLICT (content)in both files, and GitHub reportsmergeable_state: dirty. Until it is resolved, no CI runs for the branch.
Confidence 100 · mechanical · Fix: merge or rebase onto currentmain, and re-check that the gate code relocated by #540 still has the call sites this PR adds. -
New fail-closed branches that no test exercises: each mutation below stayed green when run alone (standing: not re-found by this round's blind review, then each mutation re-run at
824a3eb5— anchored) —apps/rocm/src/uninstall.rs(host_port_resolves),apps/rocm/src/uninstall_gate.rs(unreadable_service_manifestsnon-.jsonfilter; daemon killKillScope::Tree,force: true),apps/rocm/src/main.rs(plan_removes_recovery_tooling, theentry.path.starts_with(&services_dir)arm),crates/rocm-core/src/lib.rs(unixpublish_temp_file)- Each of these six still leaves every test green:
host_port_resolvesmade always false.- The non-
.jsonfilter dropped. - The
services_dirprefix arm dropped. - The daemon kill changed to
KillScope::Single. - The daemon kill changed to non-force.
- Delete-then-rename in
publish_temp_file(594rocm-coretests passed).
- Each lone failure in a full run was re-run in isolation and passed. It was the shared-process flake
restart_refuses_a_public_service_without_a_key_before_stopping_it, which also fails with no mutation. - Now covered: reading the start-time before the liveness check is caught by
a_dead_process_is_never_read_for_a_start_time. - Still holds, by reading only: discovery of a really unreadable services directory.
a_stop_pass_that_cannot_run_leaves_every_planned_path_in_placeinjects the error through a closure, so the real?path is never exercised.
Confidence 100 · logic · Fix: one test per branch that fails when it is mutated.
- Each of these six still leaves every test green:
-
The description claims the publish step is "single-sourced" (standing: not re-found by this round's blind review, then checked — anchored) — PR description,
apps/rocm/src/remote/session.rs:128
The description still says "the atomic-write publish step is single-sourced (write_file_atomically)". At824a3eb5,session.rsstill publishes its staged file with a plainfs::rename(&staging, &path), and this PR does not touch that file.
Confidence 85 · mechanical · Fix: narrow the claim to the sites this PR converts, or migratesession.rs.
Non-blocking
-
A false comment, and stale-record advice that leaves out the command that would clear it (standing) —
apps/rocm/src/uninstall_gate.rs:499-503,:75-81
The comment says "there is noservices remove".rocm services remove <id> --yesexists (added in #411) and accepts any record that is not in use. When an unrelated server that answers 401/403 holds a stale record's old port, the only advice printed is to stop that unrelated server. Removing the stale record would be the cheaper fix, and the advice doesn't offer it. Confidence 85 · logic · Fix: correct the comment, and offerrocm services remove <id> --yesfor already-stopped records. -
Comments left stale by Windows gaining start-times (standing) —
apps/rocm/src/uninstall.rs:394("Someonly on Linux"),apps/rocm/src/uninstall_gate.rs:336("arm meant for Windows"),crates/rocm-core/src/proc_lifecycle.rs:50-52("clock ticks since boot…Noneon platforms without/proc"), and theuninstall.rs:7-11module doc ("Mechanically relocated… with no behavior change"). The code governs in each case:process_start_ticksnow returns a FILETIME on Windows, and the module now changes behaviour. Confidence 85 · mechanical · Fix: rewrite them to match the code. -
The docs omit that uninstall force-kills
rocmd(standing) —apps/rocm/src/main.rs:595-599(--help),README.md,docs/manual-testing.md§8, plan warningmain.rs:21144-21162
Any run that removes the tooling tree-kills a verifiedrocmd(uninstall_gate.rs:250-262), even with no services, and that can abort the run (StopTheDaemon,RepairTheDaemonState). All of these surfaces mention only "managed model servers". AGENTS.md §5 asks for docs to stay in sync with observable behaviour. Confidence 70 · mechanical · Fix: mention the helper stop and its abort classes. -
The doc comment says "Two limits" and then lists three (standing) —
crates/rocm-core/src/lib.rs:4762-4785. Confidence 85 · mechanical · Fix: say "three", and untangle the "not the containing directory" clause. -
Logic and a test written only for macOS (standing) —
apps/rocm/src/uninstall_gate.rs:795(record_predates_start_ticks's own-pid conjunct is always true on Linux and Windows), andwithout_readable_start_times_a_bare_record_is_not_a_legacy_record, which iscfg(not(any(linux, windows))). AGENTS.md §6 says not to implement unsupported-platform behaviour. Confidence 70 · architectural · Fix: drop the macOS path, or explain why it is needed. -
A test is placed between
use super::*;anduse crate::test_support…(new) —apps/rocmd/src/service.rstests module. It is also Linux-only, though Windows now records start-times too. Confidence 70 · mechanical · Fix: move it below the imports and widen it toany(linux, windows).
Decisions for the author
-
The first uninstall after an upgrade aborts while a pre-upgrade
rocmdis still running — tradeoff
Such a record has nodaemon_start_ticks, so it lands inUnverifiableand the operator has to kill the pid by hand. That is the safe direction (never signal an unverified tree), but it is a one-time cost on the ordinary upgrade path. Confirm it is intended and say so in the release notes. -
Cache-only runs report
uninstall completewhile servers keep running — tradeoff
The ticket says "Never reportuninstall completewhile a rocm-cli-managed endpoint is still serving." The--keep-binaries --keep-dataexemption keeps the tooling, so the ticket's underlying harm doesn't occur, but that wording is not met literally. Confirm this was deliberate. -
Three atomic-write implementations remain — non-blocking-improvement
Onlypublish_temp_file/replace_file_windowswere consolidated.apps/rocm/src/therock.rs:4636-4790andapps/rocmd/src/sandbox.rs:655-760keep their own stage-and-publish chains without thesync_allthe core helper does. Exposing a_withseam on the core helper would allow one copy.
Positive signals
- The ordering puts every check that can doom the run before anything destructive: the free manifest scan, then the already-stopped probe, then the daemon kill, then the per-service stops.
StoppedRecordVerdict::blocks,StopFailureRemedy::adviceandadvice_rankare exhaustive matches, so a new variant has to be placed on one side of a destructive decision before it compiles.- The injectable
stop_*_withseams make the abort paths testable, and the tests hold up: 21 of 25 single-branch mutations turned a test red. - Wildcard binds are probed on both
127.0.0.1and::1, and the identity probe goes to the address that actually answered.
Deployment notes
runtime-state.jsongains an optionaldaemon_start_ticks. Old files still parse. A daemon started before the upgrade blocks uninstall until it is stopped by hand.- On Windows, managed-service records now carry a process creation time, so
rocm services stopcan newly report a recorded pid as recycled or unverifiable. - Service manifests, runtime state and the active-runtime marker are now replaced rather than rewritten in place. File mode is not carried over, and a symlink at the target is replaced instead of written through.
What this covered
Every file in the change read completely, by grouped workers: the gate (uninstall_gate.rs, uninstall.rs, the production hunks in main.rs, the endpoint-identity API in rocm-core); identity and persistence (proc_lifecycle.rs, rocm-core's state and atomic-write code, therock.rs, rocmd, the manifests); tests; e2e and docs. Also read: the callers of services remove, validate_bind_host and generate_service_id. Reviewed efcd4800…824a3eb5 (prw-base...HEAD).
- Runs: the unit suites for
rocmuninstall (39 tests) and the added tests (47 in rocm, 11 in rocm-core) all passed. The lifecycle e2e passed: 11 scenarios, including lifecycle-10 and lifecycle-25, with a fresh report. - Mutation pass: 25 single-branch mutations, plus one temporary reproduction test, all reverted.
- Shared build cache:
CARGO_TARGET_DIRis shared, and one rocm-core run once picked up another checkout's binary and ran 0 tests. Every run used as evidence was confirmed as a recompile with the expected test count.
Did not run or only partly ran:
- Prior-changes pass: partial.
gh apireturned no review comments on #117 or #169, and no other earlier PRs were checked. - Design review: the driver answered the questions itself from the workers' reads instead of sending them to separate workers.
- Windows
cfgarms: checked by reading only. - Crate-edge check:
cargo xtask check-crate-edgeswas not run.
The PR's discussion and earlier reviews were not reconciled here. The two anchored blocking items, and the conflict and CI notes, were added after the review by the driver. The anchored items were settled by a check that was given the objection; that check is not part of what this review read blind.
…an read-error warning; pin more fail-closed branches Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Pushed 43fbb58 (merged main incl. #540's relocations; conflicts in main.rs/architecture.md resolved, gate builds and passes on acer). Blocking: (1) port reuse: already-stopped records sharing host:port with a live record are now probed AFTER the live stops, test serving_the_same_model_again_after_a_stop_does_not_doom_the_uninstall (stop of B frees the port; A not failed). (2) advice now says 'rocm serve --host --allow-public-bind' (add --api-key), asserted. (3) the plan's read-error warning only promises a refusal where plan_removes_recovery_tooling holds, else says the run keeps the tooling; test with an unreadable services dir. (4) doomed-run test now has a verified live daemon and asserts it is not killed/recorded; added a direct Gone test for identity_state_with_observed and a reader-safety test for AutomationRuntimeState::write (fails when write reverts to fs::write: checked). Not done: the six remaining mutation survivors (host_port_resolves, non-json filter, services_dir prefix arm, daemon Tree/force scope, unix publish_temp_file delete-then-rename), the PR description rewrite, and the MI300X lane (runner OOM, same on main). |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 43fbb58
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 (round 11, full review of the whole change). This round files a change request again. It replaces the one at 824a3eb5 as this account's whole position. Each finding is tagged new or standing. It also answers your note about 43fbb588. What changed since 824a3eb5:
- Settled, by putting each to the reviewer after its report was frozen (anchored):
- Port reuse for the same model, as worded. Same host spelling, both without a key and with B requiring one: B is stopped first and A is not failed. Nothing pins the key-required variant yet. A different host spelling of the same endpoint still aborts; that is the first new blocker below.
- The plan's read-error warning. It now branches on
plan_removes_recovery_tooling, and forcing that branch turnsthe_plan_read_error_warning_only_promises_a_refusal_where_the_stop_pass_runsred. That test iscfg(unix)and skips itself, still passing, where permissions don't apply (e.g. root). - The three untested branches. Each mutation alone now turns a named test red: the doomed-run early return, the
Goneliveness check, andAutomationRuntimeState::write.
- Resolved, checked by the driver: the conflict.
git merge-tree --write-treeof the currentmain(03afcf43) and43fbb588merges cleanly. - Not a reason for this change request:
E2E tests (MI300X). Its earlier failure also happened onmain. On this head it, and the Strix Halo Windows and WSL2 lanes, had not reported yet. - Still standing: the six mutation survivors (appended at the end of Blocking), the description (including the "single-sourced" claim), and the re-serve advice, re-found blind in a narrower form.
- Not raised before, or raised before as non-blocking: the README/
--helphelper-kill point and the third atomic-write copy were non-blocking last round. Each now has new evidence (key files deleted before an abort that "leaves every file in place"; cleanup branches no test reaches). Neither alone is why this change request was filed.
Review — needs work
Full review of the whole change.
The PR closes the reported defect: uninstall now stops managed servers before removing anything. The description names EAI-8014, which was not retrieved because no tracker tool is available here. The description also cites #241 as a related unmerged PR, which this review did not read.
Blocking
-
A stopped record whose host is spelled differently from a live record on the same port aborts the whole uninstall, and the advice it prints points the operator at their own server (new) —
apps/rocm/src/uninstall_gate.rs:446-462,:529
The "defer a stopped record that shares a live endpoint" check compares host text: it joinsprobe_hosts(host)and adds the port. So0.0.0.0:P(which becomes127.0.0.1|::1) never matches a live127.0.0.1:P, andlocalhostnever matches127.0.0.1. The stopped record is then probed first, the live record's own server answers with the same model, and the run aborts. The abort says "Every process recorded … is gone … find what holds that port", which tells the operator to hand-kill a managed service. When the live partner's stop fails, the deferred record is blocked with the same wrong remedy.
Reproduced: a copy ofserving_the_same_model_again_after_a_stop_does_not_doom_the_uninstallwith only svc-a's host changed to0.0.0.0returnedcalls=[]andfailed=[svc-a … StopWhatHoldsThePort].
Confidence 100 · logic · Fix: compare normalized and resolved probe-address sets on the same port, and skip a deferred record whose live partner is already infailed. -
When the gate refuses to kill a helper pid it cannot verify, its advice tells the operator to kill that pid (new) —
apps/rocm/src/uninstall_gate.rs:161-168,:82-86
daemon_identity_unverifiedusesStopTheDaemon. That remedy's advice is "kill that pid (kill <pid>/Stop-Process -Id <pid> -Force)". Its own reason line says the pid was left alone "rather than risk signalling an unrelated process that inherited its pid".
The realistic path: upgrade, then a reboot. The pre-upgraderuntime-state.jsonstill saysrunning: trueand has no start-time, so the pid now likely belongs to an unrelated process. The PR's own testuninstall_never_kills_a_daemon_pid_from_a_state_file_that_predates_start_tickscalls that processstrangerand asserts it gets this advice.
This violates AGENTS.md §3: remediation must actually clear the condition, and this one asks the operator to do the unsafe thing the gate refused to do.
Confidence 85 · logic · Fix: give this case its own remedy: "confirm pid N isrocmd; if it is, stop it; if not, delete<automation_state_path>", naming the file. -
The re-serve advice names a command clap rejects (standing: the
--allow-public-bindadvice objection, re-found blind in a narrower form) —apps/rocm/src/uninstall_gate.rs:906
The text is`rocm serve --host <host> --allow-public-bind` (add `--api-key <key>`).Servetakes a required positionalmodel(main.rs:433-434), so the command fails as written, and it also drops the port. AGENTS.md §3 requires a named command to exist and accept those flags. The pre-existing message atmain.rs:3816has the same text; fix both.
Confidence 85 · mechanical · Fix:rocm serve <model> --host <host> --port <port> --allow-public-bind …. -
Discovery fail-closed is pinned by no test, while the plan prints a promise that depends on it (standing: earlier rounds noted the real
?path was never exercised) —apps/rocm/src/uninstall_gate.rs:423,:440;apps/rocm/src/main.rs:17955
Run here: replacing both?with.unwrap_or_default()recompiledrocm, and all 41 matching uninstall tests stayed green. That includesa_stop_pass_that_cannot_run_leaves_every_planned_path_in_place, which injects its own failing closure and never reaches this code.the_plan_read_error_warning_only_promises_a_refusal_where_the_stop_pass_runsasserts the "will refuse to remove anything" text only. AGENTS.md §3 requires the message to be asserted together with the behavior.
Confidence 100 · logic · Fix: on the tree the read-error test already makes0o000, also driveuninstall_with_pathsand assert it errors and the planned files survive. -
Several added tests cannot fail on the branch they claim to cover (new) —
apps/rocm/src/uninstall.rs:88,:91;apps/rocm/src/uninstall_gate.rs:191,:672,:840,:970; tests inapps/rocm/src/main.rs
These are from a worker's mutation runs: one mutation at a time,rocmrecompiled each time, and 53 tests ran each time.- Recycled pid treated as ours. Mutating
Recycled => NothingOfOurstoStopleavesuninstall_never_kills_a_daemon_pid_that_was_recycledgreen.terminate_verified's re-check absorbs it, and the test does not assert!helper_stopped. Building the kill identity without the start-time is also green, so each layer hides a regression in the other. - Fail-open warnings never reach the operator. Dropping
eprintln!("warning: …")is green. - Success line. Dropping the success
println!is green. So is dropping!report.helper_stoppedfrom the success-line condition, which leaves a helper-only run printing nothing. - Wrong failure reason. Forcing the "recorded stopped, … serving this record's own model" reason for a service this pass just stopped is green.
uninstall_refuses_while_a_recorded_endpoint_still_accepts_connectionschecks only the failed id. - Unrelated server on a recycled port. Widening the auth-refusal test to
status >= 300is green. No test stages a server that answers 404 or 500, so the case the warning text describes is never exercised.
Confidence 100 · logic · Fix:
- assert
!helper_stoppedin the recycled-pid and legacy-record tests; - capture stderr and stdout, or inject the sink, in an
uninstall.rscommand-level test; - add a helper-only success case;
- assert the remedy and the "after the stop" reason;
- add a 404/500-answering endpoint that expects
ProceedUnidentified.
- Recycled pid treated as ours. Mutating
-
README and
--helpmisstate what uninstall does (standing in part: the helper-kill omission was non-blocking last round; the "leaves every file in place" part is new) —README.md:819-824,apps/rocm/src/main.rs:606-610, plan warnings inbuild_uninstall_plan- The helper kill is undisclosed.
stop_background_helper_before_uninstallforce-kills therocmdprocess tree whenever the plan removes the tooling, even when there are no services. The README,--helpand the plan shown before confirmation never mention it; the only notice is a line printed after confirmation. - "Leaves every file in place" is false on two refusal paths. A per-service stop failure, or a survivor found after the stops, aborts after earlier services were already stopped. Those stops deleted their endpoint key files (
clear_endpoint_api_key), and the abort message itself admits it.
AGENTS.md §5 requires these claims to stay in sync with the code.
Confidence 85 · mechanical · Fix: add a plan warning when the helper is recorded running, mention the helper in README and--help, and say that installed files are kept but servers already stopped stay stopped and lose their keys. - The helper kill is undisclosed.
-
Two new user-observable behaviors have no scenario (new) —
tests/e2e-cucumber/features/install_lifecycle.feature
The two behaviors:--keep-binaries --keep-dataleaves a live server running. Only the Linux unit testa_cache_only_uninstall_leaves_a_live_server_runningcovers it.- Uninstall stops the background helper.
AGENTS.md §3 requires a scenario for each, or a stated justification in the PR text. The PR text gives neither.
Confidence 85 · logic · Fix: add lifecycle scenarios, or justify the gap in the PR text. -
The manual-testing refusal check cannot be followed as written (new) —
docs/manual-testing.md:393-397
Step 1 is a fullrocm uninstall --yes, which has already removed config, data and binaries. Step 2 then asks the tester to corruptservices/bad.jsonand check that those directories "remain". The doc also never says thatserviceslives under the data directory.
Confidence 85 · mechanical · Fix: start step 2 from a fresh install and give the full services path. -
The new core atomic writer is a third copy of the staging logic, and its failure cleanup is untested (standing in part: last round listed the three copies as a non-blocking improvement; the untested cleanup is new) —
crates/rocm-core/src/lib.rs:4669-4830,apps/rocm/src/therock.rs:4637-4691,apps/rocmd/src/sandbox.rs:653-703
temp_sibling_path,ATOMIC_WRITE_TEMP_ATTEMPTSand the staging loop now exist three times, and the copies already differ: only the core copy callssync_all. The core copy has no test hooks, so removing eitherremove_file(&tmp)(the cleanup after a failed write or a failed flush), or breaking the collision loop, leaves every test green (traced; not run). The therock and sandbox copies do test those paths through their hooks.
This repo's bugs tend to cluster in duplicated implementations like these.
Confidence 80 · architectural · Fix: give the core helper thesuffix_for_attemptandbefore_publishhooks undercfg(test)or a crate-private variant, and have therock and sandbox call it. -
The PR description does not match the change (standing; includes the earlier "single-sourced" objection) — PR body
- Unmentioned:
process_start_ticksnow reads a start-time on Windows (GetProcessTimes). This changes every Windowsterminate_verifiedcaller, includingrocm services stopand the lemonade/vLLM stops: a live process thatOpenProcesscannot open is now left unsignalled where it used to get a best-effort match. - Also unmentioned: the plan-warning rewrite, the
write_active_runtime_markermigration, the cache-only skip, and the deferred endpoint probing. - Misattributed: it places
stop_managed_services_before_uninstallinmain.rs(it is inuninstall_gate.rs) anddaemon_start_ticksinapps/rocmd/src/lib.rs(it is inservice.rs). - Inaccurate: it lists the report fields as
{ stopped, failed };warningsandhelper_stoppedalso exist. - Overclaimed: "single-sourced (
write_file_atomically)" — onlypublish_temp_fileis single-sourced.
Confidence 85 · mechanical · Fix: update the description (narrow it to what the change does and add the omissions). Nothing in the code needs to change for this.
- Unmentioned:
-
New fail-closed branches that no test exercises: each mutation stays green (standing: not re-found by this round's blind review, then re-run at
43fbb588— anchored) —apps/rocm/src/main.rs:13281(host_port_resolves; earlier rounds put it inuninstall.rs),apps/rocm/src/uninstall_gate.rs(unreadable_service_manifestsnon-.jsonfilter; helper killKillScope::Tree,force: true),apps/rocm/src/main.rs(plan_removes_recovery_tooling, thestarts_with(&services_dir)arm),crates/rocm-core/src/lib.rs(unixpublish_temp_file)- The five
rocmmutations, applied together (separate paths, so none masks another): all 1063rocmtests passed. They werehost_port_resolvesforced false, the non-.jsonfilter dropped, theservices_dirarm forced false,KillScope::Single, and a non-force kill. - Delete-then-rename in unix
publish_temp_file: all 598rocm-coretests passed. The truncation tests check for a truncated file, not a missing one. - The
host_port_resolvesmutation only adds warnings and never removes a block, so its test would pin a disclosure, not safety. TheSingle/non-force kills are only reachable with a live, verified helper on Linux.
Confidence 100 · logic · Fix: one test per branch that fails when it is mutated.
- The five
Non-blocking
- Comments contradict the code (standing in part) — Confidence 85 · mechanical · Fix: reword each to match the current code.
uninstall_gate.rs:615says the tests "live at the bottom of this file"; they are inmain.rs.:517-518says there is "noservices remove"; it exists atmain.rs:998, as doesservices prune.:850says the function returns file names; it returns full paths.:229and:336, plusuninstall.rs:394andmain.rs:33559and:35191, still describe Windows as the best-effort platform or call start-times Linux-only.proc_lifecycle.rs:50-51still says start-times areNonewherever there is no/proc.uninstall.rs:7still says "no behavior change".
- The claim "one implementation … for the whole workspace" is false (new) —
crates/rocm-core/src/lib.rs:4687(and the identical comments in therock and sandbox).apps/rocm/src/remote/session.rs:128still does its own temp-write andfs::rename. That writer predates this PR; the claim is new. Confidence 85 · mechanical · Fix: move it onto the helper, or soften the comment. - The two reader-safety tests disagree about Windows (new) —
crates/rocm-core/src/lib.rs:4877,:4946. The service-record test is unix-only and says Windows cannot make the same observation. The runtime-state test checks the same property on every platform. Confidence 70 · mechanical · Fix: make the two gates and comments agree, based on the Windows lane result. - "The removal is reported as complete" checks only the text (new) —
tests/e2e-cucumber/tests/e2e/lifecycle_steps.rs(then_removal_complete). It asserts neitherlast_rc == 0nor that the files are gone. Other steps in the same scenarios do check the process state. Confidence 70 · mechanical.
Decisions for the author
- The gate's tests live in
main.rs, not inuninstall_gate.rs— non-blocking-improvement
About 1,900 of the 2,283 lines added tomain.rsare tests for the new module. AGENTS.md §6 wants new subsystems out ofmain.rs, and the stale "bottom of this file" comment shows the drift has already started. Moving them into a#[cfg(test)] mod testsinuninstall_gate.rswould keep them beside the code. - Three disclosed fail-open paths — tradeoff
The three paths are an unresolvable host, an empty model list, and an unidentifiable listener on a stopped record. Each proceeds with a warning instead of blocking. That keeps a recycled port from blocking uninstall with no way out, at the cost of possibly deleting the tooling while a wedged engine of yours keeps the port. The choice looks deliberate; note that the warnings themselves are currently unpinned (see Blocking). - Substantive behavior arrived inside merge commits — non-blocking-improvement
43fbb58(confirmed with--remerge-diff) andc8bdf89add behavior in merge commits: deferred probing, the re-serve advice, the plan read-error warning, and tests. The squash merge makes the final tree correct, but a commit-by-commit reader misses these. Mention them in the PR text.
Positive signals
stop_managed_services_then_removetakes the stop pass as a closure, so the "nothing removed on abort" guarantee is asserted against real files on disk. An ordering-inversion mutation turned 5 tests red.advice(),advice_rank()andblocks()are exhaustive matches, so a new failure class cannot silently go without advice or default to "proceed".- Whether the platform can read start-times is decided by reading this process's own start-time, so a failed read of the daemon's pid cannot be mistaken for an unsupported platform.
- The plan warning and the decision to run the stop pass use the same
plan_removes_recovery_toolingcheck, so the text and the behavior cannot disagree. daemon_start_ticksis#[serde(default, skip_serializing_if)], so old and new state files read each other.
Deployment notes
- Uninstall now force-kills the
rocmdprocess tree whenever it removes the binaries or service records. - After upgrading, while an old
rocmdthat never recordeddaemon_start_ticksis still running, uninstall aborts as "unverifiable" until rocmd is restarted. This now applies on Windows as well as Linux. - On Windows, every verified stop (
rocm services stop, engine stops) now requires a readable process creation time. A process the CLI cannot open, for example an elevated one, is no longer signalled. - Service records and runtime state are now written atomically. On Unix they get umask-default permissions instead of keeping the old file's mode; on Windows
ReplaceFileWkeeps the existing ACL.
What this covered
Every file in the change, read completely, plus the callers and callees of the changed symbols in apps/rocm, rocm-core proc_lifecycle and rocmd. Range: prw-base (03afcf4) … 43fbb58.
- Fan-out: 4 independent workers — gate logic; rocm-core/rocmd; unit-test verdicts with 26 mutation runs; e2e, docs, AGENTS.md adherence, history and impact.
- Re-run here: I reproduced the discovery-fail-closed mutation myself.
- CI at the reviewed head: every completed check passed. "E2E tests (MI300X)", "E2E tests (Strix Halo, Windows)" and "E2E tests (Strix Halo, WSL2)" had no result yet.
Did not run:
- Lifecycle e2e scenarios: not run locally; they need packaging. Judged from the step code, and CI runs them with
E2E_INCLUDE_LIFECYCLE=1. - Windows cross-build: blocked locally because the mingw linker is missing. The moved Windows bodies were type-checked in throwaway crates outside the repository.
- Prior-review comments on these files: not read; that is outside this review's remit.
- EAI-8014: not retrieved; no tracker tool is available here.
Not reconciled against any PR discussion. No prompt-injection content was found. The last blocking item and the round note were added after the review by the driver; the anchored checks were given the objection and are not part of what this review read blind.
… unverifiable helper pid, correct serve advice, pin discovery fail-closed Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Pushed e0ae769. Taken: (1) host-spelling: a stopped record now defers when ANY probe address overlaps a live record's on the same port (0.0.0.0 / localhost / 127.0.0.1); test a_stopped_record_spelled_with_a_wildcard_host_still_waits_for_its_live_partner. (2) unverifiable helper pid gets its own remedy, ConfirmTheDaemonPid: check what the pid is, stop it if rocmd, else delete the stale runtime-state file; no more 'kill that pid' for a pid the gate refused to kill. (3) re-serve advice (here and at main.rs:3816) now 'rocm serve --host --port --allow-public-bind'. (4) discovery fail-closed: the read-error test now also drives uninstall_with_paths on the unreadable services dir and asserts it errors and the planned file survives. Also: helper-only success line test, and the legacy-record test asserts !helper_stopped. Not done: README/--help/plan helper-kill disclosure and 'leaves every file' wording, new lifecycle scenarios for cache-only and helper stop, the remaining mutation survivors (recycled=>Stop, stderr warnings, 404/500 endpoint, wrong-reason, plus the earlier six), PR description. |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · e0ae769
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 (round 12, full review of the whole change). This round files a change request again; it replaces the one at 43fbb588 as this account's whole position. Every finding is tagged new or standing. It also answers your note about e0ae7699 (thanks — I've reacted to it rather than replying separately).
- Settled, by putting it to the reviewer after its report was frozen (anchored): the re-serve advice. Both sites now print
rocm serve <model> --host <host> --port <port> --allow-public-bindand every flag exists; withdrawn. (The blind review keeps a non-blocking note that nothing parses it.) - Narrowed, still standing (anchored checks, details at the end of Blocking): host spelling — the named spellings are fixed, the failed-partner half is not; unverifiable helper pid — the main path is fixed, the
terminate→Unverifiedarm still prints "kill that pid"; discovery fail-closed — removing both?is now caught, each alone is not; mutation survivors — (a) and (c) partly fixed, (b), (d), (e) open; atomic writer — publish-failure cleanup now tested, write/flush cleanup and the collision loop are not. - Still standing in full (you listed these as not done): README/
--helpdisclosure, the two missing scenarios (the PR text gives no justification for the gap), the manual-testing steps, the six fail-closed branches, and the description (re-found blind this round). - New this round: a test that can delete the shared build binaries, and macOS-only logic against AGENTS.md §6.
- Not a reason for this change request: CI. Every reported check at
e0ae7699passed or was skipped; no self-hosted GPU lane appears in the rollup yet. - Verification this round was static. Disk on the review host was under the build floor, so no build, test or mutation was run; every verdict below was reached by reading the code.
Review — needs work
Full review of the whole change.
Does what EAI-8014 (now ROCMAI-290, In Progress) asks for the full uninstall. The PR does not say it closes the ticket. One way the ticket's expected behaviour is deliberately not met is listed under Decisions for the author.
Blocking
-
A test can delete the real build binaries if the gate it checks ever regresses (new) —
apps/rocm/src/main.rs:34883-34890(the_plan_read_error_warning_only_promises_a_refusal_where_the_stop_pass_runs)- The test runs the real
uninstall_with_pathswithforce_dev_binaries: trueandkeep_binariesfalse. - From a test binary in
target/<profile>/deps,daemon_binary_path()resolves to the siblingtarget/<profile>/rocm(crates/rocm-core/src/lib.rs:4478-4489).is_dev_binary_layoutis then true, and the force flag bypasses the skip (main.rs:17855). So the plan containskind: "binary"actions for the realrocm,rocmdandrocm-engine-*in the shared target directory. - Only the services-dir abort stops the removal loop. If that abort regresses, the test deletes the build output that the e2e suite and other developers use.
- Its sibling test
uninstall.rs:526-530passeskeep_binaries: truefor exactly this reason ("keeps the test off the real executable-discovery path"). The services directory sits under the data dir, so the refusal still triggers with that flag. - Confidence 85 · logic · Fix: use
keep_binaries: true(droppingforce_dev_binaries) in the executed run.
- The test runs the real
-
Adds macOS-only logic and a macOS-only test, against AGENTS.md §6 (new) —
apps/rocm/src/uninstall_gate.rs:832-845(the conjunctprocess_start_ticks(std::process::id()).is_some()inrecord_predates_start_ticks),apps/rocm/src/main.rs:35205(#[cfg(not(any(target_os = "linux", windows)))] without_readable_start_times_a_bare_record_is_not_a_legacy_record)- §6 says: "platforms outside Windows/Linux are unsupported; do not implement, debug, or 'fix' unsupported-platform behavior".
- On both supported platforms the conjunct is always true, as the change's own comments admit ("unconditionally true there", "unfalsifiable on this lane"). Its only effect is on macOS.
- The test guarding it "compiles only on macOS, which has no CI lane", so no lane ever runs it.
- The same macOS framing repeats in comments at
main.rsaround the paired test and incrates/rocm-core/src/proc_lifecycle.rs. - Confidence 85 · architectural · Fix: drop the macOS-only test and reduce the predicate to
recorded_start_ticks.is_none(). If the stub arm must stay, keep a one-line note at most.
-
The description misstates the change, both in what it locates and in what it leaves out (standing; re-found blind this round) — PR description, Changes and Test plan
- Wrong locations:
- It places
stop_managed_services_before_uninstall()inapps/rocm/src/main.rs; it lives in the newapps/rocm/src/uninstall_gate.rs:406. - It says
rocmdrecordsdaemon_start_ticksinapps/rocmd/src/lib.rs; the capture is atapps/rocmd/src/service.rs:741. - It gives
ManagedServiceStopReportas{ stopped, failed }; it also carrieswarningsandhelper_stopped.
- It places
- User-visible behaviour it never mentions:
--keep-binaries --keep-dataruns skip the stop pass and leave live servers running (plan_removes_recovery_tooling,main.rs:~18008). This is documented in--helpand the README, but not in the PR text.- Windows now reads process start-times through
GetProcessTimes(proc_lifecycle.rs,#[cfg(windows)] process_start_ticks). That changes daemon identity verification on Windows from best-effort to strict. - The plan warnings are reworded.
- The
write_active_runtime_markerrewrite (main.rs:9217-9234). - The changed public-bind advice in
ensure_public_service_has_endpoint_key(main.rs:3816).
- Test plan: it names "new
install_lifecyclescenario" (one). The change adds three: lifecycle-10 (Linux), lifecycle-24 (Windows) and lifecycle-25 (Linux refusal). - Confidence 85 · mechanical · Fix: update the description to match the change (narrow nothing in the code).
- Wrong locations:
The items below are standing objections from earlier rounds that this round's blind review did not re-find. Each was then put to the reviewer, with your note, after its report was frozen; those verdicts are anchored (the reviewer was told what to look for) and static (nothing was run). Only the part that still holds is listed.
-
A stopped record that waits on a live partner gets the wrong remedy when that partner's stop fails (standing, narrowed) —
apps/rocm/src/uninstall_gate.rs:550,:567,:708- The spelling half is fixed for the named spellings:
shares_a_live_endpoint(:466) now compares probe-address sets, so0.0.0.0/localhostagainst a live127.0.0.1defers. - Still holds: deferred records are probed at
:550, and the only skip is for a record already infailedunder its own id (:567). When the live partner's stop failed and it is still serving, the deferred record is blocked asBlockServingOurModelwithStopWhatHoldsThePort(:708), whose text says every recorded process is gone, beside the partner's correctStopTheServiceline. No test covers a failed partner with a deferred record. - Worth checking: overlap works only through loopback, so a live
0.0.0.0:Pand a stopped LAN-IP/hostname record on:P, or a live::1and a stoppedlocalhost, still share no probe address. - Fix: skip (or re-attribute) a deferred record whose live partner is in
failed.
- The spelling half is fixed for the named spellings:
-
A kill the identity check refuses still prints "kill that pid" (standing, narrowed) —
apps/rocm/src/uninstall_gate.rs:376-383,:92-98,:174-182- Fixed on the main path: an unverifiable or pre-start-time record now gets
ConfirmTheDaemonPid(:180,:367). - Still holds: when
terminate(&identity)returnsTerminationOutcome::Unverified("deliberately left untouched",proc_lifecycle.rs:223), the gate pushesStopTheDaemon— "kill that pid" — for that same unverified pid. Whether this arm is reachable outside a re-read race was not established. - The objection also asked to name the state file; the new advice says "delete the stale runtime state file" and prints no path (the blind review lists this separately under Non-blocking).
- Fix: map
UnverifiedtoConfirmTheDaemonPid, keepStopTheDaemonforTimedOut, and printautomation_state_path().
- Fixed on the main path: an unverifiable or pre-start-time record now gets
-
Discovery fail-closed: each
?alone is still unpinned (standing, narrowed) —apps/rocm/src/uninstall_gate.rs:436,:453; test atapps/rocm/src/main.rs:34824-34896- Fixed: the read-error test now drives
uninstall_with_pathsand asserts an error and a surviving planned file, so removing both?is caught. - Still holds: both
?read the same directory, so either one alone can be swallowed and the other still aborts the run — the test stays green. The test also returns early with only aneprintln!when the directory is readable (root), passing having checked nothing (:34834-34839). - Fix: a case where only one read fails (needs a seam), and fail rather than return when the precondition cannot be set up.
- Fixed: the read-error test now drives
-
Added tests that still cannot fail on the branch they cover (standing, narrowed) —
apps/rocm/src/uninstall.rs:84-90,apps/rocm/src/uninstall_gate.rs:869; tests inapps/rocm/src/main.rs- Now caught: recycled →
Stop(byevery_daemon_identity_reading_maps_to_exactly_one_gate_outcome), and droppinghelper_stoppedfrom the success condition (a_helper_only_stop_prints_a_success_line). - Still green: deleting the stderr warning loop (
uninstall.rs:84-86) and the successprintln!(:88-90) — nothing captures output; a wrong reason/remedy for a service this pass just stopped (uninstall_refuses_while_a_recorded_endpoint_still_accepts_connectionschecks ids only,main.rs:34085-34101); widening the auth-refusal check (uninstall_gate.rs:869) to>= 300— the only staged status is 401. uninstall_never_kills_a_daemon_pid_that_was_recycledstill does not assert!helper_stopped.- Fix: one command-level test capturing stdout/stderr; assert reason and remedy; stage a 404/500 endpoint expecting
ProceedUnidentified; add the!helper_stoppedassertion.
- Now caught: recycled →
-
README and
--helpmisstate what uninstall does (standing, in full) —README.md:818-823,apps/rocm/src/main.rs:606-610, plan warnings inbuild_uninstall_plan- The
rocmdkill is never disclosed before confirmation; the first mention isuninstall_gate.rs:371, after the prompt, and it runs even with no service records (:487). - "Leaves every file in place" (
README.md:821) is false after a mid-run abort: earlier stops already deleted endpoint key files and rewrote records (main.rs:15550-15561), as the abort text atuninstall_gate.rs:933-951admits. - AGENTS.md §5. Fix: plan warning when the helper is recorded running; mention it in README and
--help; say stopped servers stay stopped and lose their keys.
- The
-
Two user-observable behaviors have no scenario (standing, in full) —
tests/e2e-cucumber/features/install_lifecycle.feature- No scenario runs
--keep-binaries --keep-datawith a live server (the only keep step,lifecycle_steps.rs:1012-1023, does not pass--keep-binaries), and none plants a liverocmdbefore uninstall. lifecycle-10/-24/-25 stage a service record, not the helper. - The PR text gives no justification for the gap. AGENTS.md §3. Fix: add the scenarios, or justify the gap in the PR text.
- No scenario runs
-
The manual-testing refusal check cannot be followed as written (standing, in full) —
docs/manual-testing.md:383-397- Step 1 is a full
rocm uninstall --yes; step 2 then corruptsservices/bad.json, runs uninstall "again", and checks directories that are already gone. The doc never saysservicesis under the data directory. - Fix: start step 2 from a fresh install and give the full services path.
- Step 1 is a full
-
The core atomic writer's write/flush cleanup and collision loop are untested (standing, narrowed) —
crates/rocm-core/src/lib.rs:4669-4830- Now tested: publish-failure cleanup (
write_file_atomically_cleans_up_when_publishing_fails,lib.rs:5136-5155). - Still holds: three staging copies remain (core,
therock.rs:4637,sandbox.rs:653); the core copy has no hooks, so removing eitherremove_file(&tmp)after a failedwrite_all/sync_all, or breaking theAlreadyExistsloop, stays green. The other two copies test these through their hooks. - Fix: give the core helper the same
cfg(test)hooks and have therock and sandbox call it.
- Now tested: publish-failure cleanup (
-
Six fail-closed branches that no test exercises (standing, in full) —
apps/rocm/src/uninstall_gate.rs:592-610,:896,:270,:272;apps/rocm/src/main.rs:~18015;crates/rocm-core/src/lib.rs:4697-4699- Each stays green under its mutation:
host_port_resolvesforced false; the non-.jsonfilter dropped (every planted file is*.json); thestarts_with(&services_dir)arm forced false (no plan ever has an action inside the services dir — the arm may be unreachable, i.e. a dead-code question too);KillScope::Singleandforce: false(the stand-in is a childlesssleep); delete-then-rename in unixpublish_temp_file. - Fix: one test per branch that fails when it is mutated, or remove an unreachable arm.
- Each stays green under its mutation:
Non-blocking
-
Several comments added by this change contradict the code. In each case the code governs, because it is what runs and what the tests assert: (standing in part)
uninstall_gate.rs:537-539andmain.rs:34111say "nothing prunes them, and there is noservices remove". Both exist:ServicesCommand::Removeatmain.rs:998andPruneatmain.rs:1014. This comment is the stated reason for failing open on recycled ports (see the related item under Decisions for the author).uninstall_gate.rs:636says the tests "live at the bottom of this file". They are inmain.rs'smod tests, anduninstall_gate.rshas none.main.rs:34506-34512says the empty-listing warning "is pinned by nothing … emptying its body … would stay green". The same test asserts that warning atmain.rs:34537-34546.main.rs:34336andmain.rs:34343refer toprobe_host, which is nowprobe_hosts.- The
probe_hostsdoc block atuninstall_gate.rs:712-730hashosts_name_one_listenerinserted below it (line 731). Rustdoc therefore attaches the wildcard/IPv6 essay to the wrong function and leavesprobe_hostsundocumented. uninstall.rs:7-11still says "Mechanically relocated … with no behavior change". The file now owns the stop-then-remove orchestration and theuninstall_with_pathsseam.crates/rocm-core/src/lib.rs:4762says "Two limits" and then lists three.- Confidence 85 · mechanical · Fix: correct each comment to match the code.
-
The
ConfirmTheDaemonPidadvice says to "delete the stale runtime state file" without naming it (new) —uninstall_gate.rs:92-98,:174-182- That failure's
service_idisrocmd (pid N), so the abort shows no path. - The sibling
RepairTheDaemonStateadvice prints the path. Under AGENTS.md §3, advice has to be followable. - Confidence 80 · logic · Fix: include the state file path (
automation_state_path()) in that failure's reason or advice.
- That failure's
-
The advised serve command is checked as a string, never parsed (new) —
uninstall_gate.rs:936,main.rs:3816- AGENTS.md §3 requires advice that names a command to be backed by a test that the command exists and accepts those flags.
- The tests assert the substring only, and the
main.rs:3816rewording has no test. - I read the flags against clap and they are correct today.
- Confidence 70 · logic · Fix: add a clap
try_parse_fromtest for the advised line with placeholders filled in, or derive the text from one shared constant.
-
No Windows test exercises the gate's stop-pass logic (new) — the stop-pass tests in
main.rsanduninstall.rsare#[cfg(target_os = "linux")]or#[cfg(unix)]- On Windows only lifecycle-24 runs. It pins PID termination against a port-0 record, as its own comment says.
- Untested on Windows:
- the
running: falseguard, which the comment onuninstall_never_signals_a_daemon_pid_recorded_as_already_stoppedcalls "the only defence" on Windows; - the ordering in which a doomed run stops nothing;
- the post-stop port probe.
- the
- Confidence 70 · logic · Fix: un-gate the tests that need no stand-in (for example
stop_background_helper_withagainst arunning:falsestate file), use a PowerShellStart-Sleepchild wherelifecycle_steps.rsalready does, or state the gap in the PR text.
-
21 commits carry
Co-Authored-By: Claude …trailers (new) (for examplef323e7fand4fa47a8). AGENTS.md §11 says "avoid AI-generated boilerplate footers", and a squash-merge carries them into the landed commit body. Confidence 70 · mechanical · Fix: strip them when squashing. -
example.internalatmain.rs:35328matches the AGENTS.md §2 leak-scan pattern\.internal(new). It is a placeholder, but it will hit the scan on every push. Confidence 70 · mechanical · Fix: useexample.test.
Decisions for the author
- Cache-only runs report
uninstall completewhile managed servers keep serving — tradeoff- The ticket's expected behaviour includes "Never report
uninstall completewhile a rocm-cli-managed endpoint is still serving." --keep-binaries --keep-datadoes exactly that, deliberately: the tooling survives, so the server stays stoppable. The plan warning,--helpand the README all disclose it.- The alternative is to kill live GPU servers for a cache cleanup. Confirm that this departure from the ticket's wording is intended.
- The ticket's expected behaviour includes "Never report
- Already-stopped records answering with other models fail open silently — non-blocking-improvement
- The leniency rests on "there is no
services remove", and that is false. - Because
rocm services remove <id> --yesexists, a stricter gate could block and name that remedy instead of proceeding.
- The leniency rests on "there is no
- About 2,000 lines of
uninstall_gate.rstests sit inmain.rs'smod tests— non-blocking-improvement. AGENTS.md §6 points new subsystems at their own file, and these tests grow the god-file rather than the module they test. - The atomic-write consolidation rides along with the uninstall fix — tradeoff
ManagedServiceRecord::writegoing atomic does serve the gate, which refuses on unparseable manifests.- The
write_active_runtime_marker,therock.rsandsandbox.rspublish dedup is drive-by work under §11's one-logical-change rule. - The dedup also leaves two staging copies (
apps/rocmd/src/sandbox.rs:655-762androcm-core) that differ insync_all. That is the twin-implementation shape this repo's defects cluster in.
Positive signals
- The stop pass is injected into
stop_managed_services_then_remove, so the ordering tests assert on a planned file surviving an abort, not on a return value. - The plan warning and the stop decision both derive from
plan_removes_recovery_tooling, so the printed promise cannot disagree with the behaviour. This is the pattern AGENTS.md §3 prefers. - The gate reuses
stop_internal_managed_service(therocm services stoppath) rather than adding a second stop implementation. - The
ReplaceFileWpublish is now single-sourced inrocm-core, andwindows-syswas removed from the two app crates. EndpointIdentitykeeps "lists nothing" distinct from "serves other models", and an HTTP 401/403 counts as evidence of a live server rather than as a failed probe.
Deployment notes
- Older daemons make uninstall refuse: a
rocmdstarted by a pre-upgrade binary has nodaemon_start_ticks. While it is live,rocm uninstalltreats it as unverifiable and refuses to remove anything until the operator stops that pid by hand (daemon_identity_outcome,uninstall_gate.rs:199-211). - Windows daemon identity is now strict: the Windows
rocmdrecords now carry start-times. A pid that cannot be opened (for example an elevated daemon) reads asIndeterminate, so it blocks uninstall where it used to be treated as a match.
What this covered
Read:
- Every file in the change,
0a021785…e0ae7699(the merge-base withprw-basec1c2aeb3). - The surrounding callers and callees: the
rocm servicescommands,daemon_binary_path,collect_installed_binary_candidates, and theproc_lifecycleidentity helpers.
How it was split: four independent workers, each reading its group completely, plus my own pass:
- the gate and its production wiring;
- the added tests and the e2e feature and steps;
rocm-coreandrocmd;- agent-instruction adherence, history, docs, code comments and commit quality.
Prior changes: this pass was done only as the history diff against prw-base. I read no earlier PR discussions.
Clean merge: git merge-tree against prw-base reports no conflicts.
CI for e0ae7699: every reported check passed or was skipped. No self-hosted GPU lane appears in the rollup, and I did not establish which lane runs the Windows scenario lifecycle-24.
Did not run:
- No builds, tests or mutation checks:
df -h /showed 39G free, under the 40G floor. Every test verdict and finding was reached by reading and cross-checking the code. - The
cfg(windows)arms were read by eye only, since there is no Windows linker on this host. - The design questions (Step 7) were investigated by me, not by independent workers.
Ticket: read EAI-8014 (title, description, status) through the tracker, without its comments.
Not reconciled against the PR's review discussion. The round note, the new/standing tags and the standing items after the italic line in Blocking were added by the driver; the anchored checks were given the objection and are not part of what this review read blind.
…macOS-only start-time logic Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Pushed 7720885 and rewrote the PR description. Taken: (1) the read-error test now runs the real command with keep_binaries (no dev-binary override), so a regression cannot delete the shared build output; the services-dir abort still triggers. (2) macOS-only logic removed per AGENTS.md section 6: record_predates_start_ticks is now just is_none(), the macOS-only test and its commentary are gone, and the macOS framing in the gate's docs is trimmed. (3) Description corrected: uninstall_gate.rs location, daemon_start_ticks in rocmd/service.rs, the report's warnings and helper_stopped fields, Windows GetProcessTimes start-times, the keep-binaries/keep-data exemption, reworded plan warnings, write_active_runtime_marker, and an honest scenario list (lifecycle-10/24/25; the exemption and helper stop are unit-tested only, no scenario). Acer gate green. Not done: README/--help/plan helper-kill disclosure and the 'leaves every file' wording, the exemption/helper scenarios and manual-testing steps, the remaining mutation survivors, host-spelling failed-partner case, the terminate->Unverified 'kill that pid' arm, atomic-writer cleanup tests. |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 7720885
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 (round 13, full review of the whole change). This round files a change request again; it replaces the one at e0ae7699 as this account's whole position. Every blocking item is tagged new or standing. It also covers your note on 77208854 (I reacted to it rather than replying separately).
- Withdrawn after the anchored check: the read-error test can no longer reach the real binaries (the executed run uses
keep_binaries: true;force_dev_binariesis only in a plan that is never executed). The macOS predicate and test are gone too. The one leftover, a macOS line in a module doc, is now under Non-blocking. - Narrowed, still standing: the description is fixed except one unmentioned message change.
- Still standing in full (you listed these as not done, and the anchored checks agree): failed-partner remedy, the
Unverifiedarm, each discovery?alone, the remaining mutation survivors, README/--help, manual-testing steps, atomic-writer tests, fail-closed branches. The missing scenarios were re-found blind. - New this round: auth-refusal advice on a stopped record's old port, force-kill of pre-upgrade Windows service PIDs, the five-of-six advice test, the port-probe test that cannot fail, and further untested branches.
- Not a reason for this change request: CI. All reported checks at
77208854passed; two were skipped. - Verification this round was static. The review host's disk was too low to build, so no build, test or mutation was run. Every verdict below was reached by reading the code.
Review — needs work
Full review of the whole change.
Implements the expected behaviour of the named ticket EAI-8014: stop managed services before removal, abort non-zero and keep the tooling when a stop can't be confirmed. That key now resolves to ROCMAI-290, a Bug, In Progress.
Blocking
-
The rule for records stopped before this run rests on a false premise, and its auth-refusal advice blames the wrong cause (new) —
apps/rocm/src/uninstall_gate.rs:520-524,apps/rocm/src/uninstall_gate.rs:79-85,apps/rocm/src/uninstall_gate.rs:660-673,apps/rocm/src/main.rs:34111(test comment); the commands are atapps/rocm/src/main.rs:998,:1014,:4792
The design comment says stopped records live forever because "nothing prunes them, and there is noservices remove". At HEAD,ServicesCommand::Remove("Delete one local server record and its files") andServicesCommand::Pruneboth exist. The code governs here: those commands ship.
Now take a record already stopped before this run, whose old port now hosts an unrelated server that requires auth (another keyed vLLM, a reverse proxy). That server answers 401/403, which givesBlockAuthRefused, and the abort printsStopWhatHoldsThePort. That advice says "Every process recorded for {id} is gone, yet the endpoint still answers — the engine outlived its supervisor" and tells the operator to stop whatever holds the port. For a stranger, that cause is false, and the advice asks them to kill an unrelated service. It never mentionsrocm services remove <id> --yes, which by its definition deletes the record and so would clear the condition.
AGENTS.md §3 requires that remediation advice name a command that "actually clear[s] the condition that printed it". Every retry hits the same abort, so the comment's stated goal ("must not brick uninstall with no way out") is not met by what the advice offers.
Confidence 85 · logic · Fix: correct the premise in both comments. For already-stopped records that block on an auth refusal, word the reason as "an authenticated server answers on this record's old port". Offerrocm services remove <id> --yesas the way out when it isn't yours, and pin the new advice together with the resulting state. -
Uninstall now force-kills unverified PIDs from older Windows service records, which the daemon path refuses to do (new) —
apps/rocm/src/main.rs:15465-15500(terminate_recorded_service_pids),crates/rocm-core/src/proc_lifecycle.rs:175,apps/rocm/src/main.rs:16640-16654, contrastapps/rocm/src/uninstall_gate.rs:819(record_predates_start_ticks)
Atprw-base,process_start_ticksreturnsNoneon every non-Linux platform, so every Windows service record written before this upgrade carries no start-time.- Liveness refresh only checks
process_is_running. After a reboot that recycles the PID, the record stays live (starting/ready). stop_managed_services_withthen stops it withterminate_verified(.., KillScope::Tree, .., force=true).compare_start_ticksreturnsMatchesfor(None, _), so an unrelated process gets force-killed.
rocm services stopcould already do this, but only when a user ran it. This PR makesrocm uninstall --yesdo it automatically to every live-status record, so the weakness now has a much wider reach. The PR's own daemon path treats the same condition as unverifiable and aborts. The description's "every Windows managed-service stop can now returnRecycledorIndeterminate" holds only for records written after the upgrade.
Confidence 70 · architectural · Fix: in the uninstall stop pass, give a live record whose PIDs carry no start-time the daemon's treatment: abort, and adviserocm services stop/a manual check, rather than force-killing. - Liveness refresh only checks
-
every_failure_class_carries_its_own_recovery_adviceclaims every class but covers five of six (new) —apps/rocm/src/main.rs:33951-34010,apps/rocm/src/uninstall_gate.rs:92-97
The test's comment says "a sixth variant cannot compile without being given text … across all five at once".StopFailureRemedyhas six variants, andConfirmTheDaemonPidis missing from the report, the phrase list and the ordering check. The only other mention of it (main.rs:33737) asserts the enum value, never its advice. Blanking that advice or reordering its rank leaves the suite green.
The untested text is also wrong as rendered:idsalready holds the labelrocmd (pid 4321), so it prints "Check what pid rocmd (pid 4321) is". It also says "delete the stale runtime state file" without naming the path.
Confidence 85 · logic · Fix: add the sixth failure, assert its distinctive phrase and position, fix the wording and the "five"/"sixth" comments. -
uninstall_refuses_while_a_recorded_endpoint_still_accepts_connectionscan't fail on the claim its comment makes (standing; re-found blind this round) —apps/rocm/src/main.rs:34042-34100; samewait()pattern atapps/rocm/src/main.rs:33644
let _ = supervisor.wait();has no bound. If the stop never ran,wait()returns whensleep 60exits, and the assertion "the recorded process must really have been stopped, so only the port probe can catch the survivor" then passes anyway.
The test asserts onlyfailed == ["svc-orphaned-engine"]. That also holds when the stop simply fails, which yieldsStopTheServiceand "still "ready"", so it can't show the port probe produced the failure. The "still accepts connections after the stop" reason arm (uninstall_gate.rs:677) is asserted nowhere; only its sibling arm is.
Confidence 85 · logic · Fix: use the existingassert_stopped_within_grace, and assertremedy == StopWhatHoldsThePortandreason.contains("after the stop"). -
Several branches this change introduces are reached by no test (new; the stderr-warnings part is standing) —
apps/rocm/src/uninstall_gate.rs:307,apps/rocm/src/uninstall.rs:87,apps/rocm/src/uninstall_gate.rs:716-722,endpoint_refused_authorizationinapps/rocm/src/uninstall_gate.rsdaemon_pidguards. Neither thedaemon_pid == 0nor thedaemon_pid == std::process::id()guard instop_background_helper_withis exercised; only the!state.runningconjunct is. Dropping the own-PID check letsrocmforce-kill its own tree.- Warnings printed to stderr. The
eprintln!loop that printsstop_report.warningsbefore the gate is untested. Everyuninstall.rstest passeswarnings: Vec::new(), and this loop is the only user-visible disclosure of the fail-open paths. localhostmapping. The"localhost" => "127.0.0.1"canonicalization inhosts_name_one_listeneris untested.- 403 and the stored key. The 403 half of the 401/403 auth-refusal check, and passing the stored endpoint key to the identity probe, can each be broken alone with the suite still green.
Confidence 70 · logic · Fix: add a case for each, e.g.
runtime_state(true, 0)andruntime_state(true, std::process::id())expecting no failure andhelper_stopped == false, plus a report with a warning asserted on stderr. -
Two user-observable behaviours have no Gherkin scenario, and the gap isn't justified (standing; re-found blind this round) —
tests/e2e-cucumber/features/install_lifecycle.feature
AGENTS.md §3: a change that alters what the user observes "must be covered by a Gherkin scenario"; only "purely internal changes" are exempt, and they must say why. Two behaviours here are covered by unit tests only, as the PR text itself says:- The
--keep-binaries --keep-dataexemption: the plan says servers are left running, exit 0, and the server keeps serving. - Uninstall also stopping the background helper
rocmd.
Both are visible to the user, neither is internal, and no gated lane is named.
Confidence 85 · architectural · Fix: add scenarios for both, or state in the PR text why each cannot run and which lane would. - The
The items below are standing objections from earlier rounds that this round's blind review did not re-find, or re-found only in part. Each was then put to the reviewer, with your note, after its report was frozen. Those verdicts are anchored (the reviewer was told what to look for) and static (nothing was run). Only the part that still holds is listed.
-
The description leaves out one user-visible change (standing, narrowed) — PR description
- Fixed: file locations, the report's fields, Windows
GetProcessTimes, the keep-binaries/keep-data exemption, the plan warnings,write_active_runtime_marker, and the three-scenario test plan. - Still holds: the public-bind advice in
ensure_public_service_has_endpoint_key(apps/rocm/src/main.rs:3816) now readsrocm serve <model> --host {host} --port <port> --allow-public-bind, and the description does not mention it. - The Windows start-time claim is also affected by the second new blocking item above.
- Fix: mention the advice change; qualify the Windows claim to records written after the upgrade.
- Fixed: file locations, the report's fields, Windows
-
A stopped record that waits on a live partner gets the wrong remedy when that partner's stop fails (standing) —
apps/rocm/src/uninstall_gate.rs:533-534,:548-553,:684-692,:79-85- Deferred records are probed after the stop loop. The only skip is for a record in
failedunder its own id, and a failed partner is filed under the partner's id. - The deferred record then gets
BlockServingOurModel→StopWhatHoldsThePort("every process recorded … is gone"), beside the partner's correctStopTheServiceline, so the survivor is named twice. - Both deferral tests (
apps/rocm/src/main.rs:34777,:34905) use a stop that always succeeds. - Fix: skip, or re-attribute, a deferred record whose live partner on the same endpoint is in
failed.
- Deferred records are probed after the stop loop. The only skip is for a record in
-
A kill the identity check refuses still prints "kill that pid" (standing; reachable only in a re-read race) —
apps/rocm/src/uninstall_gate.rs:358-368,crates/rocm-core/src/proc_lifecycle.rs:297- Every non-
stopped()outcome ofterminate, includingUnverified, pushesStopTheDaemon. - The pre-check at
uninstall_gate.rs:340-353already mapsIndeterminatetoConfirmTheDaemonPid, so this needs a start-time that read once and then failed while the pid stayed alive. - The missing state-file path is part of the third new blocking item above.
- Fix: map
UnverifiedtoConfirmTheDaemonPidand keepStopTheDaemonforTimedOut.
- Every non-
-
Discovery fail-closed: each
?alone is still unpinned (standing) —apps/rocm/src/uninstall_gate.rs:868-869,:437; test atapps/rocm/src/main.rs:34831-34838- Both reads hit the same directory, and the first always aborts before the second, so removing either one alone stays green. No seam fails only one.
- When the directory stays readable (root), the test prints "SKIPPED…" and returns as a pass, which libtest hides.
- Fix: a seam to fail one read at a time, and fail rather than return when the precondition cannot be set up.
-
Added tests that still cannot fail on the branch they cover (standing, narrowed) —
apps/rocm/src/uninstall.rs:~90,apps/rocm/src/uninstall_gate.rs:~846,apps/rocm/src/main.rs:33794- Now caught by e2e: deleting the final "uninstall complete" line (
lifecycle_steps.rs:1279-1286). - Still green: deleting the gate's "stopped N managed service(s) before removal" line; widening the 401/403 check to
>= 300(the only status staged against the gate is 401,main.rs:34234). uninstall_never_kills_a_daemon_pid_that_was_recycledstill does not assert!helper_stopped.- Fix: capture stdout in one command-level test; stage a 404/500 expecting
ProceedUnidentified; add the!helper_stoppedassertion.
- Now caught by e2e: deleting the final "uninstall complete" line (
-
README and
--helpmisstate what uninstall does (standing; the blind review re-found the substance and rated it non-blocking, see below) —README.md:819-824, theUninstalldoc comment atapps/rocm/src/main.rs:~607- The
rocmdkill is not disclosed before confirmation. - Neither says that an aborted run leaves the services and helper it already stopped down, with their endpoint keys dropped; only the abort text does.
- AGENTS.md §5 requires these docs to change in the same change. Fix: add both to the README and the doc comment.
- The
-
The manual-testing refusal check cannot be followed as written (standing) —
docs/manual-testing.md:~381-397- Step 1 is a full
rocm uninstall --yes. The refusal check then corruptsservices/bad.json, runs uninstall "again", and expects directories that are already gone. No reinstall is described. - The services directory is
data_dir/services(crates/rocm-core/src/lib.rs:1984-1986), and the doc gives no path. - Fix: start the refusal check from a fresh install and give the full services path.
- Step 1 is a full
-
The core atomic writer's write/flush cleanup and collision loop are untested (standing) —
crates/rocm-core/src/lib.rs:4786-4835write_file_atomicallyhas no test hooks, so theremove_file(&tmp)after a failedwrite_all(:4818-4822) orsync_all(:4827-4831) cannot be provoked.- No test pre-creates the temp name, so the
AlreadyExistsretry loop (:4792-4808) and its exhaustedbail!are unexercised. therock.rsandsandbox.rskeep their own staging, so three copies remain.- Fix: give the core helper
cfg(test)hooks and have therock and sandbox call it.
-
Fail-closed branches that no test exercises (standing) —
apps/rocm/src/uninstall_gate.rs,apps/rocm/src/main.rs:18015,apps/rocm/src/uninstall_gate.rs:~259-262,crates/rocm-core/src/lib.rs:4697host_port_resolvesforced false: no unreachable-but-resolvable case asserts there is no warning.- The non-
.jsonfilter dropped: the only planted file issvc-corrupt.json. - The
starts_with(&services_dir)arm (main.rs:18015): the data root is caught by the next arm, so this one looks unreachable (dead code, or a test gap). KillScope::Singleorforce: false: the stand-ins are childlesssleeps.- Delete-then-rename in unix
publish_temp_file: the reader test holds an fd, so it passes either way. - Fix: one test per branch that fails when it is mutated, or remove the unreachable arm.
Non-blocking
-
Comments made stale or misplaced by this change —
apps/rocm/src/uninstall_gate.rs:697-724,apps/rocm/src/uninstall_gate.rs:620,apps/rocm/src/main.rs:35227,apps/rocm/src/uninstall.rs:7,docs/architecture.md:29,crates/rocm-core/src/lib.rs:4762- The
probe_hostsdoc block sits onhosts_name_one_listener, andprobe_hostshas none. - "tests live at the bottom of this file":
uninstall_gate.rshas no test module, and the tests are inmain.rs. - The test comment says
process_start_ticksis alwaysNoneon Windows, which this PR makes false. uninstall.rsstill says "Mechanically relocated … with no behavior change", andarchitecture.mdstill lists it under "no owned types", though it now holds the stop-then-remove wiring.- "Two limits worth knowing" heads three bullets.
Confidence 85 · mechanical · Fix: correct each one to match the code.
- The
-
A module doc still frames macOS behaviour as a design point (standing, narrowed; the rest of the macOS objection is fixed) —
crates/rocm-core/src/proc_lifecycle.rs:16-31
It says "on other platforms (macOS)" the check "degrades to best-effort" and that this is "deliberate". AGENTS.md §6 treats platforms outside Linux and Windows as unsupported. Only a doc comment remains, so this no longer gates. Fix: say that other platforms are unsupported. -
Two remedies share an
advice_rank, which can break the dedup —apps/rocm/src/uninstall_gate.rs:129-130,:942-943
StopTheDaemonandConfirmTheDaemonPidboth rank 2.sort_unstable_by_keyfollowed bydedup(which only removes adjacent duplicates) can then print a sentence twice, in a non-deterministic order. Today each run records only one helper failure, so this is latent.
Confidence 70 · mechanical · Fix: give every variant a unique rank. -
a_cache_only_uninstall_leaves_a_live_server_runningpasses ifuninstall_with_pathsdoes nothing —apps/rocm/src/uninstall.rs
It catches the predicate being forced true, which is its stated purpose. It never asserts thatcache/blobwas removed, so it doesn't show that "left the server alone" happened alongside the removal it was meant to accompany.
Confidence 70 · logic · Fix: assert!paths.cache_dir.join("blob").exists(). -
The identity probe throws away the status that
http_get_with_authnow returns —crates/rocm-core/src/lib.rs:954-1018
managed_service_endpoint_identitygoes through the text helper, so a 401 becomesErr, and the gate needs a second request (endpoint_refused_authorization) to recover it. The expected-model derivation is also duplicated betweenmanaged_service_endpoint_model_readyandmanaged_service_endpoint_identity, and their empty-URL handling has already diverged (Ok(false)vsbail!).
Confidence 70 · architectural · Fix: build the identity onhttp_get_with_auth, add a refused variant, and extract a sharedexpected_model(record). -
README and
--helpleave out two things the command does —README.md:819-824, theUninstalldoc comment atapps/rocm/src/main.rs:~607
Both mention only stopping managed model servers. They omit that uninstall also force-stopsrocmd, and that an aborted run leaves the services and helper it already stopped down, with their endpoint keys dropped. Only the abort text says so (AGENTS.md §5).
Confidence 70 · mechanical · Fix: add both to the README and the doc comment. -
The new module doesn't follow repo conventions —
apps/rocm/src/uninstall_gate.rs:18,apps/rocm/src/uninstall.rs:66-99
use crate::*;is the only glob import of the crate root inapps/rocm/src. The completion and abort lines are hand-rolledprintln!rather than the sharedcli_report::ActionReport(AGENTS.md §6).
Confidence 70 · mechanical · Fix: use explicit imports, and route the completion lines throughActionReport. -
Tests skip silently and leak children —
apps/rocm/src/main.rs(an_ipv6_only_wildcard_engine…,the_plan_read_error_warning…)
These two announce "SKIPPED" only oneprintln!, which the harness hides for passing tests. The second also skips as root and restores permissions only on success.
Most of the new process-spawning tests hold a bareChild(asleep 60) rather thanKillOnDrop, so a panic leaks it.
Confidence 70 · mechanical · Fix: make skips visible, e.g. with#[ignore]plus a reason, or by failing loudly; wrap spawned children inKillOnDrop.
Decisions for the author
- Any unparseable
services/*.jsonaborts uninstall — tradeoff
This includes a stale stopped record from an older schema, whichrocm services listskips silently. The fail-closed choice is safe and the abort names the file. On the other side, every such run needs a hand edit, and the docs never say where the services directory is. - Some already-stopped records proceed while their endpoint still answers — tradeoff
When the endpoint lists no models or doesn't identify itself, uninstall proceeds with only a stderr warning. That avoids bricking uninstall on a recycled port. Against it, the ticket's "Never reportuninstall completewhile a rocm-cli-managed endpoint is still serving": a wedged or still-loading engine of ours lands in exactly these arms. - About 1,950 lines of gate tests went into
main.rs'smod tests— non-blocking-improvement
uninstall_gate.rswas extracted as its own module but has no tests of its own, so the god-file grows. Moving them alongside the gate would keep each arm's tests next to the arm, and make the "tests live in this file" comment true.
Positive signals
StoppedRecordVerdict::blocksandStopFailureRemedy::advice/advice_rankare exhaustive matches, so a new variant can't default to the side that removes the tooling.- Passing the stop into
stop_managed_services_then_removeas a parameter makes the stop-before-remove ordering assertable against a real file surviving. - The non-destructive checks (scanning unparseable manifests, probing already-stopped records) run before anything is killed, so a run that is going to abort stops nothing.
plan_removes_recovery_toolingdrives both the plan warning and the skip of the stop pass, so the message can't disagree with what the code does (the AGENTS.md §3 pattern).- The
/proc/<pid>/statparse usesrfind(')'), so acommwith spaces or parentheses is safe. The WindowsGetProcessTimespath opens withPROCESS_QUERY_LIMITED_INFORMATIONand closes the handle on every path. daemon_start_ticksis#[serde(default, skip_serializing_if = …)], so old and new state files read both ways.
Deployment notes
- A
rocmdstarted before the upgrade has nodaemon_start_ticks. The firstrocm uninstallafter upgrading aborts until it is stopped by hand. - Service manifests, the automation runtime state and the active-runtime marker are now published by rename:
- The new file gets umask permissions rather than the old file's mode.
- A symlink at the path is replaced, not written through.
- A killed writer can leave
*.tmp-…siblings behind. The services loader skips them by extension.
windows-sysmoves fromapps/rocmandapps/rocmdintorocm-core, withWin32_Storage_FileSystemadded.
What this covered
I read every file in prw-base...772088549674293971bd0fe123db841e49e65531 completely, across four coupled groups:
- The uninstall gate and its wiring, plus the doc diffs.
rocm-core/rocmdstart-time capture, atomic writes and Cargo manifests.- The new unit tests in
main.rsanduninstall.rs, read against the production code they target. - The e2e scenarios and step definitions, with an AGENTS.md adherence check, a leak scan and a history pass.
I also read the callers of the changed symbols (stop_internal_managed_service, terminate_recorded_service_pids, liveness refresh, ServicesCommand). The design questions were investigated in the driver context, not as separate workers.
Verification was static only because the shared disk is nearly full: no builds, tests or mutation runs. The test verdicts above are reasoned from the code, and no score is 100. CI on the reviewed head shows all 25 checks green; Skill checks (skillscope) and Regenerate manifests were skipped. No check in the rollup names a Windows lifecycle or GPU lane, so I cannot tell whether lifecycle-24 ran.
Did not run or degraded:
- Prior-changes pass: earlier review comments on these files were deliberately not read.
- Merge commits: the six
Merge origin/mainmerges were not checked for bad conflict resolution (no--remerge-diffwas run). - Commit signatures and DCO: not checkable locally; CI's
Commit signatures + sign-offpassed. - Worker discipline: one worker briefly wrote and then deleted a scratch diff file under
/tmp, despite the read-only instruction. Nothing in the checkout was modified.
Not reconciled against any PR discussion here. No prompt-injection content was found. The round note, the new/standing tags, the standing items after the italic line in Blocking and the narrowed macOS item under Non-blocking were added by the driver; the anchored checks were given the objection and are not part of what this review read blind.
…on't name a failed partner's survivor twice, pin the helper advice and pid guards Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Pushed (see head). Taken: (1) an already-stopped record whose old port answers with an auth refusal now has its own remedy, RemoveTheStoppedRecord: reason 'an authenticated server answers on host:port, this record's old port', advice offers 'rocm services remove --yes' if it isn't yours (or stop it if it is); no more 'the engine outlived its supervisor'. (2) failed-partner: a stopped twin is no longer judged when its live partner on the same endpoint already failed, so one survivor is not named twice; test added. (3) every_failure_class test now covers all variants incl. ConfirmTheDaemonPid and the new remedy; the daemon label now names the state-file path and the advice wording is fixed. (4) tests for the daemon_pid==0 / own-pid guards and the localhost mapping. Not done: force-kill of pre-upgrade Windows service PIDs (needs a design call: aborting on records with no start-time would also abort every legacy Linux record), 403 / stored-key / stderr-warning tests, the unbounded wait() in the orphaned-endpoint test, the keep-binaries/helper scenarios, README/--help helper-kill disclosure, and the description's public-bind advice line. |
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · cbb5d1f
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 (round 14, full review of the whole change). This round files a change request again. It replaces the one at 77208854 as this account's whole position. Every blocking item is tagged new or standing. It also covers your note on 77208854 (I reacted to it rather than replying separately).
- Dropped as resolved: the failed-partner remedy. The new skip uses the same partner list as the deferral, and
a_failed_live_partner_is_not_named_twice_through_its_stopped_twinfails without it. - Narrowed: the auth-refusal advice is fixed, and so are the daemon-pid guards, the localhost mapping, and the
every_failure_classphrases and wording. Only the parts listed below remain. - Moved to non-blocking: what is left of the
every_failure_classordering check, and of the discovery?objection. What remains there changes only advice order, or is redundant with the other read. Neither leaves anything wrong behind. - Your design question on the Windows force-kill is answered in that item below. In short: Linux records have carried a start time since #117. A predicate like the daemon's, or a Windows-only scope, refuses only tick-less records.
- Two blind findings do not gate this change request.
- The duplicated advice line: it repeats a paragraph and leads to no wrong action.
- The description gap: the squash body comes from commit messages, so it has no consequence after merge.
- Both are still worth fixing.
- CI: no failures at
cbb5d1fb. Skillscope and manifests were skipped. The self-hosted Strix Halo lanes (Windows, Ubuntu release/nightly) were cancelled, so lifecycle-24 has not run on Strix Windows hardware for this head. - Inline comments: none this round. Every finding is below.
Review — needs work
Full review of the whole change.
The PR names EAI-8014, which is now ROCMAI-290 ("rocm uninstall reports success while leaving a publicly-bound GPU-holding server running…"). The change implements that ticket's expected behaviour, with one deliberate exception: a --keep-binaries --keep-data run leaves running servers alone and still prints uninstall complete (see Decisions).
Blocking
-
The same advice is printed twice when two failure classes share a rank and arrive interleaved (new; not gating this change request, see round note) —
apps/rocm/src/uninstall_gate.rs:985-986classes.sort_unstable_by_key(|r| r.advice_rank()); classes.dedup();only removes adjacent duplicates.StopWhatHoldsThePortandRemoveTheStoppedRecordboth rank 1, so failures in the order[Hold, Remove, Hold]stay in that order after the sort, anddedupdrops nothing.- This order can happen in one run: a single
probe_records_for_survivorscall (:499) walks several already-stopped records and can give each one either remedy (:704,:735). - I reproduced it with a test added locally next to
two_failures_of_one_class_get_that_classes_advice_once_and_name_both_ids. The abort message contains "Every process recorded for svc-a, svc-c is gone … Find what holds that port …" twice. That contradicts the contract the existing test's name states ("one class, one piece of advice"). The existing test uses a single class, so it cannot see this.
Confidence 100 · logic · Fix: give the sort a total order, e.g.
sort_unstable_by_key(|r| (r.advice_rank(), *r as u8)), or dedup by membership. Add the interleaved case as a test. -
The description leaves out a remediation-message change in an unrelated command (standing, re-found blind; not gating, see round note) —
apps/rocm/src/main.rs:3816ensure_public_service_has_endpoint_keyguardsrocm services restart. Its advice now readsrocm serve <model> --host {host} --port <port> --allow-public-bind.- Nothing in the PR body mentions this. "The uninstall plan warnings are reworded" covers a different string.
- Changes nobody mentions are changes nobody reviews, and this one has a twin it did not fix (see Non-blocking).
Confidence 85 · mechanical · Fix: mention it in the description, or move it to its own change together with the rocmd twin.
The items below are standing objections from earlier rounds that this round's blind review did not re-find, or re-found only in part. Each was then put to the reviewer, with your note where one applied, after its report was frozen. Those verdicts are anchored (the reviewer was told what to look for) and static unless stated. Only the part that still holds is listed. These are what this change request gates on.
-
Uninstall force-kills tick-less service PIDs that the daemon path refuses to kill (standing; the blind review re-found the facts and filed them as a tradeoff, see Decisions; the anchored check says it is a defect) —
apps/rocm/src/main.rs(terminate_recorded_service_pids),crates/rocm-core/src/proc_lifecycle.rs(compare_start_ticks), contrastapps/rocm/src/uninstall_gate.rs(record_predates_start_ticks)- Every Windows record written before this upgrade has no start time.
(None, _)maps toMatches, so after a reboot that recycles the PID,rocm uninstall --yesforce-kills an unrelated process tree. rocm services stopacts only on a record the user names. Uninstall now sweeps every live record automatically, so this PR widens the reach.- On your design question: at the base,
process_start_ticksalready returned a value on Linux (proc_lifecycle.rs:305-310), and Linux records have storedsupervisor_start_tickssince #117 (2026-07-17). The daemon's predicate (refuse "no recorded ticks" only where the platform can read a start time) refuses Windows legacy records and pre-July Linux records, and nothing else. A Windows-only scope avoids Linux entirely. - Fix: give live tick-less records the daemon's treatment: abort, and advise
rocm services stopor a manual check.
- Every Windows record written before this upgrade has no start time.
-
No test shows that the new
rocm services remove <id> --yesadvice clears the block (standing, narrowed; the advice itself is fixed) —apps/rocm/src/main.rs:34020- The test only checks that the message contains the command. AGENTS.md §3 requires the covering test to assert the resulting state too: remove the record, re-run the gate, show it proceeds.
- The false premise "there is no
services remove" also survives in a test comment atmain.rs:34129. The other copy is under Non-blocking below.
-
uninstall_refuses_while_a_recorded_endpoint_still_accepts_connectionscannot tell a port-probe catch from a failed stop (standing, narrowed) —apps/rocm/src/main.rs:34098- A blind mutation shows the probe branch is pinned: removing it fails the test.
- Still holds:
failed == ["svc-orphaned-engine"]also holds if the stop itself fails (StopTheService).let _ = supervisor.wait();is unbounded, so the "really stopped" assertion after it is vacuous.- The "after the stop" reason arm is asserted nowhere; the only match is a negative
!containsatmain.rs:34337.
- Fix:
assert_stopped_within_grace,remedy == StopWhatHoldsThePort, andreason.contains("after the stop").
-
Two introduced branches remain untested (standing, narrowed; the daemon-pid guards and localhost mapping are now covered) —
apps/rocm/src/uninstall_gate.rs:889,:648/:657,apps/rocm/src/uninstall.rs(warnings loop)- 403: the only refusal fixture answers 401. Drop
|| parts.status == 403and a 403 server on a stopped record's port is no longer blocked. Uninstall then proceeds with only a warning, removing the tooling while that server keeps serving. - Stored key: stop passing
endpoint_api_keyand our own key-protected survivor gets the stale-record advice instead. - Warnings on stderr: every
uninstall.rstest passeswarnings: Vec::new(). Break theeprintln!loop and every fail-open path goes silent. - Fix: stage a 403 and a keyed survivor; assert one warning reaches stderr.
- 403: the only refusal fixture answers 401. Drop
-
Two user-observable behaviours have no Gherkin scenario, and the gap isn't justified (standing) —
tests/e2e-cucumber/features/- No scenario covers the
--keep-binaries --keep-datarun with a live server, or uninstall stoppingrocmd. - The PR body admits both are "covered by unit tests only" but gives no reason and names no lane, which AGENTS.md §3 requires.
- Fix: add the scenarios, or state why each can't run and which lane would.
- No scenario covers the
-
A daemon kill the identity check refuses still advises "kill that pid" (standing) —
apps/rocm/src/uninstall_gate.rs:377-387- Every
!outcome.stopped()result,Unverifiedincluded, maps toStopTheDaemon. When the start time reads in the pre-check and then fails during the stop while the pid stays alive, the user is told tokill/Stop-Processa pid the gate just declined to signal. - The window is narrow and untested.
- Fix: map
UnverifiedtoConfirmTheDaemonPid.
- Every
-
Added tests that still cannot fail on the branch they cover (standing)
- Deleting the
println!of the "stopped N managed service(s) before removal" line inuninstall.rs: no test captures stdout. - Widening the 401/403 check to
>= 300: a 404 or 500 from an unrelated server on an old port would then falsely abort. Only a 401 is staged (main.rs:34252). uninstall_never_kills_a_daemon_pid_that_was_recyclednever asserts!helper_stopped. A wrong flag would make the abort text claimrocmd"was stopped".- Fix: capture stdout in one command-level test; stage a 404/500 expecting
ProceedUnidentified; add the assertion.
- Deleting the
-
README and
--helpmisstate what uninstall does (standing) —README.md:819-824,apps/rocm/src/main.rs:605-610- Neither mentions that uninstall stops the background helper
rocmd. - Neither says that an aborted run leaves already-stopped servers and the helper down, with their endpoint keys dropped.
- AGENTS.md §5. Fix: add both.
- Neither mentions that uninstall stops the background helper
-
The manual-testing refusal check cannot be followed as written (standing) —
docs/manual-testing.md§8- It runs a full
rocm uninstall --yesfirst, then expects the directories that removed to still exist after a second run, with no reinstall in between. - It gives no full services path (the data dir followed by
/services). - Fix: start from a fresh install and give the full path.
- It runs a full
-
The core atomic writer's failure branches are untested, and three staging copies remain (standing) —
crates/rocm-core/src/lib.rs:~4786-4835,apps/rocm/src/therock.rs:4639-4691,apps/rocmd/src/sandbox.rs:655-703- Untested: the temp cleanup after a failed
write_all/sync_all, theAlreadyExistsretry, and the exhaustedbail!. - Only the publish step was single-sourced.
- Fix:
cfg(test)hooks on the core helper; have therock and sandbox call it.
- Untested: the temp cleanup after a failed
-
Fail-closed branches no test exercises (standing, narrowed; the unreachable
starts_witharm is under Non-blocking)- Non-
.jsonfilter: drop it and any non-JSON file underservices/becomes a false "unreadable manifest" abort. - Publish order: delete-then-rename in unix
publish_temp_filepasses the reader test either way (not re-verified this round). That would reopen the window in which a reader finds no record. - Kill scope:
KillScope::Tree/forcefor the daemon stop are not pinned, because the stand-ins are childless. - Name resolution:
host_port_resolvesis not pinned against a resolvable-but-dead endpoint. - Fix: one test per branch that fails when mutated.
- Non-
Non-blocking
-
The twin of the corrected restart advice still names a command that cannot run —
apps/rocmd/src/common.rs:353-356- The rocmd copy still prints
`rocm serve --host {host} --allow-public-bind`, which omits the required<model>positional. - Its doc comment says it "Mirrors the guard of the same name in
rocm", and this PR fixed only therocmcopy. - The
--require-api-keysentence at:350-351has the same gap. That one is pre-existing and not caused by this change.
Confidence 85 · mechanical · Fix: apply the same wording to the rocmd copy.
- The rocmd copy still prints
-
Comments that the change made stale or that contradict the code — five places:
apps/rocm/src/uninstall_gate.rs:554-555says "there is noservices remove".ServicesCommand::Removeexists (main.rs:998), andRemoveTheStoppedRecordadvisesrocm services remove <id> --yes.apps/rocm/src/uninstall_gate.rs:663-666says "The tests live at the bottom of this file", and so does theStoppedRecordVerdictdoc. The file has no#[cfg(test)]; the tests are inmain.rs.apps/rocm/src/uninstall_gate.rs:~740-756: the doc paragraph forprobe_hostsis attached tohosts_name_one_listener, andprobe_hostshas none.crates/rocm-core/src/proc_lifecycle.rs:48-51saysstart_ticks"isNoneon platforms without/proc", andapps/rocm/src/main.rs:35314says "alwaysNonethere [Windows]". Windows now reads start times throughGetProcessTimes.crates/rocm-core/src/lib.rs:4762introduces "Two limits worth knowing" and then lists three.
In each case the code governs: it is what runs. Confidence 90 · mechanical · Fix: correct the comments.
-
New tests spawn
sleepthroughPATHwithout the env lock, socargo testis flaky locally- Sites:
apps/rocm/src/uninstall.rs:425, plus about 17 newCommand::new("sleep")sites inmain.rstests (prw-base has 4). therock.rs:7012setsPATHto a scratchbin/underPROCESS_ENV_TEST_LOCK, which these tests do not take.- Observed in one of my full unmutated
cargo test -p rocm --bin rocmruns:a_live_managed_server_is_stopped_before_the_planned_paths_are_removedpanicked with "failed to spawn the managed server: … NotFound". - CI on Linux does not show this. AGENTS.md §8 makes
cargo test --workspacethe local gate, and that gate is where it fails. The pattern is pre-existing, but this change multiplies it about fivefold.
Confidence 85 · mechanical · Fix: hold the env lock in these tests, or spawn
sleepby absolute path. - Sites:
-
The clause
entry.path.starts_with(&services_dir)is unpinned —apps/rocm/src/main.rs:18015- Replacing it with
falseleft the wholerocmbin suite green (1068 passed). - Plan entries are only binaries plus the config, data and cache dirs, so the clause looks unreachable.
Confidence 85 · mechanical · Fix: drop it, or add a test that reaches it.
- Replacing it with
-
About 2,000 lines of
uninstall_gate.rstests sit inmain.rs'smod tests—apps/rocm/src/main.rs:~33560-35600,~36000-36230- AGENTS.md §6 and the PR's own
docs/architecture.mdentry listuninstall_gate.rsas a full domain extraction, and the sibling modules (serve_cmd.rs,storage.rs,therock.rs,uninstall.rs) keep their tests in-file. - Nothing technical forces the placement:
test_pathsandScopedTestEnvare already imported fromcrate::testsby other modules.
Confidence 70 · architectural · Fix: move them into
uninstall_gate.rs(and the command-level ones intouninstall.rs). - AGENTS.md §6 and the PR's own
-
The
uninstall.rstests build scratch roots fromtemp_dir()plus pid and millis —apps/rocm/src/uninstall.rs:137,:310,:486- prw-base replaced this pattern with a shared unique-suffix helper (7f72556). The branch predates that commit.
- A test-root collision already exists here:
restart_refuses_a_public_service_without_a_key_before_stopping_itfailed in two of three local runs because two tests share the label"restart-public-no-key". That flake belongs to the stale base and goes away once the branch takes main.
Confidence 70 · mechanical · Fix: use the shared helper after updating the branch.
-
every_failure_class_carries_its_own_recovery_advice: the ordering check covers only the five original classes (standing, moved here from Blocking) —apps/rocm/src/main.rs:~33951ConfirmTheDaemonPidandRemoveTheStoppedRecordare not in the position chain.- Within a shared rank, the order can't be pinned at all.
- The comment says "three classes" above a list of five.
- Fix: add both to the chain; give each class a unique rank, which also fixes the duplicated-advice finding above.
-
The discovery fail-closed test passes silently as root (standing, moved here from Blocking) —
apps/rocm/src/main.rs:~34831- Dropping either
?alone is harmless, because the other read fails closed on the same directory and files. - What remains: when the directory stays readable (root), the test prints "SKIPPED" and passes.
- Fix: fail, or
#[ignore]with a reason, when the precondition can't be set.
- Dropping either
Decisions for the author
-
Tick-less service records are force-killed while a tick-less daemon is refused — tradeoff (the anchored check after the review rates this a defect; it gates as the first standing item under Blocking)
- The uninstall sweep calls
terminate_recorded_service_pids. A record with no start ticks reachescompare_start_ticks→Matches(best-effort) and is signalled. - The daemon in the same situation is refused, on the PR's own argument that killing an unrelated process cannot be undone.
- The asymmetry matters most on Windows: every record written before this change has
supervisor_start_ticks = None. A record still readingreadyafter a reboot whose pid was recycled would get a stranger terminated by uninstall, which is now a sweep over all records. - The other option is refusing tick-less live records with a remedy, at the cost of blocking upgraders. Lifecycle-24 relies on the best-effort kill.
- Confirm the asymmetry is deliberate, and document it.
- The uninstall sweep calls
-
--keep-binaries --keep-dataprintsuninstall completewhile servers keep running — tradeoff- ROCMAI-290 says "Never report
uninstall completewhile a rocm-cli-managed endpoint is still serving." - The PR deliberately exempts tooling-preserving runs, and the plan warning,
--helpand README all say so. The ticket's wording does not.
- ROCMAI-290 says "Never report
-
The upgrade path aborts on a running old
rocmd, with weak advice — non-blocking-improvement- A pre-upgrade
rocmdrecords nodaemon_start_ticks, so everyrocm uninstallaborts withConfirmTheDaemonPiduntil it is killed by hand. - That advice ("if it is
rocmd, stop it") gives no command and does not mention the upgrade case, which will be the common one.StopTheDaemondoes givekill/Stop-Process. Norocmsubcommand stops the daemon. - Its other branch, deleting the state file, makes uninstall treat the helper as absent. That fails open if the pid really was
rocmd.
- A pre-upgrade
-
The PR carries adjacent refactors — non-blocking-improvement
- The publish dedup in
apps/rocmd/src/sandbox.rsandapps/rocm/src/therock.rs(including the WindowsReplaceFileWmove and thewindows-sysmanifest changes) and thewrite_active_runtime_markerrewrite are not needed by the gate path. The description does mention them. - Splitting them out would keep the Windows-only, never-clippy-gated FFI move reviewable on its own (AGENTS.md §11, one logical change).
- The publish dedup in
Positive signals
stop_managed_services_then_removetakes the stop pass as a parameter. That makes the stop-before-remove ordering testable by asserting that planned files survive, rather than by inspecting the code.- The plan warning and the gate both branch on the same
plan_removes_recovery_toolingpredicate, so the promise printed before confirmation cannot disagree with what the run does. This is the pattern AGENTS.md §3 asks for. - A mutation that removed the survivor probe for records stopped in this pass made
uninstall_refuses_while_a_recorded_endpoint_still_accepts_connectionsfail. So that branch is genuinely pinned.
Deployment notes
- If an older
rocmdis still running (its state file has nodaemon_start_ticks),rocm uninstallaborts until thatrocmdis killed by hand. - Windows:
process_start_ticksnow returnsSome. A record written with ticks, whose process cannot be opened at stop time, now yieldsIndeterminateand is not stopped, where before it was terminated. This affectsrocm services stopas well as uninstall. rocm-coregains theWin32_Storage_FileSystemfeature ofwindows-sys;apps/rocmandapps/rocmddrop their directwindows-sysdependency.
What this covered
- What was read. Every file in the change, completely: the gate, the uninstall command, the
main.rsplan and test hunks, rocm-core, rocmd, therock, the e2e feature and steps, and the docs. Also their callers and callees, and the sibling stop pathstop_internal_managed_service/terminate_recorded_service_pids. - Commits. The diff is 0a02178…cbb5d1fb. prw-base (c1c2aeb) is 6 commits further on;
merge-treemerges cleanly. - What I ran (
cargo test -p rocm --bin rocm, shared target, fresh build):- the filtered uninstall tests: 40 passed;
- full unmutated suite twice: one different failure each time, both explained above;
- three mutations;
- one added probe test, the duplicate-advice reproduction.
- CI. All required checks pass. The self-hosted Strix Halo lanes (Ubuntu release and nightly, Windows) were cancelled together, mid-run, for this head. So lifecycle-24 has not run on Strix Windows hardware for this commit. Those lanes are not required.
- Fan-out.
- Two of the four code-pass workers spawned; the other two hit the concurrent-subagent cap.
- The tests worker was re-spawned once later and ran independently.
- The e2e and docs pass (feature file, step definitions, README and docs) was run by me, so it was not independent.
- Two design-question workers ran.
- Did not run.
- Prior-changes pass: earlier review discussion is out of bounds here.
- Windows
cfgarms: checked by reading only, not compiled. - The rocm-core worker's pass was brief, though it reported reading its whole group.
- Not reconciled against any discussion here.
The driver added these: the round note, the new/standing tags and gating notes, the standing items after the italic line in Blocking, the two standing items at the end of Non-blocking, and the parenthetical on the first Decisions item. The anchored checks were given the objection and the author's note; they are not part of what this review read blind.
…ass, Unverified daemon kill gets the confirm remedy, stale comments Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Fixed in 6555f0e: live tick-less service pids are now refused (new ConfirmTheServicePid remedy) instead of force-killed; advice classes have a total order so none prints twice (test added); an Unverified daemon kill maps to ConfirmTheDaemonPid; rocmd restart advice matches rocm; stale comments corrected. Not done this push: extra test coverage items, scenarios for keep-binaries/helper stop, README/--help, test relocation. |
…erver record Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · d1e2aae
This automation never files a GitHub approval, so no approving review will
appear here whatever the outcome — the merge decision stays with a human
reviewer.
Review — needs work
Full review of the whole change (prw-base...HEAD, merge-base 0a021785, 21 files, +5.5k lines, 50 first-parent commits).
The description says it stops managed servers before rocm uninstall removes anything, and adds start-time identity checks (Linux and Windows) and a shared atomic-write helper; it names no ticket other than the title's bare EAI-8014.
Blocking
Round history (added by the poster): this automation's previous round, at b062582d, filed five blocking findings. Each is marked here as new or standing. Two of them were re-found blind this round and are tagged where they appear below. The other three were not re-found blind. Each was then put to this round's reviewer after its report was frozen, together with the author's account of what changed. That check was anchored: the reviewer was told the objection, so it is evidence on the question asked and not a blind finding. All three still hold in part. They are appended below with the original sites. Each one says which parts still hold and which no longer do. Nothing was withdrawn: every one of the five still holds in at least one part.
-
[new] A live vLLM-managed server is refused by the new gate with advice that does not apply to it. —
apps/rocm/src/uninstall_gate.rs:481-511,crates/rocm-core/src/lib.rs:4320-4335,engines/vllm/src/state.rs:140-148,apps/rocm/src/main.rs:15470(the kill that used to run)
The gate refuses any live record whose pid has no recorded start time (record_predates_start_ticks(*ticks) && process_start_ticks(*pid).is_some()).refresh_from_engine_statetakesengine_pidfromserver_pidand its ticks only fromserver_start_ticks. The vLLM engine writesserver_pid(the Python child, distinct from the launcher recorded assupervisor_pid) but onlystart_ticks, neverserver_start_ticks. A running vLLM service therefore always reads as an unverifiable engine pid (only Lemonade writesserver_start_ticks). I ran the gate (stop_managed_services_with) on a record of that shape (launcher with ticks, a distinct live engine pid withengine_start_ticks: None) and it returnedConfirmTheServicePid: "no start time was recorded for live pid(s) [..] ... cannot be told apart from a reused pid". Every uninstall with a live vLLM server (the very scenario in the description) would abort with the "record was written before start times were recorded" advice, and a retry aborts the same way. Before this PR that pid took the best-effort stop. Every test fixture usesengine_pid == supervisor_pid(managed_record_for_pid,main.rs:33462), which the pid de-duplication atuninstall_gate.rs:488-489collapses, so none reaches this shape. The weakness (vLLM omitsserver_start_ticks) predates the PR; the PR enlarges its blast radius by making it a hard refusal.
Confidence 85 · architectural · Fix: have the vLLM state write also recordserver_start_ticksforserver_pid(or haverefresh_from_engine_statefall back tostart_tickswhenserver_pid == pid), and add a gate test with a distinct engine pid whose ticks come only from engine state. -
[new] The recovery advice for a tick-less live pid ends in the unsafe kill the gate refused to do. —
apps/rocm/src/uninstall_gate.rs:95-103,apps/rocm/src/main.rs:5198-5221
ConfirmTheServicePidsays: if the pid "is something else, delete the stale record withrocm services remove <id> --yes". The case only arises for a pid that is running.remove_managed_service_recordrefuses whenmanaged_service_record_is_in_use(a recorded pid is running) and answers "Try:rocm services stop <id> --yes". That stop runsterminate_recorded_service_pidson an identity with no start time, which takes the best-effortMatchesarm and force-kills the tree of the unrelated process. The text also says "Check what each pid is ({})" but fills the placeholder with service ids, not pids. The code governs: the removal guard is deliberate (its doc says so), so the advice is what must change.
Confidence 85 · logic · Fix: name an action that works for a live-pid record (delete the manifest file by path, asRepairTheRecorddoes), print the pids, and add a test that follows the named command against such a record. -
[new; also re-finds blind part of the standing liveness-assertion objection from
b062582d: the endpoint test and the never-kills tests] Added tests stay green with the production branch they name broken. Verdicts (reverted / single-branch mutation), run on this head withcargo test -p rocm --bin rocmand-p rocm-core --lib:- Zombie check in
identity_state_with_observed—crates/rocm-core/src/proc_lifecycle.rs:198. Deleting|| process_has_exited(id.pid)leaves all 598rocm-coretests and the 40uninstalltests green. The only test reaching theGonearm (an_exited_pid_reads_as_gone_whatever_start_time_is_observed,:490-508) reaps the child first, so the zombie clause is never reached; on Linux a zombie still has/proc/<pid>/stat, so without it a zombie reads asMatches(the verdict that permits a kill). Confidence 100. TerminationOutcome::Unverified->ConfirmTheDaemonPid—uninstall_gate.rs:393-397. Changing the condition tofalse &&leaves the fullrocmbin suite green (1070 tests);a_daemon_stop_that_is_not_confirmed_aborts_and_a_confirmed_one_is_recorded(main.rs:~36096) injects onlyTimedOutandGraceful. Added in a fix commit named after this branch. Confidence 100.ConfirmTheServicePidadvice text —uninstall_gate.rs:95-103. Replacing the whole arm withString::new()leaves everything green;every_failure_class_carries_its_own_recovery_advice(main.rs:~33960) claims "every class" but its fixture has 7 of 8 variants. Confidence 100.- "still accepts connections after the stop" vs "is recorded stopped" wording —
uninstall_gate.rs:781. Forcingstopped_by_this_passtofalseleaves everything green;uninstall_refuses_while_a_recorded_endpoint_still_accepts_connections(main.rs:34060) asserts only the failed id, and ends with an unboundedsupervisor.wait()before!process_is_running, the patternassert_stopped_within_grace's own doc calls "measuring nothing". Confidence 100. - "Never kills an unrelated pid" tests —
main.rs:33728,:33820,:35118,:35122. They assert withprocess_is_running, which is true for an unreaped killed child (kill(pid,0)succeeds on a zombie), so they cannot detect the kill their messages name. Inserting aterminate_verified(.., None ticks ..)of the daemon pid into theNothingOfOursarm (uninstall_gate.rs:379) leavesuninstall_never_kills_a_daemon_pid_that_was_recycledand the whole suite green (confirmed by run). The sibling test at~35290already usestry_waitand explains why. Confidence 100.
Fix: usetry_wait().is_none()for the liveness assertions, add the Linux unreaped-zombie test, add theUnverifiedcase and the missingConfirmTheServicePidfixture entry, and assert the reason text and bounded wait in the endpoint test.
100 · logic
- Zombie check in
-
[new] Windows
GetProcessTimescall will fail the now-required Windows clippy gate. —crates/rocm-core/src/proc_lifecycle.rs:431(GetProcessTimes(handle, &mut created, &mut exited, &mut kernel, &mut user))
The base moved after this branch: #488 (0d85577) runsclippy --workspace -D warningsin the requiredwindows-build-and-testjob, with pedantic enabled (Cargo.tomlworkspace.lints.clippy), and converted exactly this pattern to&raw mut(seecrates/rocm-core/src/lib.rs:1335on the base). This branch'scfg(windows)code has never been through Windows clippy (the description gates only Linux), and the unsafe block carries no// SAFETY:comment, unlikereplace_file_windowsbeside it. I could not run Windows clippy here (only a Linux target is installed), so this is argued from the base's lint config and its fix commit.
Confidence 80 · mechanical · Fix:&raw mutfor all four arguments, a SAFETY comment, and run Windows clippy on the rebased tree. -
[standing from
b062582d(its AGENTS.md section 3 part), re-found blind] User-observable uninstall behaviour with no Gherkin scenario (AGENTS.md section 3). —tests/e2e-cucumber/features/install_lifecycle.feature
Scenarios exist and exercise real behaviour: lifecycle-10 (Linux stop), lifecycle-24 (Windows stop), lifecycle-25 (unreadable record refuses and keeps everything). The description states that the--keep-binaries --keep-dataexemption and the helper stop are "covered by unit tests only"; section 3 says a unit test of the internal helper does not discharge this, and gives no justification for the gap. Also uncovered: refusal on an unverifiable (tick-less) record, refusal on an unreadableruntime-state.json, and the helper-stop line. All three scenarios are@lifecycle, which runs only whenchanges.heavyis set, so none is on the always-on mock lane; the base'sfeatures/uninstall.feature(added after this branch, mock lane, already passes--keep-binaries) is the natural home. Mutatingremoves_recovery_tooling(uninstall.rs:55-58) to always true is caught only by a unit test.
Confidence 85 · rule cited (AGENTS.md section 3) · Fix: add scenarios for the keep-binaries+keep-data exemption and the unreadable-state refusal on the mock lane, a tick-less-record scenario beside lifecycle-10, and name the@id:/lane for each in the PR text. -
[new] Comments and docs this PR made stale or wrong. (a wrong comment is a defect; the side that governs is the code in each case, per the reason given)
apps/rocm/src/uninstall_gate.rs:801-820: the 18-lineprobe_hostsdoc sits abovehosts_name_one_listener, so rustdoc attaches it there andprobe_hosts(:828) has none.uninstall_gate.rs:345-371: describes reading "our own PID" to tell the platform, and "one reading of the daemon's start-time"; the code at:372-377reads neither of those (record_predates_start_ticksjust testsis_none()).crates/rocm-core/src/lib.rs:2880-2891(newdaemon_start_ticksdoc) prescribes the same dropped approach. The code governs:record_predates_start_ticks(:920-923) says both supported platforms record a start time.uninstall_gate.rs:274("off Linux it is the only one that applies"),:354-367("/proc", "best-effortMatchesarm meant for Windows"),lib.rs:2880,proc_lifecycle.rs:50-51("ticks since boot" — on Windows it is aFILETIME),proc_lifecycle.rs:182,apps/rocm/src/uninstall.rs:394("Someonly on Linux"),main.rs:33559("None elsewhere"): all predate the Windows reader this PR adds. macOS reasoning in new comments (proc_lifecycle.rs:15-30,lib.rs:2890) designs for an unsupported platform (AGENTS.md section 6).apps/rocm/src/uninstall.rs:7-10("mechanically relocated ... no behavior change") and the matchingdocs/architecture.mdwording: this PR adds the stop-before-remove behaviour to that module.docs/manual-testing.md:394-397: the refusal procedure says to corrupt a services file and runrocm uninstall --yes"again", straight after step 1 succeeded; on Linux that run already removed the binary and the config/data directories, so it cannot be followed.install_lifecycle.feature:290-300: says the grandchild case is covered by "unit tests aroundstopped_record_verdict"; the stopped-this-pass block is a different function (uninstall_gate.rs:692-797) and its test (main.rs:~34058) is Linux-only, so no Windows lane covers the port check.main.rs:34128-34129and:34527-34533: test comments that contradict themselves and the assertion beneath them.
Confidence 85 · mechanical · Fix: reword each to what the code does; move the doc block ontoprobe_hosts; repair the manual procedure (reinstall, create theservicesdir).
-
[new] The PR does not merge cleanly and no check except the docs build has reported on this head. —
README.md,docs/architecture.md
gh pr viewreportsmergeable: CONFLICTING;git merge-tree prw-base HEADconflicts inREADME.md(the base added two symlink/uncheckable-folder paragraphs in the same spot; keep all three) anddocs/architecture.md(the base reflowed the module map into per-module list items, so theuninstall_gate.rsentry must be redone as one bullet). The status rollup holds only the Read the Docs status. No Linux or Windows build, clippy, coverage or e2e check has run, so the Windows code, the new e2e scenarios and the coverage floors are unverified by CI (I ran therocmbin tests, 1070 passed, and therocm-corelib tests, 598 passed, on Linux only).
Confidence 100 (observed) · mechanical · Fix: rebase onto the current base, resolve the two docs conflicts, then drive CI to green. -
[new] The description does not match the change in two places. —
apps/rocm/src/main.rs:~3816,apps/rocmd/src/common.rs:355
It does not mention that the public-bind respawn/restart advice was reworded in two places unrelated to uninstall (rocm serve <model> --host {host} --port <port> --allow-public-bind). Its test plan says each unit test "was verified against a faithful revert of the production change it names"; the mutations above show several are not (the exceptions it names are only a directory-hygiene check and a false-positive guard). Remedy: narrow the claim, or finish the tests; mention the advice change or split it out.
Confidence 85 · mechanical -
[standing from
b062582d; not re-found blind; settled by an anchored check this round: still holds for 3 of 7 sites] Several branches the PR adds are exercised by no test.- Original sites: daemon stop not confirmed; the
ProceedUnidentifiedwarning; warnings printed to stderr (apps/rocm/src/uninstall.rs); the 403 arm (status == 401 || 403); the API key passed to the identity probe (endpoint_api_key.as_deref());classes.dedup(); the "No files were removed." tail. - Still holds at
d1e2aaeb(each mutation applied alone; fullcargo test -p rocm --bin rocmstayed green): the stderr warning loop replaced by a no-op (uninstall.rs:87-88);status == 401 || status == 403narrowed to401(uninstall_gate.rs:950); bothendpoint_api_key.as_deref()arguments replaced byNone(uninstall_gate.rs~709 and ~718). Without the key a guarded endpoint is refused at identification rather than identified, and no test tells those apart. - No longer holds: the daemon-not-confirmed push, the
ProceedUnidentifiedwarning,dedup()and the tail are now each caught by a test. - Confidence 100 (ran) · mechanical · Fix: add a test for each remaining site.
- Original sites: daemon stop not confirmed; the
-
[standing from
b062582d; not re-found blind; settled by an anchored check this round: still holds for the daemon half] The new Windows start-time capture has no Windows test, and no Windows lane runs the daemon-stop path it enables.- No longer holds:
crates/rocm-core/src/proc_lifecycle.rsnow has two#[cfg(windows)]tests ofprocess_start_ticks. - Still holds:
apps/rocmd/src/service.rsa_fresh_runtime_state_records_this_daemons_start_timeis still#[cfg(target_os = "linux")], and every daemon-stop test inapps/rocm/src/main.rsisunixorlinux. Ifbuild_runtime_staterecordeddaemon_start_ticks: Noneon Windows, every uninstall that finds a liverocmdwould abort with the pre-upgrade advice and no Windows lane would catch it. No e2e scenario has a liverocmd. - Confidence 85 · logic · Fix: run that capture test on Windows too (its assertion is platform-neutral).
- No longer holds:
-
[standing from
b062582d; not re-found blind; settled by an anchored check this round: the ids-in-advice and always-warns parts still hold] Two test assertions cannot fail on the branch they name.- Still holds: replacing every
ids.join(", ")inStopFailureRemedy::advicewithString::new()leaves the suite green.every_failure_class_carries_its_own_recovery_adviceand the newtwo_failures_of_one_class_get_that_classes_advice_once_and_name_both_idsmatch ids against the whole abort message, whose opening detail already lists every id. - Still holds: forcing the unresolvable-host warning on for every closed port (
uninstall_gate.rs~677) leaves the suite green. No test assertswarnings.is_empty()for a resolvable host with nothing listening. - No longer holds: per-class grouping is now exercised; deleting
dedup()fails two tests. - Confidence 100 (ran) · logic · Fix: assert the ids inside the advice text (or test
advice(&ids)directly), and assertwarnings.is_empty()in a resolvable-but-refused case.
- Still holds: replacing every
The other two standing findings from b062582d were settled the same way. The liveness-assertion finding still holds for the endpoint test, the pre-start-ticks daemon test and the recycled-pid daemon test, all covered by the third blocking item above. It no longer holds for the trailing live.wait() in uninstall_skips_already_dead_managed_service, which runs after every assertion. The description finding: its five omissions and the scenario count are now fixed. Its AGENTS.md section 3 part still holds and is the fifth blocking item above.
Non-blocking
- [new] The tick-less check treats "could not read this pid's start time" like "pid is dead". —
uninstall_gate.rs:496-499; test commentmain.rs:35279-35286says the conflation is "prevented by construction". A live pid whose start time cannot be read (for examplehidepid, a transient read failure) skips the refusal and reaches the best-effort kill.process_is_running(*pid)is the question being asked. Confidence 75 · logic. - [standing]
identity_state_with_observedhas no remaining reason to exist, and its one caller reads the start time before the liveness check, the order its own doc (proc_lifecycle.rs:186-190) warns about. —uninstall_gate.rs:373-376,proc_lifecycle.rs:179-202, re-exportlib.rs:91.terminate_verifiedre-checks with the safe order before signalling, so no harm results;identity_stategives the same answer. Confidence 80 · architectural. - [new] A live wildcard-bound record plus a stopped record of the same model on a specific address is misjudged. —
uninstall_gate.rs:538-550,:820-826.probe_hostsfor0.0.0.0is loopback only, so no overlap is seen; the stopped record is probed first, the live sibling answers with the same model, and the run aborts with "engine outlived its supervisor". Confidence 70 · logic. - [new] Service records are loaded and liveness-refreshed (readiness plus inference probes, possible rewrite) twice in one stop pass, and the pid de-duplication loop is copied —
uninstall_gate.rs:476,:519,:480-493versusmain.rs:15468-15483. Confidence 80 · mechanical. - [new] The abort headline always says "could not stop managed service(s)", including when the only failures are an unreadable daemon state or a corrupt manifest.
uninstall_gate.rs:1067. Confidence 70 · mechanical. - [standing]
write_file_atomicallyis a third copy of the staging logic (crates/rocm-core/src/lib.rs:4672-4682,:4785-4836; others intherock.rs:4640-4775,sandbox.rs:656-756), and the core copy has no test seam, so its write-failure andsync_allcleanup branches are untested. Confidence 70 · architectural. - [new] Windows test gating disagrees between two siblings.
lib.rs:4941a_runtime_state_write_never_shows_a_reader_a_truncated_fileis ungated while its twin:4871-4874is#[cfg(unix)]on the stated ground that WindowsReplaceFileW"does not offer the same observation". One of the two is wrong; check the Windows job. Confidence 70 · logic. - [new] Weak or environment-dependent tests.
a_service_record_write_leaves_no_scratch_sibling(lib.rs:4989) passes with a plainfs::write(its own comment says so; the sibling at:5052covers the property).the_plan_read_error_warning_only_promises_a_refusal_where_the_stop_pass_runsreturns green when run as root and is the only test of that warning text (stage the failure with a directory namedx.jsoninstead).the_daemon_stop_skips_pid_zero_and_its_own_pid: the pid-0 half is vacuous (process_is_running(0)already returns false atuninstall_gate.rs:342). Several#[cfg(unix)]tests depend onprocess_start_ticksreturningSome, which only Linux does. Several tests leave a 60 ssleeprunning on panic (aKillOnDropexists).a_record_whose_host_no_longer_resolvesdoes a real DNS lookup. Confidence 70-85 · mechanical. - [new] New fixtures use
<label>-<pid>-<millis>roots (uninstall.rs:136-141,:311,:487,lib.rs:4850) instead of the base'stest_support::unique_suffix()(#603 showed that scheme collides within a process). Labels are distinct today. Confidence 70 · mechanical. - [standing in part: the tests-in-
main.rspoint] Structure. About 2,000 lines ofuninstall_gatetests sit inmain.rsmod testsanduninstall_gate.rs:18usesuse crate::*, whiledocs/architecture.mddescribes it as full domain extraction reached by qualified paths. Hand-rolledprintln!("stopping ...")for a bounded grace wait (uninstall_gate.rs:389,:578) is the long-quiet-wait case the sharedAnimatedSpinnerexists for (AGENTS.md section 6). The e2e step hardcodes the mock helper's file namee2e-mock.json(lifecycle_steps.rs:533) rather than adding the field toServiceRecordOptions. Confidence 70. - [new] User docs are incomplete. README (
:819-824), the--helptext anddocs/manual-testing.mddo not say uninstall also stopsrocmd, that a pre-upgrade record or state file causes a refusal rather than a stop, or that services stopped before an abort stay stopped and lose their endpoint keys. Confidence 70. - [new] Commit hygiene. 50 first-parent commits, 7 merges from main; commit
43fbb58carries about 250 lines of substantive fixes inside a merge commit; several fix-on-fix commits. Every commit is authored and committed as a personal address (fredespi <...@gmail.com>), five carry aSigned-off-byfor a different identity than the author, and AGENTS.md section 2 asks for the employer identity where required. Commit signature status cannot be read here. Suggest squashing into a few logical commits with one identity.
Decisions for the author
- One PR does four things — non-blocking-improvement. The stop gate, Windows start-time reads (a behaviour change for every Windows managed-service stop, per the description), the atomic-write consolidation across
rocm-core,therock.rsandrocmd, and unrelated serve-advice rewording total about 5.5k lines. Splitting out the Windows start-time work and the atomic-write refactor would make each reviewable and let the Windows clippy and test results land per change. - Fail-closed on a tick-less live record versus best-effort stop — tradeoff. Refusing protects an unrelated process after pid reuse across a reboot but turns every older-format or engine-without-ticks record into a manual step (see the vLLM finding); best-effort stopping keeps uninstall working but force-kills a tree on an unproven pid. The PR chose fail-closed; confirm that the manual step is acceptable once vLLM records carry ticks.
- Gate placed in a new
uninstall_gate.rswhile its tests live inmain.rs— tradeoff. Keeps the new module small against the repo's direction of shrinkingmain.rs, at the cost of the new module not owning its tests.
Positive signals
- The ordering tests assert that planned files still exist after an aborted run rather than checking a return value, and lifecycle-10 polls the planted child with
try_waitso a zombie cannot make it pass. - Start-time identity comparison is read only after the liveness check in
identity_stateand the new serde field isdefault/skip_serializing_if, so records stay compatible in both directions. - The gate probes both loopback families for wildcard binds and fails closed on an unparseable manifest or unreadable runtime state instead of skipping them.
Deployment notes
- Windows managed-service stops start verifying process creation time (
GetProcessTimes); a recorded pid that cannot be verified now yieldsRecycled/Indeterminatewhere it used to always match. rocmdruntime-state files gaindaemon_start_ticks; state files written by an olderrocmdmakerocm uninstallrefuse until the helper is stopped by hand.crates/rocm-corenow owns thewindows-sysWin32_Storage_FileSystemfeature;apps/rocmandapps/rocmddrop their ownwindows-sysdependency (Cargo.lockupdated).- Merging needs the base's
README.mdanddocs/architecture.mdconflicts resolved.
What this covered
Every file in the change read completely by five independent read-only workers (production gate; added tests; process-identity and daemon layer; e2e scenarios and docs; agent-instruction/history/prior-changes/comments passes), prw-base...HEAD against merge-base 0a021785… and head d1e2aaeb16c653b6cf2c7636d48b9804ead9e78c, plus callers and callees in crates/rocm-core, engines/vllm, engines/lemonade and apps/rocmd. Every blocking item above was re-checked by the driver against the source. Run on this head on Linux: cargo test -p rocm --bin rocm (1070 passed), cargo test -p rocm-core --lib (598 passed), the uninstall filter (40 passed), a throwaway gate test on the vLLM record shape (failed as described), and five single-branch mutations (zombie check, Unverified arm, ConfirmTheServicePid advice, stopped_by_this_pass wording, recycled-daemon kill), all of which left the suite green. Each mutated file was restored with git checkout after its run. Not run: Windows build or clippy (only a Linux target is installed here), workspace clippy, the e2e cucumber suite, scripts/smoke_local.py, and any GPU lane; Windows-only code (GetProcessTimes, ReplaceFileW, lifecycle-24) was reviewed by reading only. CI: only the Read the Docs status has reported on this head. The step 7 design questions were answered by the driver from the workers' evidence rather than by separate per-question workers; the history and prior-changes passes ran (the latter read earlier merged PRs' review comments, not this PR's). The leak scan over the diff is clean (hits are a reserved test hostname, an identifier, a "private key" release-signing line, a bare ticket id and the committers' sign-off lines). Not reconciled against any discussion here.
…ws clippy-safe GetProcessTimes, tests Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
|
Pushed 0ad0b6b (merged main, docs conflicts resolved). Fixed: vLLM engine ticks (state writes server_start_ticks; core falls back to start_ticks when server_pid==pid, test added); tick-less advice no longer points at services remove, now a manual kill/delete-file route; liveness check uses process_is_running; GetProcessTimes uses &raw mut + SAFETY; Unverified daemon arm and advice text now tested; daemon-ticks capture test runs on Windows too. Not done yet: Gherkin scenarios for keep-binaries/helper stop, the 403/stored-key/stderr tests, zombie test, comment cleanups, README/help text, commit squash. |
Signed-off-by: fredespi <fredrik.espinoza@gmail.com>
rominf
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 6c48991
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 (round 15, full review of the whole change). This round files a change request again. It replaces the one at cbb5d1fb as this account's whole position. Every blocking item is tagged new or standing.
- Your two notes (on
cbb5d1fb's report and on the other reviewer'sd1e2aaebreport) are acknowledged with a reaction. This report covers what they claim. - Dropped as resolved (checked against the code at
6c489913):- The force-kill of live tick-less service pids. The gate now refuses them before stopping anything.
- The
Unverifieddaemon advice, which now maps toConfirmTheDaemonPidand is tested. - The duplicated advice line. Every class now has its own rank.
- The rocmd twin of the restart advice, which now names
<model>.
- Not gating this change request:
- The last blind finding (description and comments describe a removed check). The squash body comes from commit messages, and the comments mislead no runtime behaviour.
- The
starts_witharm inside the untested-branches finding, which was non-blocking last round.
- No build or test was run this round. The review host is low on disk, so every test verdict below comes from reading the code. Nothing is scored 100.
- CI at
6c489913: every reported check passed or was skipped (skillscope and manifests skipped). - Inline comments: none. Every finding is below.
Review — needs work
Full review of the whole change.
Implements EAI-8014 (now ROCMAI-290): stop managed services, then remove files, and abort with recovery advice if a stop can't be confirmed. The ticket's expected behaviour is covered.
Blocking
-
Any service that rocmd has recovered makes every later uninstall refuse, and the refusal blames the wrong cause (new) —
apps/rocmd/src/service.rs:546-558,apps/rocmd/src/watchers.rs:1452-1464,apps/rocm/src/uninstall_gate.rs:471-511,apps/rocm/src/uninstall_gate.rs:919-924,tests/e2e-cucumber/tests/e2e/lifecycle_steps.rs:~530- The path: recovery re-runs
rocmd supervise.supervise_servicerebuilds the record withManagedServiceRecord::new(..., std::process::id(), ...), which leavessupervisor_start_ticks: None. Nothing inapps/rocmdsets that field; the only writers arerocm serve(main.rs:4161,main.rs:15720). - What the gate does with it: the pre-check sees a live supervisor pid with no recorded start time and aborts with
ConfirmTheServicePid. - What the user is told: "the pid may have been reused after a reboot", but the pid really is our live supervisor. The doc comment on
record_predates_start_ticks("an absent value means the record was written by a pre-upgraderocmd") is also false. - The advice may not clear it (argued, not run): the refusal comes before the helper is stopped, and the helper recovers managed services. So "kill the pid, re-run" can just produce another record without a start time.
- Why the scenario misses it: the e2e step writes the start time into the record itself, so the gap never shows.
- Why it is this PR's: before this change a missing start time only made a stop best-effort. Now it hard-blocks uninstall.
- Confidence 85 · logic · Fix: record
process_start_ticks(std::process::id())insupervise_service, and refresh it for the recovered child. Add a test that stages a record shaped the way recovery writes it.
- The path: recovery re-runs
-
uninstall_never_kills_a_daemon_pid_that_was_recycledcannot detect a kill (standing, re-found blind) —apps/rocm/src/main.rs:33921- It checks
rocm_core::process_is_running(pid)on its own child, which it never reaps. On Linux that iskill(pid, 0)(crates/rocm-core/src/lib.rs:1611-1623), which returns true for a zombie. So a stranger that gets killed still reads as running. - The test never asserts
report.helper_stopped, andreport.stoppedlists only services. A gate that killed on the bare pid would pass this test. - The PR description lists this test as "verified against a faithful revert".
- Confidence 85 · logic · Fix: assert
stranger.try_wait()returnsNone(asuninstall_never_signals_a_daemon_pid_recorded_as_already_stoppeddoes) and assert!report.helper_stopped.
- It checks
-
every_failure_class_carries_its_own_recovery_advicecannot fail on the ids it claims to check, and skips a class (new) —apps/rocm/src/main.rs:34078,apps/rocm/src/main.rs:36355,apps/rocm/src/uninstall_gate.rs:997-1003- The abort message always starts with
detail = "{service_id} ({reason})"for every failure. So "id-carrying remedies must name their ids" passes even if everyids.joinis removed fromadvice(). ConfirmTheServicePidis missing from the "every class" list.- Its own test,
the_tickless_pid_advice_names_ids…, asserts no id at all. - Confidence 80 · logic · Fix: assert the ids on the text after the detail part, and add
ConfirmTheServicePid.
- The abort message always starts with
-
uninstall_refuses_while_a_recorded_endpoint_still_accepts_connectionscannot tell a port-probe block from a failed stop (standing, re-found blind) —apps/rocm/src/main.rs(atfn uninstall_refuses_while_a_recorded_endpoint_still_accepts_connections)- It calls
supervisor.wait()with no time limit. If the stop failed, the wait lasts untilsleep 60exits by itself, and "the recorded process must really have been stopped" still passes. - A failed stop also yields
failed == ["svc-orphaned-engine"]. - The test never asserts
StopWhatHoldsThePortor the "after the stop" reason. The sharedassert_stopped_within_gracehelper was added in this PR for exactly this case. - Confidence 80 · logic · Fix: use
assert_stopped_within_grace, and assert the remedy and the reason.
- It calls
-
The new
engine_start_ticksfallback is only tested where it applies; the dangerous direction is untested (new) —crates/rocm-core/src/lib.rs:4357-4364,apps/rocm/src/main.rs:36373- The test is named "…distinct_engine_pid…", but its fixture has
pid == server_pid(4242 for both). - If the
== Some(u64::from(pid))guard were dropped, the test stays green. Then a launcher's start time would be adopted for a different engine pid, so a live engine reads as recycled and is treated as gone. - Confidence 80 · logic · Fix: add a fixture with
server_pid != pidand assertengine_start_ticksstaysNone. Rename or fix the existing test.
- The test is named "…distinct_engine_pid…", but its fixture has
-
Branches this change adds that no test reaches (standing in part, re-found blind: the stderr warnings and the 403 arm; the rest new; the
starts_witharm was non-blocking last round and does not gate on its own) —apps/rocm/src/uninstall.rs:~84-86,apps/rocm/src/uninstall_gate.rs:638,main.rs(plan_removes_recovery_tooling),endpoint_refused_authorization- The
eprintln!("warning: …")loop is the only thing that shows the user each fail-open. Deleting it keeps every test green; the tests checkreport.warningsas values, never the print. - The deferred-partner probe: all three tests that share a port stay green if the call is deleted.
- The
entry.path.starts_with(&services_dir)arm ofplan_removes_recovery_tooling. - The 403 response (only 401 is staged).
- Confidence 75 · logic · Fix: inject a writer, or return the warnings so they can be asserted, and stage each missing case.
- The
-
The plan promises a stop that the gate then refuses, and the printed stop claims are never checked against the actual outcome (new; the unchecked stop line is standing) —
apps/rocm/src/main.rs:~18000-18010(build_uninstall_plan),apps/rocm/src/uninstall_gate.rs:1083-1096,tests/e2e-cucumber/tests/e2e/lifecycle_steps.rs(then_removal_complete)- The plan counts records by status and prints "those servers will be stopped before removal" just before the confirm prompt. A live record with no start time, which includes every recovered service (first finding), is refused instead.
the_plan_warning_only_promises_a_stop…only stages records that have a start time.- The "stopped N managed service(s)" line is pinned only with fabricated reports. The e2e step checks just "uninstall complete". None of the string pins names the test that proves the behaviour, which AGENTS.md §3 requires.
- Confidence 75 · logic · Fix: compute the plan line from the same start-time check the gate uses. Assert the stop line in the lifecycle-10 and lifecycle-24 then-steps.
-
The PR description and code comments describe a platform check that was removed (new; not gating, see round note) — PR description ("
identity_state_with_observedso one reading of the PID answers both the 'could this platform verify?' and 'is this still the same process?' questions"),apps/rocm/src/uninstall_gate.rs:354-371,apps/rocm/src/uninstall_gate.rs:272-285,crates/rocm-core/src/lib.rs(daemon_start_ticksdoc)- The code now uses
record_predates_start_ticks(x) = x.is_none(). Nothing reads our own pid to answer the platform question. - The comments still explain that defence ("answered by a process that is definitely alive — this one", "the best-effort
Matchesarm meant for Windows", "(/proc)"). - They also say "Two residual gaps" but describe only one.
- The description's claim that the recycled-pid test was "verified against a faithful revert" is contradicted by the second finding.
- Confidence 85 · mechanical · Fix: correct the description and rewrite these comments to the current rule (no start time means refuse).
- The code now uses
The items below are standing objections from cbb5d1fb that this round's blind review did not re-find as blocking. After its report was frozen, each was put to the reviewer together with your note. Those verdicts are anchored: the reviewer was told what to look for. They are also static. Only the part that still holds is listed.
-
No test follows the gate's advice and shows the gate then proceeds (standing; anchored check: holds) —
apps/rocm/src/uninstall_gate.rs:109-115, tests atmain.rs:34808-34870,:34147RemoveTheStoppedRecordstill advisesrocm services remove <id> --yes. The tests that reach it stop at the abort.- No test removes the record, re-runs
stop_managed_services_before_uninstall, and shows that uninstall proceeds. The same gap applies to the delete-the-file routes. lifecycle-25 asserts only the refusal. - AGENTS.md §3: remediation advice naming a command must be shown to clear the condition.
-
The stored endpoint key passed to the identity probe is untested (standing; anchored check: holds) —
apps/rocm/src/uninstall_gate.rs:695,:708,:717- The only authorization test (
main.rs:34838) asserts that no key is stored. - If both
.as_deref()arguments becomeNone, our own keyed survivor getsRemoveTheStoppedRecord("not one of yours, delete the record") instead ofStopWhatHoldsThePort, and nothing fails.
- The only authorization test (
-
Only a 401 is staged, so widening the refusal check cannot fail (standing; anchored check: holds) —
uninstall_gate.rs:949-951,main.rs:34379- With
>= 300, a 404 or 500 from an unrelated server on an old port would falsely abort, and no test would fail.
- With
-
No Gherkin scenario covers the
--keep-binaries --keep-datarun with a live server, or uninstall stoppingrocmd(standing; the blind review raised it under Decisions; anchored check: holds) —tests/e2e-cucumber/features/install_lifecycle.feature- The feature adds only lifecycle-10, 24 and 25.
- The PR body says "unit tests only" but gives no reason and names no lane. AGENTS.md §3 requires one.
-
README and
--helpdo not say uninstall stopsrocmd, or what an aborted run leaves down (standing; the blind review raised it as non-blocking; anchored check: holds) —README.md,apps/rocm/src/main.rs:611-615- An aborted run leaves servers and the helper down, and drops their endpoint keys.
- AGENTS.md §5.
-
The manual-testing refusal check cannot be followed as written (standing; anchored check: holds) —
docs/manual-testing.md§8- The check runs again right after a full
uninstall --yesthat exits 0, with no reinstall in between. - It gives only the relative
services/bad.json.
- The check runs again right after a full
-
The core atomic writer's failure branches are untested, and two staging copies remain (standing; anchored check: partly holds) —
crates/rocm-core/src/lib.rs:4832,:4839,apps/rocmd/src/sandbox.rs:676-763,apps/rocm/src/therock.rs:4636-4645- The publish-failure cleanup is now tested (
lib.rs:5165). - Still untested: the write/sync-failure cleanup, the
AlreadyExistsretry and the exhaustedbail!.
- The publish-failure cleanup is now tested (
-
Fail-closed branches no test exercises (standing; anchored check: holds for the non-
.jsonfilter; the rest marked unverified this round)- Non-
.jsonfilter: onlysvc-corrupt.jsonis ever planted, so dropping the filter would turn any non-JSON file underservices/into a false "unreadable manifest" abort. - The checker could not tell, or did not verify, the other three: kill scope for the daemon stop, publish order, and
host_port_resolvesagainst a resolvable-but-dead endpoint.
- Non-
Non-blocking
- The description still does not mention the reworded
rocm services restartadvice (standing, not gating; anchored check: partly holds) —apps/rocm/src/main.rs:3825. The rocmd twin is now fixed. - Other stale or broken comments — confidence 85 · mechanical
- Start times described as Linux-only, although this PR adds Windows:
apps/rocm/src/uninstall.rs:394,apps/rocm/src/uninstall.rs:471,apps/rocm/src/main.rs:33686, theProcessIdentitydoc ("clock ticks since boot"; on Windows the value is a FILETIME), and theapps/rocmd/src/service.rstest expect message. probe_hosts's doc comment sits abovehosts_name_one_listener, so rustdoc attaches it there:apps/rocm/src/uninstall_gate.rs:800-827.- A garbled, duplicated sentence that says there is "no
services remove", although that command exists and the gate's own advice names it:apps/rocm/src/main.rs:34255-34256. - "pinned by nothing", although the same test now asserts it:
main.rs:34654. probe_hostshould beprobe_hosts:main.rs:34481,main.rs:34488.- A detached comment fragment:
uninstall_gate.rs:518-523. uninstall.rs:5-11anddocs/architecture.mdstill say "mechanical relocation… no behavior change / no owned types".
- Start times described as Linux-only, although this PR adds Windows:
- The daemon stop reads the start time before checking liveness —
uninstall_gate.rs:372-377. This is the orderidentity_state_with_observed's own doc warns can returnMatchesfor a recycled pid.terminate_verifiedre-checks before signalling, so the worst case is a false "stopped the background helper" line. Confidence 75 · logic · Fix: callidentity_state(&identity). - The "shares a live endpoint" overlap check misses a wildcard listener reached on a specific IP —
uninstall_gate.rs:535-559,uninstall_gate.rs:819-825. A live record on0.0.0.0:Pplus a stopped record on192.168.x.y:Pare not seen as the same listener. Our own live server then answers the stopped record's probe, which produces a false abort with "Every process recorded … is gone" advice. Confidence 70 · logic. - The abort's re-serve advice is incomplete —
uninstall_gate.rs:1012-1017.- It covers only publicly bound services. A loopback service started with
--require-api-keyalso loses its key. - "stopped before the failure" also lists services stopped after it, because the loop does not break on a failure.
- Confidence 70 · logic.
- It covers only publicly bound services. A loopback service started with
- The e2e harness imports production code to build its own test data —
tests/e2e-cucumber/tests/e2e/lifecycle_steps.rscallsrocm_core::process_start_ticks.tests/e2e-cucumber/README.mdsays "Black-box only… No imports from the rocm-cli codebase", and therocm-coredev-dependency comment justifies it forFileLockalone. Because the oracle is the same function the CLI uses, a units mismatch would pass. Confidence 75 · architectural. - The pid-dedup logic exists twice —
uninstall_gate.rs:480-493andmain.rs:15475-15490(terminate_recorded_service_pids). The safety check can drift from the list that actually gets killed. Confidence 70 · architectural. - Some tests are Linux-only without needing processes —
main.rs:34922,main.rs:35050,main.rs:35090. They use an injected stop, but thecfg(target_os = "linux")gate means the required Windows lane never runs the partner logic. Confidence 70 · mechanical. - User-facing docs omit two behaviours —
README.md, theUninstalldoc comment (main.rs:612-616) anddocs/manual-testing.mddon't say that uninstall now force-stops the rocmd helper. They also don't say that live services with no start time (e.g. started on Windows by an earlier version) make uninstall refuse. Confidence 75 · mechanical. - About 2,400 lines of gate tests went into
main.rs—uninstall_gate.rshas no test module, anduninstall_gate.rs:719-725points atmain.rs. That runs against AGENTS.md §6 ("not growth insidemain.rs"). Confidence 70 · architectural.
Decisions for the author
- Refusing on any live pid without a start time — tradeoff. It never force-kills an unproven process tree. The cost is that every pre-upgrade Windows record, and every recovered service until the first finding is fixed, needs manual cleanup. Confirm that this friction is intended once the recovery path records start times.
- Scope and size — non-blocking-improvement. About 5.5k lines, including the atomic-write consolidation, the
write_active_runtime_markerchange and thewindows-sysdrops, which are unrelated to uninstall. The description does disclose them. Splitting them out would make the gate itself reviewable (AGENTS.md §11). - Hand-rolled progress and summary output — non-blocking-improvement.
println!("stopping managed service …")covers a wait the code itself says "reads as a hang". AGENTS.md §6 points tocli_progress::AnimatedSpinner/cli_report::ActionReport. Existing uninstall output is hand-rolled too. - No scenario for the
--keep-binaries --keep-dataexemption or the helper stop — non-blocking-improvement. The description discloses this but gives no reason; AGENTS.md §3 asks for the reason when a scenario is left out.
Positive signals
- The stop pass decides everything that could doom the run first (unparseable manifests, pids without a start time, already-stopped probes) and only then stops anything, so an aborted run has stopped nothing.
StopFailureRemedy::advice/advice_rankandStoppedRecordVerdict::blocksare exhaustive matches, so a new failure class cannot silently default to "proceed".- Fail-open disclosures are values on the report rather than bare prints, which makes them assertable.
- Wildcard binds are probed on both loopback families, and "did not resolve" (
host_port_resolves) is kept separate from "nothing listening". stop_managed_services_then_removetakes the stop as a parameter, so the stop-before-remove ordering is pinned by tests that check files still exist after an abort.daemon_start_ticksis#[serde(default)], so old state files still load.
Deployment notes
- On Windows, live managed services started by an earlier
rocmhave no start time recorded. After upgrading,rocm uninstallrefuses until they are stopped by hand or their records are deleted. - Windows managed-service stops now check pid identity (
GetProcessTimes), so they can now returnRecycled/Indeterminatewhere they previously always matched. AutomationRuntimeStategains an optionaldaemon_start_ticksfield, which is backward-compatible on read.ManagedServiceRecordandAutomationRuntimeStatenow publish throughReplaceFileWon Windows.
What this covered
Every file in the change, read completely, plus the callers and callees of the changed symbols: rocmd supervise/recovery, rocm-core identity and termination, and the clap definitions. The diff was 7a062ad…6c489913 (merge-base with prw-base fe47c48 … PR head), checked across four independent workers: gate core, rocm-core/rocmd start-time side, unit tests, and e2e/docs/rules/history/prior-PR comments. CI rollup for 6c48991: every reported check passed or was skipped.
Did not run:
- No build or test was run. The host disk is under 40 GB free, so the review was static only. Every test verdict above was reached by reading, not by mutation runs, and nothing is scored 100.
- The design-review step ran in the driver, not as independent workers.
- The Windows
ReplaceFileWerror-1176 cleanup path was looked at but could not be established. - Ticket: EAI-8014 was retrieved by its ID (it resolves to ROCMAI-290); its comments were not read.
Not reconciled against any discussion on the PR.
The driver added these parts:
- the round note;
- the new/standing tags and gating notes;
- the standing items after the italic line in Blocking;
- the first Non-blocking item.
The anchored checks were given the objection and the author's note. They are not part of what this review read blind. Most were asked one at a time; the last nine were asked together in one message, each to be judged separately.
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.write_active_runtime_markernow publishes through the atomic helper instead of remove-then-rename.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, and lifecycle-25 refuses and keeps everything when a service record cannot be parsed. Existing scenarios were renumbered. The--keep-binaries --keep-dataexemption and the helper stop are covered by unit tests only, not by a scenario.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.