Skip to content

feat(daemon): run production runtimes through monitors - #414

Merged
clvsh merged 5 commits into
feat/service-monitor-prepfrom
feat/service-monitor-integration
Oct 11, 2026
Merged

clvsh merged 5 commits into
feat/service-monitor-prepfrom
feat/service-monitor-integration

Conversation

@clvsh

@clvsh clvsh commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

PR 4b of the monitor stack, on top of #413. This is the cutover: the Gateway, PHP workers and Managed Resource tracks now run under per-runtime monitors instead of as daemon children.

It follows your four decisions from 2026-10-10:

  1. Config proofs stay in the per-runtime JSON records.
  2. The 30 s health tick stays; there are no push events.
  3. The cutover runs automatically at daemon start.
  4. PV logs a held exit to daemon.log, then releases the monitor.

What changes

  • Supervisor internals (supervisor.rs) now run on monitors. Its API is unchanged, so gateway.rs and managed_resources/ barely change.
    • start launches a monitor through the active pv binary. It writes the runtime's JSON record with the monitor instance and no pid file. If that write fails, it stops the monitor.
    • Ownership: a runtime is PV's while its record names a monitor whose runtime still runs as recorded. This uses live_runtime from feat(daemon): let supervisors check monitor runtimes without a request #413, which is synchronous.
    • stop asks the monitor, logs the exit, then releases the monitor. If cleanup is unproven, PV keeps the monitor and its record, which blocks that runtime.
    • Removed: the spawn/reaper code (SpawnedProcessGroup), identity stabilisation and pid-file writing.
  • stop_subject runs before reconciliation starts a runtime and before the foreign-listener check. It clears any monitor the subject still has, including one that a cancelled start left without a record. Before this, such an orphan blocked its own replacement as "something else is listening".
  • Disable and uninstall stop every monitor, then remove records that no monitor backs. They still stop legacy pid-file runtimes with fix: stop managed runtimes before disabling PV #411's verified stop.
    • Postgres counts as stopped only after exit 0 with cleanup complete. Otherwise PV keeps its monitor and records.
  • Startup cutover: while holding jobs.lock, before the first reconciliation, the daemon stops runtimes that an older release left running. It removes their pid files and keeps their JSON proofs, so reconciliation restarts them under monitors. Failures are logged and startup continues. After the first run this does nothing.
  • DESIGN.md now describes current runtime supervision, records, adoption, disable and the cutover.

Bugs found while stress testing

A runtime whose children were still exiting could be blocked until a reboot. The monitor's fallback cleanup could mark cleanup unproven because of how macOS answers killpg while a group member is still exiting. That fix and its regression test now live in #412 (f9a9f59), because the bug is in the monitor's own cleanup.

A worker that exited right after starting reported the wrong error. Staging the config proof right after start found the runtime gone and reported RuntimeOwnershipChanged. It now reports "exited before readiness". Monitor startup takes slightly longer than the old direct spawn, so targeted_project_reconciliation_preserves_old_route_until_new_worker_is_ready hit this under load.

Tests

  • Deleted:
    • pid-identity tests (forged argv, shebangs, reused pids, malformed start identity)
    • the reaper tests
    • the two fixture self-tests
  • Migrated: fixtures install the test monitor (pv_fake::install_monitor), and the test lifeline cleans up runtimes when a test process dies. Guards no longer stop runtimes in Drop.
  • New:
    • A daemon restarted after its gateway was killed logs signal 9 and starts a replacement with a new pid.
    • The startup cutover stops a pid-file gateway and restarts it under a monitor.
    • Disable removes a JSON-only proof and releases every monitor.
    • Disable and uninstall refuse to act on a monitor record from another protocol version. This replaces the forged-JSON-identity variants.
    • A previous-boot legacy Postgres record is removed without signalling the reused pid. The pid now belongs to a plain process, not a monitored one.
    • start_replaces_a_monitor_left_without_a_record

Not in this PR

  • The planned integration test for "the Caddy verifier rejects a foreign peer" is skipped. mismatched_peer_process_group_receives_no_http_request_bytes already covers that rejection, and every gateway test runs the monitor-backed verifier.
  • The pid_path field, the legacy stop code and boot_session_id handling stay until PR 5.

Open risks

  • A future monitor protocol bump strands the monitors already running, because read_record refuses their records. The next bump needs a stop-before-update step.
  • A manual downgrade to a release without monitors fails on the foreign-listener check. Downgrades aren't supported.
  • CodeRabbit's recovery race from feat: add per-service runtime monitor core #412 is still open: an identity check followed by a group signal by number. Normal stops are immune because the monitor holds the unreaped leader. The window remains only in recover_monitor after a monitor dies.

Test plan

Summary by CodeRabbit

  • Reliability
    • Runtime processes remain available across daemon restarts and can be adopted without changing their process IDs.
    • Shutdown gives processes time to exit gracefully before force-stopping any that remain.
    • PostgreSQL records are retained unless a successful exit confirms that backend processes have stopped.
    • Legacy runtimes are stopped only after their identity is verified; records from earlier boot sessions are removed without signalling potentially unrelated processes.
    • If a runtime record cannot be saved after startup, the runtime is stopped.
    • Daemon disable and uninstall release runtime monitors and remove only records without an active monitor.
  • Bug Fixes
    • Reconciliation releases existing monitors before checking ports, helping prevent stale supervision state from interfering with recovery.

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Runtime supervision now uses per-runtime monitors for startup, ownership, adoption, and shutdown. Reconciliation handles monitor-backed and legacy runtime records. Daemon and CLI tests use monitor metadata and cover restart, cleanup, and recovery.

Changes

Runtime supervision

Layer / File(s) Summary
Monitor-backed startup and ownership
crates/daemon/src/supervisor.rs, crates/daemon/src/monitor.rs, crates/daemon/src/monitor/macos.rs, crates/daemon/src/lib.rs
ProcessSupervisor starts and adopts runtimes through monitors. Runtime metadata records monitor instances. Monitor operations are exported across targets, with non-macOS implementations reporting monitor availability and process-containment results.
Reconciliation and runtime cleanup
crates/daemon/src/runtime_stop.rs, crates/daemon/src/gateway.rs, crates/daemon/src/managed_resources/mod.rs, crates/daemon/src/jobs.rs, crates/daemon/src/structured_log.rs
Gateway and managed-resource paths stop runtimes through monitor subjects. Reconciliation and update jobs attempt legacy-runtime cleanup. Shared shutdown processes monitor-backed and legacy records and aggregates cleanup failures.
Monitor-backed test fixtures and coverage
crates/daemon/src/managed_resources/*, crates/daemon/src/health.rs, crates/daemon/src/jobs.rs, crates/daemon/tests/*
Tests install fake monitors and use runtime metadata instead of PID files for runtime checks. Coverage includes daemon restart, legacy-runtime replacement, monitor cleanup, Postgres monitor retention, and recovery after cancellation during a bounded worker wave.
Fake monitor and CLI fixtures
crates/pv-fake/src/*, crates/cli/tests/daemon.rs, crates/cli/tests/setup.rs, crates/cli/tests/support/runtime.rs
The fake installer can install a PV monitor. CLI tests cover disable and uninstall behavior with prior-boot and foreign monitor records. Runtime test helpers can save, alter, and restore a foreign monitor record.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature


Merge Risk | 🟡 Moderate · up to b4fb6

Merge Risk: 🟡 Moderate · up to b4fb6

Resolve the runtime cleanup and Postgres lifecycle concerns before merging to avoid stale processes, incorrect recovery guidance, or unsafe cleanup.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to b4fb6

Normal shutdown retains ownership records when cleanup cannot be proven. However, an interrupted startup can leave a monitor outside shutdown’s discovery pass, potentially allowing a runtime to start after shutdown reports success. No remote privilege-escalation path was established.

Retained concerns

  • Medium · reliability · inferred: An interrupted startup can escape shutdown’s recorded-monitor sweep. Once a monitor receives its startup request, it can continue after the controller disappears. Before publication, neither subject lookup nor shutdown enumeration discovers it. If enumeration finishes during that interval, shutdown can report success before the monitor publishes and launches its runtime. This weakens the guarantee that disable or uninstall contains all runtime activity. The outcome is inferred from the source ordering, not reproduced.
Security review details

Security Blast Radius

  • inferred — The lifecycle change affects the local installation’s Gateway, PHP workers, and Managed Resource tracks. The identified shutdown gap concerns continued runtime activity under existing authority, not demonstrated cross-account or cross-tenant privilege gain.

Security Findings and Attack Paths

  • inferred — The supported failure path requires interruption after a monitor accepts startup but before it publishes, followed by shutdown enumeration before publication. The monitor can then launch the supplied runtime despite completed cleanup. This is a shutdown-containment concern; no remotely attacker-triggerable exploit was established.

Trust Boundaries and Controls

  • observed — The monitor socket accepts same-UID clients and requires the current protocol and instance. This authorization model predates the PR and does not distinguish the daemon from other same-UID processes. No new higher-privilege authority was established; whether runtime isolation is required remains a threat-model gap.

Resilience and Maintainability Implications

  • observed — Record-before-exec ordering prevents an executing runtime from preceding its monitor record. Recorded unavailable monitors have an identity-checked recovery path, and normal stops preserve evidence on unproven cleanup. These controls constrain the concern to the pending-start discovery interval rather than a permanently invisible executing runtime.

Hardening Proposals

  • proposed — Make successful shutdown account for reserved-but-unpublished starts, or retain cancellation-safe startup ownership until each monitor reaches a discoverable state. A shutdown barrier should prevent a pending monitor from publishing and starting a runtime after cleanup completes.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 67.03% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 364 functions across 28 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: production daemon runtimes now run through per-runtime monitors.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR



















🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@clvsh
clvsh added this pull request to stack #415 October 10, 2026 06:11
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T01:12:04.987372Z cf5b6fd New commits
🔒 Security Review ✅ Completed 2026-10-10T06:16:44.073782Z b6b0e9f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codspeed

codspeed Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 7 untouched benchmarks


Comparing feat/service-monitor-integration (b4fb654) with feat/service-monitor-prep (f5ccf6f)

Open in CodSpeed

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

The cutover can discard ownership/recovery evidence without a verified stop. Three lifecycle issues need fixing before merge, including PostgreSQL cleanup and prior-boot recovery.

Reviewed changes Reviewed the production monitor cutover and the helper-only follow-up that arrived during this review.

  • Runtime supervision: Gateway, PHP workers, and Managed Resources now start, adopt, stop, and record config proofs through per-runtime monitors.
  • Lifecycle transitions: Startup migrates legacy runtimes; reconciliation retires orphan monitors; disable/uninstall enumerate monitors and retain unproven PostgreSQL exits.
  • Cleanup and coverage: Reviewed the EPERM group-exit handling, held-exit logging, fixture lifelines, migrated tests, new regressions, and platform-specific helper cleanup.
  • Validation: Formatting and the portable readiness test pass. The broader Linux library run was not usable for macOS lifecycle validation (unsupported platform/capabilities and missing fixture examples); artifact E2E failed before runtime execution because artifacts.prvious.test did not resolve.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment on lines +1299 to +1303
MonitorStopped::Stopped(state) => {
structured_log::runtime_exited(paths, subject, &exit_description(state.exit));
monitor::release_monitor(paths, subject).await
}
if started_at.elapsed() >= PROCESS_IDENTITY_TIMEOUT {
return Err(DaemonError::MissingProcessIdentity {
name: spec.name.clone(),
pid,
MonitorStopped::Unavailable => monitor::recover_monitor(paths, subject).await,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Reconciliation can discard the PostgreSQL evidence that offline shutdown deliberately retains. A monitor holding Signal(9) or a nonzero exit is released here after group cleanup, and an unavailable monitor is recovered without the PostgreSQL check; resource replacement/undemanded cleanup then deletes its records even though escaped backends have not been proven gone.

Technical details
# Preserve PostgreSQL cleanup proof across all retirement paths

## Affected sites
- `crates/daemon/src/supervisor.rs:1299-1303`: generic monitored stop releases any completed exit or recovers an unavailable monitor.
- `crates/daemon/src/supervisor.rs:432-452`: offline shutdown correctly retains unsuccessful/unavailable PostgreSQL exits.
- `crates/daemon/src/managed_resources/mod.rs:2458-2473`: replacement uses generic stop, deletes metadata, and starts a replacement.
- `crates/daemon/src/managed_resources/mod.rs:2661-2675`: undemanded cleanup uses generic stop before deleting records/releasing ports.

## Required outcome
- Current-boot PostgreSQL retirement must not discard a held unsuccessful exit or unavailable-monitor evidence without proving escaped backends gone.
- A failed offline stop must not become removable merely by re-enabling the daemon and running resource reconciliation.
- Add replacement and undemanded-resource integration regressions for a killed/nonzero PostgreSQL runtime, asserting retained monitor/metadata and no replacement or successful stopped observation.

## Suggested approach
- Share the PostgreSQL clean-exit admission rule between generic and offline retirement, retaining the prior-boot exception separately.

Related paths: replacement, undemanded cleanup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one: it isn't new with monitors. Before this PR, reconciliation restarted a crashed Postgres the same way: adopt_recorded found the dead pid and a fresh start replaced the records. Undemanded cleanup removed them too. Postgres also guards itself: a new postmaster refuses to start while old backends still hold its shared memory. The keep-the-records rule exists for pv daemon:disable and pv uninstall, which go on to delete binaries and data. Those paths still require a clean exit 0.

Comment thread crates/daemon/src/jobs.rs Outdated
Comment on lines +490 to +492
if let Err(error) = runtime_stop::stop_legacy_runtimes(&paths).await {
structured_log::legacy_runtime_stop_failed(&paths, &error.to_string());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] A failed legacy stop is logged, but the following reconciliation can erase the failed runtime’s ownership records. For an installed resource with no remaining demand, stop_resource_runtime finds no monitor, treats stop_subject(false) as success, and deletes the legacy PID/JSON while its process may still be alive, leaving later disable/uninstall unable to stop it safely.

Technical details
# Keep failed legacy cutover subjects blocked

## Affected sites
- `crates/daemon/src/jobs.rs:490-500`: legacy-stop failure does not constrain subsequent System reconciliation.
- `crates/daemon/src/supervisor.rs:243-355`: legacy stop deliberately preserves records on identity mismatch or unproven cleanup.
- `crates/daemon/src/supervisor.rs:469-470`: stop_subject returns false when no monitor is recorded, without checking retained legacy records.
- `crates/daemon/src/managed_resources/mod.rs:2625-2705`: undemanded resource cleanup records Stopped, deletes legacy records/config, and releases ports despite that false result.

## Required outcome
- Failed legacy stops must retain their exact ownership records and prevent replacement/undemanded cleanup for that subject, while independent reconciliation may continue.
- Add a startup integration test with an undemanded legacy resource whose verified stop fails while the runtime remains alive; assert retained records, a reported failure, and no successful stopped observation.

## Suggested approach
- Treat a remaining legacy PID record as an unresolved lifecycle transition before monitor-only start/cleanup paths can proceed, or carry the failed subjects into reconciliation.

Related path: resource cleanup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one. Logging a failed cutover and carrying on was a deliberate decision for this PR. The harm needs an old runtime that is both still running and no longer demanded at upgrade time, and whose verified stop then fails. Reconciliation stops undemanded resources promptly, so a running old runtime is demanded in practice. After cf5b6fd the cutover also runs at the start of every job, so it always comes before any cleanup. Blocking every subject, or failing every job, over one bad record would be worse for the common case.

Comment thread crates/daemon/src/supervisor.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6b0e9f075

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2660 to +2664
supervisor
.stop_subject(
&paths.resource_runtime_metadata(&track.resource_name, &track.track),
stop_signal,
RESOURCE_STOP_GRACE_PERIOD,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retain legacy records when no monitor was stopped

When startup cutover fails to stop a still-live pre-monitor Managed Resource, startup intentionally logs the error and continues into reconciliation. Because a legacy runtime has no monitor, this call returns false, but the result is ignored; the code then records the runtime as stopped, deletes its PID/JSON ownership evidence, and releases its ports while the process may remain alive. The same pattern affects stale workers in gateway.rs. Preserve the records and report the stop failure whenever a legacy PID record remains.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one, for the same reason as the Pullfrog thread on jobs.rs. It needs a verified stop of an old runtime to fail at cutover, and that runtime to be no longer demanded. Logging and continuing was a deliberate decision, and after cf5b6fd the cutover runs before any job's cleanup. The stale-worker path in gateway.rs only stops runtimes that the routing plan no longer contains, which has the same precondition.

Comment on lines +399 to +402
if let Err(error) = write_runtime_metadata(&spec, process.pid, &process.instance) {
let _stop_result =
stop_monitored_runtime(&self.paths, &process.subject, &process.instance, stop)
.await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate cleanup failures after metadata write errors

If writing the runtime metadata fails because of a filesystem or permission error, stopping the already-started monitor/runtime can also fail due to monitor unavailability, timeout, unproven cleanup, or release failure. Discarding _stop_result reports only the write error even though a runtime without its normal runtime record may remain alive and occupy its ports. Preserve both errors, as the existing monitor-start cleanup path does, so callers know cleanup was not completed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one. If the record write fails and the compensating stop also fails, the monitor's own record still names the runtime. The next reconciliation's stop_subject finds that monitor even though it has no runtime JSON beside it, which is the orphan case it handles, so nothing is left running unmanaged. The only cost is the second error message, after a record write fails on a disk PV already writes to.

Comment thread crates/daemon/src/jobs.rs Outdated
Comment on lines +488 to +490
// Runtimes from a release before monitors restart under monitors. Reconciliation reports any
// that couldn't be stopped, since it can't start a replacement over them.
if let Err(error) = runtime_stop::stop_legacy_runtimes(&paths).await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate other reconciliation until legacy cutover completes

The daemon starts its request server and config watcher concurrently with the startup task, and all of them enqueue into the same queue. A foreground or watcher-triggered reconciliation can therefore be queued and run before the startup job obtains its turn, while this cutover only runs afterward. During an upgrade, that earlier reconciliation treats healthy pre-monitor runtimes as unmonitored—for example, rejecting a demanded Gateway as a foreign listener or cleaning an undemanded legacy runtime without stopping it. Run the cutover before opening reconciliation admission, or otherwise gate every non-startup job until it completes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed in cf5b6fd. The queue runs jobs in arrival order, and on the update that installs monitors, pv update releases jobs.lock and immediately re-execs to queue its own update job. The new daemon's startup job is still retrying the lock every 50 ms, so the update job could run first and reconcile before the old runtimes were stopped. The cutover no longer lives only in the startup job: every reconciliation and update job runs it first (stop_legacy_runtimes_before_reconciliation), so whichever job goes first does it. Once nothing is left it only lists the run directory. New test: any_first_job_restarts_a_gateway_from_before_monitors_under_a_monitor runs a plain system reconciliation job with a pid-file Gateway present. It fails without the change.

let supervisor = ProcessSupervisor::new(paths.clone());
let mut failures = Vec::new();
// Every runtime started since monitors, including one whose record never got written.
for subject in monitor::recorded_monitor_subjects(&paths)? {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject monitor subjects that do not match their record directory

recorded_monitor_subjects trusts the subject embedded in each monitor record without verifying that it hashes to the directory containing that record. If a record is corrupted or copied from another subject, shutdown attempts to stop the embedded subject, never visits the actual monitor, and then retains its runtime JSON because that monitor directory still contains a record; with no accumulated failure, disable or uninstall can report success while the original runtime and monitor remain running. Validate the subject-to-directory association while enumerating records and fail closed on a mismatch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one. Only PV writes monitor records, into ~/.pv/run, which is 0700. A record would have to be a valid record for one subject, sitting in another subject's directory. That needs someone copying files by hand inside PV's private state, and nothing in PV does that.

Comment on lines +146 to +148
runtime
.block_on(crate::stop_recorded_runtimes(paths.clone()))
.map_err(anyhow::Error::from)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make Drop cleanup independent of the jobs lock

If a test returns early or unwinds while it still owns JobsLock, this Drop path calls stop_recorded_runtimes, which immediately tries to acquire that same exclusive lock and fails. The error is only printed, leaving the test's monitors and runtime processes alive until the entire test binary exits; this can retain listeners and contaminate later tests. The fallback teardown must stop its exact recorded monitors without depending on a lock that the unwinding test may still hold.

AGENTS.md reference: AGENTS.md:L3-L3

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one, since it's test-only. The guard is created before any JobsLock in these tests, so it is dropped after the lock, during unwinding too. Nextest also runs each test in its own process. If cleanup does fail, the test lifeline stops those monitors when that test's process exits, so nothing carries over into other tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (2)
DESIGN.md (1)

267-267: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the Postgres "exited before the stop" rule with stop_monitored_for_shutdown.

Line 267 says PV keeps the monitor and records if Postgres "exited before the stop". The code does not apply that rule. ProcessSupervisor::stop_monitored_for_shutdown in crates/daemon/src/supervisor.rs (Lines 431-446) checks only state.exit != Some(MonitorExit::Code(0)). It does not look at state.stop. A Postgres runtime that exited by itself with code 0 is therefore released, and its records are removed.

A held exit 0 is valid proof that the backends are gone, so the code is probably correct. The document describes a stricter rule than the code applies. That rule only holds for legacy pid-file runtimes, which have no exit status. Reword the monitored-path sentence so that it matches the code.

📝 Proposed wording
-`pv daemon:disable` and `pv uninstall` stop and release every monitor, then remove records that no monitor backs. Only a successful Postgres exit status proves that its escaped backends are gone. If Postgres exited before the stop, needed a force-kill, exited unsuccessfully, or its monitor is unavailable, PV keeps its monitor and records; `--force` does not bypass this proof.
+`pv daemon:disable` and `pv uninstall` stop and release every monitor, then remove records that no monitor backs. Only a successful Postgres exit status proves that its escaped backends are gone. If Postgres needed a force-kill, exited unsuccessfully, or its monitor is unavailable, PV keeps its monitor and records; `--force` does not bypass this proof. A held exit status of 0 counts as proof even when Postgres exited before the stop.
crates/pv-fake/src/install.rs (1)

58-64: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Build all daemon examples before running the CLI package tests.

The CLI fixtures install fakes below .pv/resources, so the new install_with branch calls install_monitor. After the existing pv-fake example is available, a package-scoped cargo nextest run -p cli can still fail because it does not build pv-monitor.

Build both examples before the package-scoped run:

cargo build --locked --all-features -p daemon --examples
cargo nextest run --locked --all-features -p cli

The pv-fake contract tests use temporary paths outside .pv and do not trigger this branch.

🧹 Nitpick comments (1)
crates/daemon/src/supervisor.rs (1)

114-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use [TypeName] links in the new doc comments.

The new doc comments refer to the monitor and the supervisor in plain prose only. Link the types so the docs resolve:

  • Line 114: link [ProcessSupervisor].
  • Line 136: link [monitor::live_runtime], which decides whether the monitor "still runs it as recorded".
📝 Proposed doc links
-/// A runtime this supervisor has just started under a monitor.
+/// A runtime that [`ProcessSupervisor::start`] has just started under a monitor.
-/// A runtime whose monitor still runs it as its record names.
+/// A runtime whose monitor still runs it as its record names; see [`monitor::live_runtime`].

As per coding guidelines: "PREFER [TypeName] references when writing Rust doc comments".

Also applies to: 136-136

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: QUIET
  • Plan: Advanced
  • Run ID: 65094ce9-dc63-4da7-b2f2-85e2554c56ba
📥 Commits

Reviewing files that changed from the base of the PR and between e8299ad and 9b12810.

⛔ Files ignored due to path filters (10)
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__mysql_tests__mysql_project_demand_installs_missing_fixture_track_before_start.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__mysql_tests__mysql_reconciliation_creates_database_allocation_and_renders_env.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__demanded_resource_cleans_runtime_files_when_process_exits_after_readiness.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__demanded_resource_records_failed_runtime_when_readiness_fails_before_env_rendering.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_preload_configuration_rejects_missing_library.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_preload_configuration_rejects_unsafe_combination.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_reconciliation_records_generated_env_when_readiness_fails_after_initdb.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__system_resource_reconciliation_stops_unlinked_project_runtime.snap is excluded by !**/*.snap
  • crates/daemon/src/snapshots/daemon__jobs__tests__partial_update_reconciliation_and_reporting.snap is excluded by !**/*.snap
  • crates/daemon/tests/snapshots/supervisor_foundation__supervisor_captures_logs_and_runtime_metadata_then_stops_child.snap is excluded by !**/*.snap
📒 Files selected for processing (28)
  • DESIGN.md
  • crates/cli/tests/daemon.rs
  • crates/cli/tests/setup.rs
  • crates/cli/tests/support/runtime.rs
  • crates/daemon/src/gateway.rs
  • crates/daemon/src/health.rs
  • crates/daemon/src/jobs.rs
  • crates/daemon/src/lib.rs
  • crates/daemon/src/managed_resources/mod.rs
  • crates/daemon/src/managed_resources/mysql_tests.rs
  • crates/daemon/src/managed_resources/runtime_contracts.rs
  • crates/daemon/src/managed_resources/tests.rs
  • crates/daemon/src/monitor.rs
  • crates/daemon/src/monitor/macos.rs
  • crates/daemon/src/runtime_stop.rs
  • crates/daemon/src/structured_log.rs
  • crates/daemon/src/supervisor.rs
  • crates/daemon/tests/daemon_foundation.rs
  • crates/daemon/tests/gateway_reconciliation.rs
  • crates/daemon/tests/gateway_runtime_contracts.rs
  • crates/daemon/tests/monitor.rs
  • crates/daemon/tests/real_artifact_gateway_e2e.rs
  • crates/daemon/tests/supervisor_foundation.rs
  • crates/pv-fake/src/events.rs
  • crates/pv-fake/src/fake.rs
  • crates/pv-fake/src/install.rs
  • crates/pv-fake/src/lib.rs
  • crates/pv-fake/tests/contracts.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

clvsh added 2 commits October 10, 2026 13:37
The Gateway, PHP workers and Managed Resource tracks now start through
per-runtime monitors instead of as daemon children. The supervisor keeps
its API. start launches a monitor and writes the runtime's JSON record
with the monitor instance and no pid file. A runtime is PV's while its
record names a monitor whose runtime still runs as recorded. stop asks
the monitor, logs the held exit to daemon.log, then releases it. Config
proofs stay in the JSON records.

Before reconciliation starts a runtime, stop_subject clears whatever
monitor its subject still has, including one a cancelled start left
without a record, so that monitor no longer looks like a foreign
listener.

At startup, before the first reconciliation, the daemon stops runtimes
that a release from before monitors left running and keeps their JSON
proofs, so reconciliation restarts them under monitors. Disable and
uninstall stop every monitor, remove records that no monitor backs, and
still stop pid-file runtimes. Postgres counts as stopped only after a
clean exit.

A worker or Gateway that exits right after it starts now reports that
exit instead of lost ownership.
Only the macOS monitor strips the PHP ini environment and reaps a runtime, so the constant moves into the monitor and the unused non-macOS reap stub goes.
@clvsh
clvsh force-pushed the feat/service-monitor-integration branch from 9b12810 to 314c0f2 Compare October 10, 2026 17:37
@clvsh clvsh mentioned this pull request Oct 10, 2026
4 of 5 tasks

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
crates/daemon/src/supervisor.rs (1)

1344-1351: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Show the Postgres recovery hint only for Postgres runtimes.

stop_monitor_instance handles every monitored subject. Its incomplete-cleanup error always adds {POSTGRES_RECOVERY}. When a Caddy, FrankenPHP, Redis or other runtime has MonitorCleanup other than Complete, the user gets Postgres recovery steps that do not apply to that runtime. Add the hint only when subject_resource_name(subject) == "postgres". For other runtimes, use a generic recovery message.

Proposed fix
     if state.cleanup != MonitorCleanup::Complete {
+        let recovery = if subject_resource_name(subject) == "postgres" {
+            format!(" {POSTGRES_RECOVERY}")
+        } else {
+            String::new()
+        };
         return Err(DaemonError::RuntimeCleanupUnproven {
             pid: state.runtime_pid,
             reason: format!(
-                "PV could not prove that the runtime's processes exited, so it kept its monitor and records. {POSTGRES_RECOVERY}"
+                "PV could not prove that the runtime's processes exited, so it kept its monitor and records.{recovery}"
             ),
         });
     }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: QUIET
  • Plan: Advanced
  • Run ID: bf24ddac-b681-4fe6-ad6c-58e6979e0878
📥 Commits

Reviewing files that changed from the base of the PR and between 9b12810 and 314c0f2.

⛔ Files ignored due to path filters (10)
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__mysql_tests__mysql_project_demand_installs_missing_fixture_track_before_start.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__mysql_tests__mysql_reconciliation_creates_database_allocation_and_renders_env.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__demanded_resource_cleans_runtime_files_when_process_exits_after_readiness.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__demanded_resource_records_failed_runtime_when_readiness_fails_before_env_rendering.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_preload_configuration_rejects_missing_library.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_preload_configuration_rejects_unsafe_combination.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__postgres_reconciliation_records_generated_env_when_readiness_fails_after_initdb.snap is excluded by !**/*.snap
  • crates/daemon/src/managed_resources/snapshots/daemon__managed_resources__tests__system_resource_reconciliation_stops_unlinked_project_runtime.snap is excluded by !**/*.snap
  • crates/daemon/src/snapshots/daemon__jobs__tests__partial_update_reconciliation_and_reporting.snap is excluded by !**/*.snap
  • crates/daemon/tests/snapshots/supervisor_foundation__supervisor_captures_logs_and_runtime_metadata_then_stops_child.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • crates/daemon/src/supervisor.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 314c0f2762

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +381 to +382
let instance =
monitor::recorded_monitor_instance(&self.paths, &subject)?.ok_or_else(|| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clean up monitors after post-start setup errors

If start_monitor succeeds but rereading its record fails (for example, due to a filesystem/JSON error), this ? returns while the newly started monitor and runtime keep running; the later process-identity lookup has the same problem. Because start returns no ManagedProcess, the caller cannot compensate, leaving a runtime without its normal runtime record and potentially occupying its ports after reconciliation reports failure. Apply the same compensating stop used for metadata persistence failures to every fallible post-start step.

AGENTS.md reference: AGENTS.md:L4-L4

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one, for the same reason as the record-write thread. Either way, the monitor has published its record, so the next reconciliation's stop_subject clears it before anything else starts for that subject. The two steps are re-reading a record the monitor has just written, and looking up the identity of a process that is running. Neither fails in practice, so I'd rather not add a third compensation path.

Comment on lines +432 to +433
MonitorStopped::Stopped(state)
if postgres && state.exit != Some(MonitorExit::Code(0)) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve Postgres records without a proven graceful stop

When Postgres already exited with code 0 before disable/uninstall, or its leader exits 0 but cleanup later escalates to SIGKILL for remaining group members, the monitor still reports Code(0) with Complete cleanup. This guard therefore releases the monitor and allows the following pass to delete its records, although DESIGN.md requires retaining them for pre-stop exits and force-killed shutdowns. The stop outcome must record that this request caused a non-escalated shutdown before accepting it as clean.

AGENTS.md reference: AGENTS.md:L4-L4

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping the code as it is, and I've fixed the doc in cf5b6fd. The postmaster exits 0 only after its backends have exited, so a held exit 0 proves they're gone whoever asked it to stop; the approved plan settled on exactly that rule. The SIGKILL case can't leave a Postgres process behind either: by the time the leader has exited 0, it has no children left in the group. DESIGN.md still had the stricter wording carried over from the pid-file rules ("exited before the stop"). It now says a held exit 0 counts whoever stopped it.

let supervisor = ProcessSupervisor::new(paths.clone());
let mut failures = Vec::new();
// Every runtime started since monitors, including one whose record never got written.
for subject in monitor::recorded_monitor_subjects(&paths)? {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wait for unpublished monitors during shutdown

If the daemon dies after spawning a monitor but before that monitor publishes monitor.json, a subsequent disable/uninstall can miss it because recorded_monitor_subjects explicitly skips directories without records. The monitor is independent of the daemon and can still publish and start its runtime after this one-time loop reports success, leaving a live listener after the LaunchAgent is removed. Drain or coordinate with in-flight monitor reservations before declaring recorded-runtime cleanup complete.

AGENTS.md reference: AGENTS.md:L4-L4

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one. A monitor starts nothing until it receives its start request over the private startup socket. If the daemon dies before sending it, the monitor sees EOF and exits. Once it has the request, it publishes its record within about 20 ms. Disable only stops runtimes after the LaunchAgent bootout and a confirmed daemon exit, which takes far longer than that window.

The real-artifact suites run PV in a temporary home with no installed pv binary, so every monitor spawn failed. They now install the test monitor there, as the contract suites do, and the Gateway E2E cleans up with stop_recorded_runtimes.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

No new issues found in this delta. Approval remains withheld because the existing lifecycle review threads are unresolved.

Reviewed changes Reviewed d849e5c8 against the previously examined 314c0f27 tree, focusing on real-artifact test startup and teardown.

  • Installed test monitors: Added monitor installation to the Gateway E2E and resource-matrix temporary homes so real runtimes can start through the monitor-backed supervisor.
  • Replaced Gateway cleanup: Switched from adopting only live Gateway/worker records to stop_recorded_runtimes, which also handles exited runtimes and monitors without runtime metadata.
  • Validated changed targets: Both integration targets compile on Linux, all five portable helper tests pass, and cargo fmt --all --check passes. The three opt-in artifact tests were not run locally; macOS artifact verification remains incomplete, with the Gateway E2E step failed and later steps still running at the last observation.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d849e5c812

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/daemon/src/jobs.rs Outdated
Comment on lines +490 to +492
if let Err(error) = runtime_stop::stop_legacy_runtimes(&paths).await {
structured_log::legacy_runtime_stop_failed(&paths, &error.to_string());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail startup when legacy cutover is incomplete

When a failed legacy record belongs to a removed Managed Resource track, logging this error and continuing allows the startup job to report success while the pre-monitor runtime remains alive: subsequent reconciliation only visits tracks present in the database, so it never performs the promised retry for this orphaned record. Propagate the cutover failure through startup rather than assuming reconciliation will report it.

AGENTS.md reference: AGENTS.md:L4-L4

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one. A track is removed through PV, which stops its runtime first, so a legacy runtime for a removed track would need a failed earlier stop as well as a failed cutover. That's two independent failures for one rare record. Failing startup, and with it every job, until a reboot would cost more than the case it covers. pv daemon:disable and pv uninstall still stop every remaining pid-file runtime with the verified stop and fail if they can't.

Comment on lines +828 to 829
if process_group_has_exited(self.owned.pid)? {
return Ok(());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Release monitors after test process-group cleanup

When RuntimeFixture::cleanup calls this method, the runtime group disappears but its monitor is never released; the fixture then discards its ownership handle, leaving the monitor process, reservation, socket, and record alive until the entire test binary exits. Repeated CLI fixtures can therefore accumulate monitors and violate the repository's fixture-cleanup isolation contract, so ordinary cleanup must release the exact monitor after the verified kill.

AGENTS.md reference: AGENTS.md:L3-L3

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one, since it's test-only. The fixture's monitor is released by the test lifeline when that test's process exits, and nextest runs each test in its own process. Some tests also depend on the monitor still holding the exit after this kill. For example, daemon_disable_keeps_dead_postgres_records_when_cleanup_is_unproven checks that disable refuses a Postgres that ended by signal 9.

…r-boot Postgres monitors

The legacy cutover ran only in the startup job, but the queue runs jobs
in arrival order. On the update that installs monitors, `pv update`
releases jobs.lock and queues its own update job right away, while the
new daemon's startup job retries the lock every 50 ms. When the update
job won, it reconciled before the old runtimes were stopped: it saw the
old Caddy as a foreign listener, and could delete the records of an old
runtime that was still running. Every reconciliation and update job now
runs the cutover first. Once nothing is left it only lists the run
directory.

After a reboot, a Postgres monitor is gone, so offline disable and
uninstall refused its record and told the user to restart their Mac,
which could never help. A record from an earlier boot names nothing
that still runs, so recovery now removes it without signalling,
as pid-file records already allowed.

DESIGN.md now says a held Postgres exit 0 counts whoever stopped it,
as the code and the approved plan do.
@clvsh

clvsh commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai on the two notes in your review summaries:

  • DESIGN.md wording for the Postgres exit rule: agreed, fixed in cf5b6fd. It now says a held exit 0 counts whoever stopped Postgres, which matches stop_monitored_for_shutdown and the approved plan. It also documents that a monitor record from an earlier boot is removed without signalling anything (new in the same commit).
  • The POSTGRES_RECOVERY hint for other runtimes: skipping. The text is just "Restart your Mac, then run this command again." That is the right recovery for any runtime whose cleanup is unproven: after a reboot, the earlier-boot records are removed without signalling. Only the constant's name mentions Postgres.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cf5b6fdf07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/daemon/src/jobs.rs
timing.queue_wait(),
&[],
);
stop_legacy_runtimes_before_reconciliation(paths).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reconcile every runtime stopped by the cutover

When the first post-upgrade job has a Project or resource scope, this call stops every pre-monitor runtime, but the branch below reconciles only the requested scope; an obsolete-Project job skips reconciliation entirely. For example, a Redis-scoped job can stop an unrelated legacy Gateway, worker, or MySQL runtime and leave it unavailable until a later system job, potentially after a long scoped install. Promote any job that performs the global cutover to system reconciliation, or stop only the legacy runtimes covered by its scope.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipping this one: the gap closes itself quickly. In-process jobs share one queue and one jobs.lock. Once any job is queued, the startup job, which retries every 50 ms, enqueues right behind it without waiting for the lock. That startup job is a full System reconciliation, so it restarts every runtime the cutover stopped. Unrelated runtimes are only down for the length of that one narrow job. Pullfrog's review of cf5b6fd checked the same thing. For a narrow job to go first at all, it has to land in the ~50 ms after pv update releases the lock, ahead of pv update's own job, which is itself System-wide. And that only happens once, at the upgrade to monitors. Limiting the cutover to a job's scope, or widening such a job to System, would add code for a gap the queue already closes.

Comment on lines +697 to +701
let Some(start_identity) = self.start_identity else {
return Ok(true);
};

Ok(platform::inspect_process_start_identity(self.pid)? != Some(start_identity))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Release the monitor after detecting a just-ready exit

When a Managed Resource becomes ready and then exits during the post-readiness confirmation window, this identity check returns true, but PendingManagedResourceReadiness::wait at managed_resources/mod.rs:2518-2522 only deletes the runtime files and returns an error. The monitor process, reservation, socket, and monitor record therefore remain until a later reconciliation or shutdown; stop and release the ManagedProcess before deleting its files while preserving the primary exit error.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, fixed in b4fb654. The exits-just-after-ready branch in PendingManagedResourceReadiness::wait now calls stop_with before deleting the runtime files, as the readiness-failure branch already does. The monitor is released right away and logs the exit it was holding. demanded_resource_cleans_runtime_files_when_process_exits_after_readiness now also checks that the monitor directory is gone. It fails without the change and passed 20 out of 20 stressed runs with it.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

No new issues found in this delta. Approval remains withheld while the two existing lifecycle review threads remain unresolved.

Reviewed changes Reviewed cf5b6fdf against the prior Pullfrog review at d849e5c8, focusing on legacy cutover ordering and boot-aware PostgreSQL recovery.

  • Moved legacy cutover before every job: Reconciliation and update jobs now stop pre-monitor runtimes before doing their work, including when an update job runs before startup reconciliation. Verified that the independently queued startup System job subsequently covers unrelated demanded runtimes.
  • Enabled prior-boot PostgreSQL cleanup: Offline shutdown now routes validated earlier-boot monitor records through recovery without signalling their recorded runtime PIDs, while unavailable current-boot PostgreSQL monitors remain fail-closed. Resolved the corresponding review thread.
  • Clarified clean-exit proof and added regressions: Documented that a held PostgreSQL exit 0 counts regardless of who stopped it, and added coverage for ordinary-job cutover and prior-boot recovery with an unrelated reused PID.
  • Validated the changed targets: Formatting, daemon library test compilation, CLI daemon test compilation, and the portable readiness regression pass locally. The new macOS lifecycle behavior was not executed locally on Linux; current-head CI has passed macOS 14, 26, and 27 arm64 lanes, while macOS 15 Intel was still running at the last check.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

@coderabbitai

coderabbitai Bot commented Oct 11, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd '^DESIGN\.md$'
rg -n -C5 'held|earlier.boot|Postgres|PostgreSQL' DESIGN.md
ast-grep outline crates/daemon/src/supervisor.rs --match 'stop_monitored_for_shutdown' --view expanded
rg -n -C8 'POSTGRES_RECOVERY|async fn stop_monitored_for_shutdown|monitor_from_earlier_boot|require_successful_exit|exit_success' crates/daemon/src/supervisor.rs

Length of output: 36326


@clvsh Thanks for the clarification.

  • DESIGN.md: Confirmed at Line 267. The text now accepts a held Postgres exit 0 regardless of who stopped it and documents earlier-boot record removal without signalling.
  • POSTGRES_RECOVERY: I withdraw this note. The message is runtime-neutral and applies when runtime cleanup is unproven. The constant’s name alone does not require a change.

✏️ Learnings added
Learnt from: clvsh
URL: https://github.com/prvious/pv/pull/414

Timestamp: 2026-10-11T01:15:57.283Z
Learning: In prvious/pv, crates/daemon/src/supervisor.rs uses POSTGRES_RECOVERY for the runtime-neutral message "Restart your Mac, then run this command again." This recovery guidance applies to any runtime whose cleanup is unproven, not only Postgres. Earlier-boot monitor records are removed without signalling their recorded processes. Do not flag reuse of this hint for other runtimes solely because the constant's name mentions Postgres.

Learnt from: clvsh
URL: https://github.com/prvious/pv/pull/414

Timestamp: 2026-10-11T01:15:57.283Z
Learning: In prvious/pv, DESIGN.md and ProcessSupervisor::stop_monitored_for_shutdown in crates/daemon/src/supervisor.rs accept a monitor-held Postgres exit code 0 as proof of clean shutdown regardless of who stopped Postgres. The rule does not require PV to have initiated the shutdown.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

…eadiness

A Managed Resource that exits during the post-readiness check had its runtime files removed, but its monitor kept running and holding the exit until a later reconciliation released it. That branch now stops the runtime through its monitor first, as the readiness-failure branch already does, so the monitor is released and its exit logged right away.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No new issues found in this delta. Approval remains withheld while the two existing lifecycle review threads remain unresolved.

Reviewed changes Reviewed b4fb654d against the prior Pullfrog review at cf5b6fdf, focusing on resource cleanup immediately after readiness.

  • Released fast-exit monitors before deleting runtime files when a newly started Managed Resource exits during the post-readiness check. Verified that this logs the held exit and preserves runtime records if monitor cleanup fails.
  • Strengthened regression coverage by asserting that the fast-exit Mailpit monitor directory is absent before fixture teardown, alongside the existing runtime-file and failed-observation assertions.
  • Validated portable checks with cargo fmt --all --check, daemon library test compilation, and the bounded-readiness regression, all passing. The monitor-dependent regression was not executed locally on Linux; current-head CI has passed its Linux and Windows runtime lanes, with all four macOS lanes still running at the last check.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Preserve legacy shutdown before releasing resource state. · mod.rs:2664-2670

crates/daemon/src/managed_resources/mod.rs:2664-2670
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Preserve legacy shutdown before releasing resource state.

When stop_subject returns false and a legacy PID/JSON record exists, use stop_legacy_runtime before recording Stopped, deleting runtime files, or releasing ports. Keep the records and port assignments if verified shutdown fails.

stop_legacy_runtimes_before_reconciliation logs legacy-stop errors and continues. An undemanded resource track can then reach this function while its legacy process is still running. Since that process has no monitor, stop_subject returns false; the current code ignores that result and releases the runtime state.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: QUIET
  • Plan: Advanced
  • Run ID: 99978b9c-898b-4090-a16a-1ab1a82a8eec
📥 Commits

Reviewing files that changed from the base of the PR and between cf5b6fd and b4fb654.

📒 Files selected for processing (2)
  • crates/daemon/src/managed_resources/mod.rs
  • crates/daemon/src/managed_resources/tests.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

@clvsh
clvsh merged commit 8fe7d31 into main Oct 11, 2026
10 checks passed
@clvsh
clvsh deleted the feat/service-monitor-integration branch October 11, 2026 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant