diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index df4e6b8c..cc49b047 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -1473,12 +1473,21 @@ impl ProdContainerOrchestrator { // pass so the scanner overlays queued-but-down apps as Restarting // instead of Stopped. Each app is deregistered as its turn finishes, // so a start that genuinely failed shows its real state again. - crate::crash_recovery::pending_boot_starts_add(manifests.iter().flat_map(|lm| { - [ - lm.manifest.app.id.clone(), - compute_container_name(&lm.manifest), - ] - })); + crate::crash_recovery::pending_boot_starts_add( + manifests + .iter() + // An app mid-lifecycle-op must not enter the pending overlay: + // the scanner would show it Restarting while its stop worker + // is deliberately taking it down (and this pass will skip it + // via the same probe below anyway). + .filter(|lm| !crate::app_ops::lifecycle_op_in_flight(&lm.manifest.app.id)) + .flat_map(|lm| { + [ + lm.manifest.app.id.clone(), + compute_container_name(&lm.manifest), + ] + }), + ); for lm in manifests { let app_id = lm.manifest.app.id.clone(); let container_name = compute_container_name(&lm.manifest); diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index a125f6cd..48eb94f0 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -1277,10 +1277,16 @@ async fn scan_and_update_packages( // below knows this Restarting is scanner-authored (resolve it as soon // as podman reports a settled state) and not owned by an RPC restart // task (whose transitional state must be preserved). + // …but never for a user-stopped app: the stop marker is written + // before the stop runs, and a reconcile pass that queued the app + // moments earlier must not repaint the user's deliberate Stopped as + // Restarting (gate iteration-5 vaultwarden stop-wait, 2026-07-09 — + // the overlay held 'restarting' past the 120s window). if matches!( pkg.state, crate::data_model::PackageState::Stopped | crate::data_model::PackageState::Exited ) && crate::crash_recovery::is_pending_boot_start(id) + && !user_stopped.contains(id) { pkg.state = crate::data_model::PackageState::Restarting; pkg.exit_code = None;