diff --git a/apps/desktop/loopx-control-plane/README.md b/apps/desktop/loopx-control-plane/README.md index 18e5cb337b..5a5180862f 100644 --- a/apps/desktop/loopx-control-plane/README.md +++ b/apps/desktop/loopx-control-plane/README.md @@ -265,6 +265,14 @@ secret. Updater signing does not provide Apple notarization. It also does not change Goal authority, grant capabilities, or stop running agents on behalf of the user. Services may briefly disconnect during reconciliation. +The optional startup feed check has a three-second deadline so an unavailable +update server cannot hold workspace startup indefinitely. A manual **Check for updates** +allows 45 seconds for the same official feed, including redirects; the panel +keeps showing its checking state while the existing workspace remains readable. +A timeout remains an explicit retryable error, never a claim that the App is +current. The separate 30-second download/signature-verification deadline and +installation/recovery boundaries are unchanged. + Published macOS preview artifacts use ad-hoc code signing to verify bundle integrity without requiring an Apple Developer account. They are not signed with a Developer ID and are not notarized, so macOS may require the operator diff --git a/apps/desktop/loopx-control-plane/docs/assets/manual-update-check-after.png b/apps/desktop/loopx-control-plane/docs/assets/manual-update-check-after.png new file mode 100644 index 0000000000..d874edb8fb Binary files /dev/null and b/apps/desktop/loopx-control-plane/docs/assets/manual-update-check-after.png differ diff --git a/apps/desktop/loopx-control-plane/docs/assets/manual-update-check-before.png b/apps/desktop/loopx-control-plane/docs/assets/manual-update-check-before.png new file mode 100644 index 0000000000..b8e45dc457 Binary files /dev/null and b/apps/desktop/loopx-control-plane/docs/assets/manual-update-check-before.png differ diff --git a/apps/desktop/loopx-control-plane/src-tauri/src/maintenance.rs b/apps/desktop/loopx-control-plane/src-tauri/src/maintenance.rs index b3c7984e71..95fa072129 100644 --- a/apps/desktop/loopx-control-plane/src-tauri/src/maintenance.rs +++ b/apps/desktop/loopx-control-plane/src-tauri/src/maintenance.rs @@ -401,7 +401,9 @@ pub async fn desktop_update( app.restart(); } state.manual_failure_pending.store(false, Ordering::Release); - let outcome = perform(&app, &action, &channel, url).await; + // A user-requested check may cross a slow release-feed redirect. It is + // asynchronous and should not inherit the optional startup check's budget. + let outcome = perform(&app, &action, &channel, url, Duration::from_secs(45)).await; if let Err(error) = &outcome { state.publish_manual_failure(error, &channel); } @@ -462,6 +464,7 @@ async fn perform( action: &str, channel: &str, url: tauri::Url, + check_timeout: Duration, ) -> Result { let state = app.state::(); if action == "forget_runtime_selection" { @@ -483,7 +486,7 @@ async fn perform( }) .endpoints(vec![url]) .map_err(|_| "update_unavailable")? - .timeout(Duration::from_secs(3)) + .timeout(check_timeout) .build() .map_err(|_| "update_unavailable")? .check() @@ -849,11 +852,13 @@ fn automatic_app_update(app: &AppHandle) { channel, async { let url = endpoint(channel)?; - let checked = perform(app, "check", channel, url.clone()).await?; + // Keep the optional feed lookup on the startup path bounded. + let checked = + perform(app, "check", channel, url.clone(), Duration::from_secs(3)).await?; Ok((checked["phase"] == "available").then_some(url)) }, |url| async move { - perform(app, "apply", channel, url).await?; + perform(app, "apply", channel, url, Duration::from_secs(3)).await?; Ok(()) }, || app.restart(),