From 89200975f38964d0a91c8b73e1d6964cd8d0d550 Mon Sep 17 00:00:00 2001
From: BB-fat <45072480+BB-fat@users.noreply.github.com>
Date: Wed, 12 Aug 2026 14:45:34 +0800
Subject: [PATCH] fix(update): run periodic update check in daemon so hints
actually fire (#76)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(update): run periodic update check in daemon so hints actually fire
The CLI spawned a detached thread to refresh ~/.bsk/update-check.json,
but the process exits as soon as the command finishes, killing the
thread before the fetch could complete — so the cache was never
written and the new-version hint never showed.
Move the periodic check into the daemon: a tokio task ticks every
30 minutes (down from 24h), refreshes the cache when missing/stale,
and logs failures at warn/info into the daemon log. The CLI path is
now read-only: it prints the hint only from a fresh cache and never
spawns threads or touches the network.
* fix(update): decouple daemon refresh window so checks truly run every 30min
The refresh decision reused the 30min tick interval as its freshness
window, and with is_fresh()'s inclusive bound a cache written just
after tick N still counted as fresh at tick N+1 — so in steady state
the daemon only refetched every other tick (~60min).
Judge staleness against DAEMON_REFRESH_WINDOW (25min = 5/6 of the
tick) instead: every 30min tick now finds the cache stale and really
refreshes, while a daemon restarted with a younger-than-25min cache
still skips its first tick. CLI hint freshness semantics
(UPDATE_CHECK_INTERVAL = 30min) are unchanged.
---
crates/bsk-cli/src/cli/update.rs | 183 +++++++++++++++++++++++++----
crates/bsk-cli/src/daemon/start.rs | 65 ++++++++++
crates/bsk-cli/src/main.rs | 2 +-
3 files changed, 228 insertions(+), 22 deletions(-)
diff --git a/crates/bsk-cli/src/cli/update.rs b/crates/bsk-cli/src/cli/update.rs
index 679e0d2..fc93ba6 100644
--- a/crates/bsk-cli/src/cli/update.rs
+++ b/crates/bsk-cli/src/cli/update.rs
@@ -19,7 +19,21 @@ pub const DEFAULT_MANIFEST_URL: &str =
"https://github.com/Tencent/BrowserSkill/releases/latest/download/version.json";
const FETCH_TIMEOUT: Duration = Duration::from_secs(10);
const ARCHIVE_FETCH_TIMEOUT: Duration = Duration::from_secs(60);
-const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
+/// How often the daemon ticks the update check, and how long a cache
+/// entry counts as fresh for the CLI hint. The daemon is the only
+/// writer; CLI commands only ever read the cache.
+pub(crate) const UPDATE_CHECK_INTERVAL: Duration = Duration::from_secs(30 * 60);
+
+/// Freshness window the daemon uses to decide whether a tick actually
+/// refreshes the cache. Deliberately shorter than the tick cadence
+/// ([`UPDATE_CHECK_INTERVAL`]): with an equal window, a cache refreshed
+/// just after tick N would still count as fresh at tick N+1 (age just
+/// under 30min), so steady state would only refetch every *other* tick.
+/// At 5/6 of the interval (25min) every 30-minute tick finds the cache
+/// stale and really refreshes it, while a daemon restarted with a
+/// younger-than-25min cache still skips its first tick.
+pub(crate) const DAEMON_REFRESH_WINDOW: Duration =
+ Duration::from_secs(UPDATE_CHECK_INTERVAL.as_secs() * 5 / 6);
#[derive(Debug, Clone)]
pub struct UpdateManifest {
@@ -423,14 +437,30 @@ pub fn write_update_cache(path: &Path, cache: &UpdateCheckCache) -> Result<()> {
Ok(())
}
-fn now_epoch_secs() -> u64 {
+pub(crate) fn now_epoch_secs() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0)
}
-fn refresh_update_cache(cache_path: &Path) -> Result