Skip to content

ROCMAI-83: extract service.rs and watchers.rs from apps/rocmd/src/lib.rs - #487

Draft
jussielo-amd wants to merge 6 commits into
ROCm:mainfrom
jussielo-amd:rocmai-83-service
Draft

jussielo-amd wants to merge 6 commits into
ROCm:mainfrom
jussielo-amd:rocmai-83-service

Conversation

@jussielo-amd

@jussielo-amd jussielo-amd commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Seventh and eighth (final) PRs of Phase 5 (ROCMAI-83, part of the modularization epic ROCMAI-27), consolidated into one PR. Pulls service.rs and watchers.rs out of apps/rocmd/src/lib.rs. Originally opened separately as #487/#489; consolidated here to cut down the PR count for this phase. The two are coupled — watchers.rs's server-recover recovery logic calls directly into service.rs's managed-service lifecycle.

service.rs — the managed-service PID lifecycle (stop/terminate/descendant-pid discovery), run_daemon's foreground loop, supervise_service's spawn-and-recover path, and serve-log startup-phase polling. Non-contiguous in the source (the startup-phase/shutdown-signal helpers sit far from the rest of the cluster, separated by the entire test block) — both halves moved.

watchers.rs — event collection/dispatch for all built-in watchers (TheRock update, GPU metrics/thermal-pressure, cache-warm, driver-upgrade, server-recover), managed-service recovery classification, and automation-proposal queuing. Also non-contiguous: queue_proposal/queue_proposal_with_arguments/proposal_tool_for_action/proposal_arguments_for_action sit between persistence.rs-territory code, and detached_rocmd_command sits at the tail of the file before the test module. Watcher-only consts (SERVER_RECOVER_BACKOFF_MS, SERVER_TRANSIENT_STALE_MS, ENDPOINT_HEALTH_TIMEOUT, THEROCK_UPDATE_INTERVAL_MS, GPU_METRICS_INTERVAL_MS, GPU thermal/VRAM pressure thresholds) moved with it.

  • No behavior change. Reaches back into still-crate-root items that are actually in common.rs/persistence.rs (already merged/landed via ROCMAI-83: extract persistence.rs from apps/rocmd/src/lib.rs #477 and ROCMAI-83: extract common.rs, webhook.rs, and cli.rs from apps/rocmd/src/lib.rs #479) via the appropriate qualified paths.
  • 47 tests that exercise these modules' own logic (13 service + 34 watchers: the supervise key-guard regression tests, run_daemon's automation-loop gate, descendant-pid discovery, startup-phase log parsing, event-collector/gpu/cache-warm/driver-upgrade/server-recover dispatch, therock-update handling, watcher-policy mode mapping, recovery-reason classification) moved into each module's own #[cfg(test)] mod in this same PR.
  • docs/architecture.md updated in this PR — this completes Phase 5: lib.rs is now top-level glue (module declarations and the two externally-consumed entry points re-exported from cli.rs).

Targets main directly (not stacked on a sibling PR branch) so this PR can't be silently auto-closed if an intermediate branch gets deleted. Depends on #479 and #483 merging first — this code calls into sandbox::/mcp:: paths that only exist once #483 lands, so CI will be red until then; that's expected, not a regression. Merge order: #479 → #483 → this PR.

Supersedes #489, which is closed in favor of this PR.

Test plan

@jussielo-amd
jussielo-amd marked this pull request as ready for review October 1, 2026 13:52
@jussielo-amd
jussielo-amd requested a review from a team as a code owner October 1, 2026 13:52
@jussielo-amd
jussielo-amd requested a review from r0x0r October 1, 2026 13:52
@jussielo-amd
jussielo-amd enabled auto-merge October 1, 2026 13:52

@r0x0r r0x0r left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (draft): extraction is clean; held by the phase-wide merge problem

The code motion verified as faithful, including the endpoint-key guard that the regression tests in this PR exist to protect. The reasons to hold are not about this diff in isolation — they are about how the eight PRs in this phase combine.

service.rs has 13 crate:: references that break when siblings merge

This module reaches back into the crate root for items that other open PRs move elsewhere:

crate:: reference relocated by
record_event, load_managed_services #477 → persistence.rs
optional_arg, parse_gpu_indices_arg, apply_endpoint_key_env, engine_healthcheck_ready, ensure_public_service_has_endpoint_key #479 → common.rs
start_local_webhook_source, receive_local_webhook_event #480 → webhook.rs
evaluate_watchers, evaluate_watchers_for_events, reconcile_watcher_snapshots, load_service_record #489 → watchers.rs

Each becomes a dangling path once the owning PR lands. The part that makes this worth blocking on rather than noting: these references live in service.rs, a file none of those PRs touches. There is no overlapping hunk, so git merges them cleanly and the breakage shows up as a compile error on main with no conflict to warn anyone.

I checked this across all eight PRs rather than just this one. 52 such references break, spread over six of them — #489 watchers.rs (18), this PR (13), #481 cli.rs (11), #484 mcp.rs (5), #480 webhook.rs (1), #479 common.rs (1). The payload_string case between #480 and #489 is the cleanest illustration and I've written it up there.

The crate::-reach-back approach is fine for one PR against main; it only fails because eight are in flight against the same base. Two ways out:

  1. Stack them — each branches off its predecessor, so every crate:: path is correct by construction. AGENTS.md §11 already describes this ("keep stacked PRs in draft until dependencies merge upstream, then rebase and move out of draft").
  2. Keep them independent, rebase serially — after each lands, rebase the next and repoint its paths before merging.

The architecture-doc hunk

The doc text lists seven modules as extracted; this branch has lib.rs, main.rs, service.rs.

Worth being precise, because it would be reasonable to assume this is inherited drift rather than new: it isn't. main's current text is `lib.rs` is **not yet modularized** — see EAI-7768 (docs/architecture.md:34). Every one of the eight PRs rewrites that same single line with its own cumulative list, so each is independently introducing the inaccuracy, and the result is only true at merge order 477 → 479 → 480 → 481 → 483 → 484 → 487 → 489. Scoping each hunk to its own module makes it correct at any order, and turns eight whole-line rewrites into eight trivial appends.

Verified clean

  • The endpoint-key guard survived intact. ensure_public_service_has_endpoint_key is still called twice in supervise_service (service.rs:574, :619), in the same order, with the same arguments and the same previously_required registry-propagation path via load_managed_services. This is the one I most wanted to be sure of: the PR moves the key-guard regression tests in the same commit as the code they guard, so a mistake in the guard could have travelled with its own tests and still gone green.
  • PID lifecycle unchanged — terminate_process sends -TERM, force_terminate_remaining_processes sends -KILL after thread::sleep(Duration::from_millis(750)), and descendant-PID discovery is identical. Signal choice, ordering and escalation delay all preserved; nothing here can orphan or over-kill a process differently than before.
  • run_daemon's loop unchanged — all three tokio::select! arms (ticker, webhook event, shutdown) identical, MissedTickBehavior::Delay preserved.
  • Startup-phase log polling unchanged — offset arithmetic *pos += read as u64 and the rotation reset *pos = 0 when len < *pos both preserved.
  • GPU-required behavior intact (AGENTS.md §6) — device_policy still threaded through to engine_serve_http_args; no CPU fallback introduced.
  • The non-contiguous move is correct — both halves landed despite being separated by the entire test block, with nothing swept in from between them.
  • Call sites — stop_managed_service, run_daemon, supervise_service and print_status all repointed to service:: (lib.rs:281, :294, :309, :615, :2270); no definition left behind in lib.rs.
  • Imports — HashSet and tokio::time::{self, MissedTickBehavior} correctly moved; BufRead correctly kept in lib.rs, where stdin.lock().read_line() at line 1556 still needs it, and the #[cfg(test)] use tokio::time; is correctly narrowed to the one test that uses time::timeout. Nothing fails under -D warnings.
  • Tests — 130 attributes conserved (117 + 13), matching the claimed 13 exactly.
  • No dead code, stubs, todo! or unimplemented!; every let _ = suppression matches its original.
  • No module-name collision — no sibling PR creates a service.rs or defines run_daemon/supervise_service/stop_managed_service/print_status.
  • Blast radius — nothing outside apps/rocmd/src/ references any moved symbol.

🤖 by agent-hub on AMD AgentHub

@r0x0r r0x0r added the agent-hub-reviewed agent-hub has reviewed this label Oct 1, 2026
@jussielo-amd
jussielo-amd changed the base branch from main to rocmai-83-mcp October 2, 2026 14:43
@jussielo-amd
jussielo-amd marked this pull request as draft October 2, 2026 14:43
auto-merge was automatically disabled October 2, 2026 14:43

Pull request was converted to draft

@jussielo-amd

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. Both blocking points are resolved by stacking, per AGENTS.md §11:

  • The 13 crate:: references: rebased this branch onto rocmai-83-mcp (base now set to rocmai-83-mcp, PR back to draft). All references owned by already-landed siblings (persistence.rs, common.rs, webhook.rs) are now repointed to those modules (persistence::load_managed_services/record_event, common::parse_gpu_indices_arg/optional_arg/apply_endpoint_key_env/engine_healthcheck_ready/ensure_public_service_has_endpoint_key, webhook::start_local_webhook_source/receive_local_webhook_event). The remaining crate:: reaches (evaluate_watchers, evaluate_watchers_for_events, reconcile_watcher_snapshots, WATCHER_TICK_INTERVAL, load_service_record) are correctly left as-is — they're still owned by lib.rs until #489/watchers.rs lands, so the path is valid today; that PR will repoint them when it extracts.
  • architecture.md: scoped to an incremental append onto the predecessor's cumulative list (adds just service.rs's entry, narrows "still pending" to watchers.rs) rather than a whole-line rewrite, so it's correct regardless of final merge timing.

While rebasing I also found and fixed the other direction of the same hazard: cli.rs, mcp.rs, and sandbox.rs (landed by predecessor PRs #481/#484/#483) had their own stale bare crate:: calls into stop_managed_service/run_daemon/supervise_service/print_status — those now point at service::. And I deduped service.rs's test-only temp_app_paths/unique_test_root helpers in favor of the shared crate::test_support module (same duplicate-ownership pattern as update_check_message/CommandCapture in the earlier PRs of this stack).

All 130 test attributes (117 + 13) still present and green, cargo clippy --workspace --all-targets -- -D warnings clean, cargo fmt --check clean.

Second PR of Phase 5 (rocmd modularization, ROCMAI-27): pull the
helpers shared across >=2 of the still-inline sandbox/mcp/service/
watchers clusters into their own module, ahead of extracting those
clusters themselves. Covers GPU/amd-smi snapshotting, the
bridge-snapshot diagnostic, CommandCapture/command-timeout plumbing,
and small arg/healthcheck/endpoint-key utilities. No behavior change;
call sites repointed to common::. The 8 tests that exercise these
helpers directly (not mixed with a not-yet-extracted cluster's own
logic) moved into common.rs's own #[cfg(test)] mod in this same PR.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Third PR of Phase 5 (rocmd modularization, ROCMAI-27): pull the local
webhook source (axum routes, request validation, watcher-kind
allow-list) into its own module. Fully self-contained aside from one
call into payload_string, which stays at the crate root until
watchers.rs is extracted in a later PR (PR8 will need to re-point that
one call site once it moves).

No behavior change; call sites repointed to webhook::. The 17 tests
that exercise this module's own logic directly (not mixed with
run_daemon/Cli-parsing/watcher-domain logic that happens to share the
local_webhook_* name prefix) moved into webhook.rs's own

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Fourth PR of Phase 5 (rocmd modularization, ROCMAI-27): pull the
Cli/Command clap definitions, SandboxToolArg/SandboxToolPolicy, and
the top-level dispatch (run_cli/run_bin_cli/run_from_args) into their
own module, following the mechanical-relocation pattern (thin
dispatch; call sites into not-yet-extracted clusters stay crate::-
qualified until those land). lib.rs re-exports run_bin_cli/
run_from_args via pub use so the crate's only two external call sites
are untouched.

No behavior change. SandboxToolArg/SandboxToolPolicy are consumed
pervasively by the still-inline sandbox.rs cluster (and its test
suite), so every one of those ~89 call sites is repointed to
cli::SandboxToolArg/cli::SandboxToolPolicy.

The 4 tests that exercise Cli/Command parsing directly (not mixed
with sandbox/mcp/webhook/service domain logic that happens to share
a name prefix or construct these types as a parameter) moved into
cli.rs's own #[cfg(test)] mod in this same PR.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Fifth PR of Phase 5 (rocmd modularization, ROCMAI-27): pull bubblewrap/
native sandbox execution, the atomic-write helper family, artifact
prefetch policy gating, and the check_updates/driver_plan sandbox-tool
result shaping into their own module.

No behavior change. Rebased onto the common.rs/cli.rs/persistence.rs
extractions earlier in this stack, so this module now reaches those
items through their real owners (common::CommandCapture,
cli::SandboxToolArg/SandboxToolPolicy, persistence::load_managed_services)
instead of a crate-root placeholder; restart_managed_service,
run_rocm_capture_for_paths, and stop_managed_service stay at the crate
root pending service.rs. The rebase also found update_check_message
independently duplicated in common.rs (landed earlier in this stack) and
sandbox.rs (this PR); sandbox.rs now calls common's copy instead of
keeping its own, and cli.rs's two sandbox-dispatch call sites
(run_sandbox_runner, run_sandbox_tool) were updated to the sandbox::
path.

37 tests that exercise this module's own logic (atomic-write semantics,
sandbox-tool dispatch and output shaping, prefetch policy gating,
huggingface-URL classification) moved into sandbox.rs's own
are really about the managed-service-registry stop/restart lifecycle
(service.rs), or that use this module's sandbox_check_updates_value/
sandbox_driver_plan_value purely as mock watcher-event data, stayed in
lib.rs to move with service.rs/watchers.rs in their own later PRs.

sandbox.rs's test module now reuses crate::test_support::temp_app_paths
(the shared fixture introduced earlier in this stack) instead of its own
copy of temp_app_paths/unique_test_root/workspace_test_artifact_dir.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Sixth PR of Phase 5 (rocmd modularization, ROCMAI-27): pull the MCP
stdio server, tool schema table, tool dispatch, and the rocm-subprocess
capture/argv-building helpers behind the MCP tools into their own
module.

No behavior change. `CommandCapture` and `run_command_with_timeout`
were independently claimed by this PR and by the already-landed
common.rs extraction (both authored against the pre-stack monolith,
unaware of each other); common.rs keeps ownership since it landed
first in the merge order, so mcp.rs now imports both from
`crate::common` instead of redefining them. The other crate-root
reach-backs this module needed (`build_bridge_snapshot`,
`gather_gpu_snapshot_for_config`, `bridge_engine_inventory`,
`load_managed_services`) are now reached via `crate::common`/
`crate::persistence`, since those modules landed earlier in this
stack; `stop_managed_service` stays reached via bare `crate::` since
it has not moved out of lib.rs yet. `cli.rs`'s MCP dispatch arms and
sandbox.rs's `run_rocm_capture_for_paths` import (both landed before
this PR, so both still reached into lib.rs directly) are repointed to
`crate::mcp::`.

15 tests that exercise this module's own logic (MCP tool-schema/
dispatch shape, read-only-verb classification, install_sdk/
install_engine/launch_server/watcher_enable argv building, and
read_tail_lines) moved into mcp.rs's own #[cfg(test)] mod in this
same PR, reusing `crate::test_support::workspace_test_artifact_dir`
instead of a second local copy. Tests that share a name prefix or
construct these types but actually exercise Cli parsing, run_daemon,
or watcher-domain logic stayed in lib.rs for their own later PRs.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Seventh PR of Phase 5 (rocmd modularization, ROCMAI-27): pull the
managed-service PID lifecycle (stop/terminate/descendant-pid
discovery), run_daemon's foreground loop, supervise_service's
spawn-and-recover path, and serve-log startup-phase polling into their
own module. Non-contiguous in the source (the startup-phase/
shutdown-signal helpers sit far from the rest, separated by the whole
test block) -- both halves moved in this PR.

No behavior change. Reaches back into still-crate-root items
(load_managed_services, record_event, start_local_webhook_source,
receive_local_webhook_event, evaluate_watchers/evaluate_watchers_for_
events/reconcile_watcher_snapshots, WATCHER_TICK_INTERVAL,
parse_gpu_indices_arg/optional_arg/ensure_public_service_has_endpoint_
key/apply_endpoint_key_env/engine_healthcheck_ready,
load_service_record) via crate::, since the modules that will
eventually own those (persistence.rs/webhook.rs/watchers.rs/
common.rs) are separate, independent PRs not present on this branch.

13 tests that exercise this module's own logic (the supervise
key-guard regression tests, run_daemon's automation-loop gate,
stop_managed_service, descendant-pid discovery, startup-phase log
parsing, engine_serve_http_args) moved into service.rs's own
sandbox/mcp-domain tests that construct ManagedServiceRecord or call
stop_managed_service merely as setup stayed in lib.rs for
watchers.rs/other PRs.

Stacked on rocmai-83-mcp (ROCMAI-83 Phase 5 batch, AGENTS.md §11):
rebased service.rs's crate:: reaches onto the sibling modules already
landed by the predecessor PRs (persistence.rs, common.rs, webhook.rs),
leaving only the references still owned by lib.rs (evaluate_watchers*,
reconcile_watcher_snapshots, WATCHER_TICK_INTERVAL, load_service_record)
until watchers.rs lands. Deduped the test-only temp_app_paths/
unique_test_root helpers in favor of the shared crate::test_support
module, and repointed cli.rs/mcp.rs/sandbox.rs's stale crate::-root
calls to service::stop_managed_service/run_daemon/supervise_service/
print_status now that this module owns them.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
@jussielo-amd jussielo-amd changed the title ROCMAI-83: extract service.rs from apps/rocmd/src/lib.rs ROCMAI-83: extract service.rs and watchers.rs from apps/rocmd/src/lib.rs Oct 6, 2026
@jussielo-amd

Copy link
Copy Markdown
Collaborator Author

Scope note: this PR now covers service.rs + watchers.rs (consolidated from #489, which is closed), and now targets main directly instead of rocmai-83-mcp. Diff and description updated accordingly. Depends on #479 and #483 merging first — CI will be red until then.

@jussielo-amd
jussielo-amd marked this pull request as draft October 6, 2026 09:48

@r0x0r r0x0r left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review (draft): the code motion holds, but the pushed head is not what the description says

Since my review at fbb96b9, the branch was stacked and rebased (f9bd7cf), #489 was closed, and the description and a comment now say this PR is service.rs + watchers.rs against main. The branch on GitHub does not match that. I'm commenting only, since it is a draft with a conflict.

Status of my earlier findings

  • (b) crate:: breakage across sibling PRs: resolved in mechanism, unverifiable in practice. The serial order #479 -> #483 -> this PR turns a silent break into an explicit dependency. I overlaid service.rs and watchers.rs (from #489's 1c066ea) on main and on #483's head and resolved every crate:: path. On #483's head there are zero unresolved paths, so the order works. On main there are 14. See Blocking 2 for why #483 is not yet a sound base.
  • (c) architecture.md: not resolved. The hunk is still a whole-line cumulative rewrite. Head's text still says "Still pending: the watcher cluster", which contradicts "completes Phase 5". It also drops the "earns a place in common.rs" admission-rule sentence that #479 put on main. Resolve the conflict by keeping that sentence.

Blocking before approval

  1. Head f9bd7cf has no watchers.rs and is still the old stack. Against main, the diff is 8 files, +9325/-9161. It carries the common/webhook/cli/sandbox/mcp commits on the 57b78c6 base, and service.rs is only the last of those. watchers.rs exists only on closed #489's head. git merge-tree shows add/add on common.rs and conflicts in lib.rs and architecture.md. The commit message is also stale ("Stacked on rocmai-83-mcp"). The "47 tests / 13 + 34" and "scope note" comments describe an unpushed state. Please push the rebased branch.
  2. The dependency claims do not match main or #483.
    • #479 landed common.rs only. webhook.rs and cli.rs are not on main.
    • #479's review fixup moved parse_gpu_indices_arg, optional_arg and engine_healthcheck_ready back into lib.rs. So crate::common::{parse_gpu_indices_arg, optional_arg, engine_healthcheck_ready} does not resolve on main.
    • #483's head (36513c1) re-adds those helpers, plus print_bridge_snapshot, to common.rs. That quietly reverses the landed fixup and the rule in docs/architecture.md. By that rule parse_gpu_indices_arg and engine_healthcheck_ready have exactly one caller (service.rs:525, :839) and belong in service.rs. optional_arg is shared by service.rs and watchers.rs, so common.rs is right for it.
    • The body says the code needs mcp:: paths from #483, but neither module references mcp::, and #483's head has no mcp.rs (its body says sandbox + mcp). The paths actually needed are webhook::, cli:: and sandbox::.
    • #483's inline MCP code still calls bare stop_managed_service (lib.rs:775). This PR has to repoint it once rebased.
      Please make the descriptions and branches agree, and decide where those three helpers live.

Non-blocking

  • lib.rs keeps WATCHER_TICK_INTERVAL (used only by run_daemon) and ARTIFACT_PREFETCH_TIMEOUT (sandbox). It also keeps one sandbox-subject test, sandbox_tool_stop_server_updates_manifest_and_skips_current_pid. The crate-level #![allow(clippy::items_after_test_module)] is now vacuous. Moving these next to their users would make "lib.rs is glue" literal.
  • The "130 tests" claim is stale. main now has 131 because of install_sdk_rejects_system_prefix_reached_by_escaping_home. It will conflict in lib.rs on rebase and likely belongs in mcp.rs.
  • Only a docs-build status is reported on the head. cargo xtask coverage (AGENTS.md coverage floors, rocmd 75.36) is not in the test plan. Motion should be neutral, but run it.

Verified clean (service.rs from f9bd7cf, watchers.rs from #489's 1c066ea; no cargo here, so nothing was built or run)

  • A token-level diff of the removed lib.rs code against service.rs and watchers.rs shows only module-path prefixes, pub(crate) and rustfmt reflow. That covers the endpoint-key guard, the TERM/KILL 750ms escalation, the run_daemon select! arms, log offset arithmetic, watcher dispatch, recovery classification, proposal queuing and detached_rocmd_command (both cfg variants). Every watcher threshold constant is unchanged and moved with it.
  • No function was lost from lib.rs. pub(crate) coverage and path resolution check out in both heads.
  • Tests: the 130 test names are identical to pre-stack (13 service + 34 watchers). There is still 1 #[ignore], and no todo!/stubs.
  • Nothing outside apps/rocmd/src and docs/ is touched.

🤖 by agent-hub on AMD AgentHub

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-hub-reviewed agent-hub has reviewed this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants