diff --git a/core/archipelago/src/container/boot_reconciler.rs b/core/archipelago/src/container/boot_reconciler.rs index 4fedc7d4..fbd2d9d1 100644 --- a/core/archipelago/src/container/boot_reconciler.rs +++ b/core/archipelago/src/container/boot_reconciler.rs @@ -128,21 +128,34 @@ impl BootReconciler { continue; }; let failures = crate::container::companion::reconcile(&installed).await; - // Reap failures are logged but deliberately kept OUT of - // `failures`, which drives the backoff below. Repair keeps - // a companion available; reaping only tidies one away. A - // reap that fails persistently (a wedged systemctl, say) - // must not back the repair path off to its 1h ceiling and - // starve the thing that actually matters. - for (companion, err) in - crate::container::companion::reap_orphans(&installed).await - { - tracing::warn!( - companion = %companion, - error = %err, - "companion reap failed" - ); - } + // `reap_orphans` is deliberately NOT called here. It is + // implemented and tested, and it must stay unwired until a + // DURABLE record of "this app is installed" exists. + // + // Proven harmful on archi-dev-box 2026-08-08: it removed + // archy-bitcoin-ui (36 minutes of no Bitcoin UI, until the + // operator reinstalled the backend) and archy-lnd-ui, both + // for apps that ARE installed. It was not a logic error — + // it did exactly what it was told. The inputs lied: the + // backends' containers were missing because of the + // clean-exit vanishing bug, and both had already aged out + // of running-containers.json, which only ever records what + // is CURRENTLY RUNNING. So container-presence and + // installation-evidence, the two independent signals the + // reaper trusts, were false at the same time and for the + // same underlying reason. + // + // Reaping turns one lost app into two, which is strictly + // worse than the orphan it cleans up. Leaving an orphan + // costs a stale UI tile; reaping a live app's companion + // costs the operator a working screen. Until "installed" + // can be answered without inferring it from runtime state, + // absence is not evidence of uninstallation. + // + // The provisioning half above is the actual fix for + // "fedimint installs but does not work" and stands on its + // own: a companion is never stood up for an app nobody + // installed, so no NEW orphans are created. for (companion, err) in &failures { tracing::warn!( companion = %companion, diff --git a/core/archipelago/src/container/companion.rs b/core/archipelago/src/container/companion.rs index c400eca5..0fe1cd26 100644 --- a/core/archipelago/src/container/companion.rs +++ b/core/archipelago/src/container/companion.rs @@ -682,6 +682,24 @@ fn due_after_grace( /// Stop and remove any companion whose backend app is not installed. /// +/// ⚠️ NOT WIRED, ON PURPOSE. Do not call this from the reconciler until a +/// DURABLE record of "this app is installed" exists to drive it. +/// +/// It ran on archi-dev-box on 2026-08-08 and removed two companions whose +/// backends were installed — archy-bitcoin-ui (36 minutes of no Bitcoin UI) +/// and archy-lnd-ui. Not a bug in the arithmetic below: the inputs were false. +/// Both backends' containers were missing because of the clean-exit vanishing +/// bug, and both had aged out of `running-containers.json`, which only records +/// what is CURRENTLY RUNNING. The two signals `installed_app_ids` combines are +/// therefore not independent — one root cause falsifies both at once, and +/// `ORPHAN_GRACE` cannot help because the condition is persistent, not +/// transient. +/// +/// The asymmetry that settles it: an un-reaped orphan costs a stale UI tile, +/// while a wrongly-reaped companion costs a working screen and turns one lost +/// app into two. Absence of evidence of installation is not evidence of +/// uninstallation. +/// /// The counterpart to `reconcile`, which can only ever *add*. Without this, /// a companion outlives its backend permanently: `remove_for` fires only on /// the explicit uninstall RPC path, so an install that fails after the