Skip to content

Commit 1d53ef8

Browse files
nickrociclaude
andcommitted
fix(daemon): restart legacy (no-version-stamp) daemons too
Anchor the stale check on the INSTALLED version, not on both sides being present. A running daemon with no recorded version is a legacy process from before the stamp existed — exactly the already-running daemon a user has when they first adopt this feature — so it must restart, not be left alone. Still fail-safe on the installed side: an unreadable installed version (thin/uvx env) never triggers a restart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3884635 commit 1d53ef8

2 files changed

Lines changed: 23 additions & 8 deletions

File tree

tests/test_daemon_restart.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,19 @@ def test_no_restart_when_installed_version_unknown(
6464
assert stopped == []
6565

6666

67-
def test_no_restart_when_running_version_missing(
67+
def test_restart_legacy_daemon_with_no_version_stamp(
6868
home: Path, monkeypatch: pytest.MonkeyPatch
6969
) -> None:
70-
# A legacy daemon (pre-this-feature) wrote no version — never restart blindly.
70+
# A legacy daemon (pre-this-feature) wrote no version. Anchored on a KNOWN
71+
# installed version, that's unambiguously old code → restart. This is the
72+
# migration case: the daemon a user already has running when they first adopt
73+
# the feature has no stamp, and must still be replaced.
7174
_write_state(home, pid=os.getpid(), version=None)
7275
monkeypatch.setattr(_daemon, "_installed_daemon_version", lambda: "0.3.0")
73-
assert _daemon.restart_if_stale() is False
76+
stopped: list[int] = []
77+
monkeypatch.setattr(_daemon, "_stop_daemon", lambda pid, **k: stopped.append(pid))
78+
assert _daemon.restart_if_stale() is True
79+
assert stopped == [os.getpid()]
7480

7581

7682
def test_restart_when_stale_stops_and_clears_backoff(

ultan/_daemon.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,18 +153,27 @@ def restart_if_stale() -> bool:
153153
metadata (cold-cheap, hot-needless). The running version is what the daemon
154154
stamped into ``daemon.state`` at ITS startup; the installed version is read
155155
live, so an ``uv tool install`` update is detected even though the old
156-
process keeps serving the previous code. Fail-safe: any uncertainty (no
157-
daemon, dead pid, unknown version on either side) leaves the daemon alone."""
156+
process keeps serving the previous code.
157+
158+
Anchored on the INSTALLED version: if we can't read it (a thin/uvx env with
159+
no daemon dist, or unreadable metadata) we never restart — there's nothing to
160+
compare against. Given a readable installed version, a daemon whose recorded
161+
version differs is stale; so is one with NO recorded version, because that is
162+
a legacy daemon from before this stamp existed (a current daemon always
163+
stamps a version when the metadata it reads is itself readable). Restarting
164+
that legacy daemon is the whole point — it's exactly the process left on old
165+
code that this feature exists to replace."""
158166
state = _read_daemon_state()
159167
if not state:
160168
return False
161169
pid = state.get("pid")
162170
if not _pid_alive(pid):
163171
return False
164-
running = state.get("version")
165172
installed = _installed_daemon_version()
166-
if not running or not installed or running == installed:
167-
return False
173+
if not installed:
174+
return False # can't tell what's installed — never restart blindly
175+
if state.get("version") == installed:
176+
return False # up to date
168177
_stop_daemon(cast("int", pid))
169178
# Clear the spawn backoff so ensure_running() respawns immediately rather
170179
# than throttling this intentional restart as if it were a crash loop.

0 commit comments

Comments
 (0)