Skip to content

feat: daemon auto-update support with hourly check loop#777

Merged
svarlamov merged 4 commits intofeat/async-mode-mar-17from
devin/1774314837-daemon-auto-update
Mar 24, 2026
Merged

feat: daemon auto-update support with hourly check loop#777
svarlamov merged 4 commits intofeat/async-mode-mar-17from
devin/1774314837-daemon-auto-update

Conversation

@svarlamov
Copy link
Member

@svarlamov svarlamov commented Mar 24, 2026

Summary

Adds auto-update support to daemon mode. A new background thread wakes hourly to check if a newer version is available. When one is found, the daemon gracefully shuts down, and the installer runs post-shutdown to replace the binary. The next git command auto-starts a fresh daemon from the updated binary (via existing ensure_daemon_running()).

Changes:

  • src/commands/upgrade.rs: Added DaemonUpdateCheckResult enum and two new public functions:
    • check_for_update_available() — queries the releases API + updates cache, no install
    • check_and_install_update_if_available() — reads cached update state, re-fetches release, downloads + runs installer silently. Intentionally bypasses the 24h time guard (reads update_available from cache instead) so the install actually runs after the hourly check already confirmed availability.
    • Both use Config::fresh() (not the OnceLock singleton) so the long-running daemon respects runtime config changes
  • src/daemon.rs:
    • daemon_update_check_loop() — background thread sleeping in 5s ticks (for prompt shutdown), checks hourly
    • daemon_update_check_interval() — reads GIT_AI_DAEMON_UPDATE_CHECK_INTERVAL env var override (clamped to min 1s to prevent spin-loops), defaults to 3600s
    • daemon_run_pending_self_update() — called after clean shutdown to run the installer
    • Spawned as a third thread alongside control and trace listeners in run_daemon()
  • tests/daemon_mode.rs: Three integration tests exercising the full daemon update thread lifecycle:
    • daemon_update_check_loop_detects_cached_update_and_shuts_down — seeds fake update cache, starts daemon with 1s interval, verifies self-shutdown with clean exit, lock release, and socket closure
    • daemon_update_check_loop_respects_disabled_auto_updates — same cache but auto_updates_disabled=true, verifies daemon stays alive
    • daemon_update_check_loop_no_update_stays_alive — no pending update in cache, verifies daemon remains running
  • Unit tests for the new enum and update-check building blocks

Review & Testing Checklist for Human

  • check_for_update_available cache-hit path: When should_check_for_updates returns false (not yet 24h), the function still returns UpdateReady if a previous check cached a pending update. This means the daemon could shut down on a stale cache entry from a prior non-daemon check. Verify this is acceptable.
  • Post-shutdown install re-fetches release from API: check_and_install_update_if_available confirms the update via cache, then makes a network call to get the release tag for the installer. If the API is unreachable at shutdown time, the install silently fails (debug_log only) and the old binary continues. Verify this graceful degradation is acceptable.
  • daemon_run_pending_self_update runs on every shutdown, not just update-triggered ones. Manual git-ai daemon shutdown will also attempt an install if a cached update exists. This seems correct but should be verified as intended.
  • Integration tests don't cover the actual install step: The tests verify the check→shutdown sequence end-to-end, but the post-shutdown check_and_install_update_if_available() call will fail silently (no real release server). The install path is only exercised by the unit tests on its building blocks. Consider whether a mock release server test is needed.
  • GIT_AI_DAEMON_UPDATE_CHECK_INTERVAL env var is not gated behind cfg(test): It's available in production builds to allow external testing. Verify this is acceptable or whether it should be debug-only.

Suggested manual test plan:

  1. Build a debug binary, start the daemon (git-ai daemon run)
  2. Manually write a fake update cache entry with update_available = true and a future version to ~/.git-ai/internal/update_check
  3. Set GIT_AI_DAEMON_UPDATE_CHECK_INTERVAL=10 for a short cycle
  4. Verify the daemon detects the cached update, shuts down, and attempts the install
  5. Verify the next git command starts a new daemon

Notes

  • Stacked on PR Async #743 (feat/async-mode-mar-17).
  • No changes to install.sh or install.ps1 — Unix atomic mv works on running binaries, and Windows install.ps1 already has Wait-ForFileAvailable polling.
  • The hourly wake interval is separate from the 24h API check interval — the thread just evaluates whether the cache has expired.
  • The interval is clamped to max(1) to prevent a CPU spin-loop if the env var is set to 0.

Link to Devin session: https://app.devin.ai/sessions/f511157ae89e4449a7edb023a9c55cf7
Requested by: @svarlamov


Open with Devin

@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration bot and others added 4 commits March 24, 2026 02:29
- Add DaemonUpdateCheckResult enum and two public functions to upgrade.rs:
  - check_for_update_available(): checks if newer version exists (no install)
  - check_and_install_update_if_available(): checks + downloads + runs installer
  Both use Config::fresh() to respect runtime config changes.

- Add daemon_update_check_loop() in daemon.rs: background thread that wakes
  hourly (sleeping in 5s ticks for prompt shutdown response), calls
  check_for_update_available(), and requests graceful shutdown on UpdateReady.

- Add daemon_run_pending_self_update() in daemon.rs: runs after clean shutdown
  to fetch and install the update. Unix: atomic mv replaces binary. Windows:
  detached PowerShell installer polls until exe is unlocked.

- Spawn update check thread alongside control and trace listener threads in
  run_daemon().

- Add unit tests for DaemonUpdateCheckResult and update check building blocks.

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
The post-shutdown install function was re-checking the 24h time guard
via should_check_for_updates(), which would always return false because
check_for_update_available() had just updated last_checked_at. Now it
reads the cached update_available flag directly, bypassing the time
guard. Also clears the cache after successful install to prevent
redundant re-installs.

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
Three integration tests verify the daemon auto-update thread behavior:

1. daemon_update_check_loop_detects_cached_update_and_shuts_down:
   Seeds a fake update cache with a pending update, starts the daemon
   with a short check interval (1s via GIT_AI_DAEMON_UPDATE_CHECK_INTERVAL
   env var), and verifies the daemon self-shuts-down after detecting
   the cached update. Asserts clean exit, lock release, and socket closure.

2. daemon_update_check_loop_respects_disabled_auto_updates:
   Same cached update but with disable_auto_updates=true via config patch.
   Verifies the daemon stays alive through multiple check cycles.

3. daemon_update_check_loop_no_update_stays_alive:
   Seeds cache with no pending update, verifies daemon remains running.

Also adds GIT_AI_DAEMON_UPDATE_CHECK_INTERVAL env var override to
daemon_update_check_interval() so integration tests can use a short
interval without waiting the production 1-hour cycle.

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
Prevents a CPU spin-loop when GIT_AI_DAEMON_UPDATE_CHECK_INTERVAL is
set to 0. Without the clamp, both interval and tick become 0, causing
the inner while-loop to skip immediately and the outer loop to call
check_for_update_available() in a tight busy-loop.

Co-Authored-By: Sasha Varlamov <sasha@sashavarlamov.com>
@svarlamov svarlamov force-pushed the devin/1774314837-daemon-auto-update branch from a66d7db to 2407968 Compare March 24, 2026 02:35
@svarlamov svarlamov merged commit a9064c8 into feat/async-mode-mar-17 Mar 24, 2026
5 of 9 checks passed
@svarlamov svarlamov deleted the devin/1774314837-daemon-auto-update branch March 24, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants