diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index f66fb1a9..4a39c0b5 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -1686,14 +1686,27 @@ impl ProdContainerOrchestrator { /// from `Some(vec![])`: a caller that removes things on absence must not /// treat "I could not look" as "nothing is installed". /// - /// The presence of a container — in ANY state — is the whole test. It - /// deliberately does NOT carry over the `user_stopped` / `disabled` - /// filters the old `manifest_ids` applied: a stopped app is still an - /// installed app, its container still - /// exists (exited), and treating it as uninstalled would make stopping an - /// app tear its companion down and rebuild it on the next start. "Is it - /// installed" and "is it currently meant to be running" are different - /// questions, and only the first one belongs here. + /// An app counts as installed if its container exists in ANY state, OR its + /// container name appears in the durable last-running snapshot. + /// + /// Both halves are load-bearing. The snapshot is what `crash_recovery` + /// itself calls "installation evidence" and what `reconcile_all_with_mode` + /// already trusts to recreate a previously-running app whose container + /// vanished. It is needed here because a container's absence is NOT proof + /// of uninstallation: containers on this node demonstrably come and go — + /// `lnd` read as absent and then present again within the hour, and the + /// boot reconciler logs "previously-running app has no container after + /// boot — recreating" for bitcoin-knots and electrumx repeatedly. Judging + /// on live containers alone would let a momentary gap look like a removal + /// and cost a healthy companion its unit. + /// + /// This also deliberately does NOT carry over the `user_stopped` / + /// `disabled` filters the old `manifest_ids` applied: a stopped app is + /// still an installed app, its container still exists (exited), and + /// treating it as uninstalled would make stopping an app tear its + /// companion down and rebuild it on the next start. "Is it installed" and + /// "is it currently meant to be running" are different questions, and only + /// the first one belongs here. pub async fn installed_app_ids(&self) -> Option> { let present: std::collections::HashSet = self .runtime @@ -1703,12 +1716,16 @@ impl ProdContainerOrchestrator { .into_iter() .map(|c| c.name) .collect(); + let was_running = crate::crash_recovery::load_last_running_names(&self.data_dir).await; let state = self.state.read().await; Some( state .manifests .iter() - .filter(|(_, lm)| present.contains(&compute_container_name(&lm.manifest))) + .filter(|(_, lm)| { + let container = compute_container_name(&lm.manifest); + present.contains(&container) || was_running.contains(&container) + }) .map(|(app_id, _)| app_id.clone()) .collect(), )