From 2354ce2c2c07ea64fefa1dea1a510c2ac64db678 Mon Sep 17 00:00:00 2001 From: Julian lechner Date: Thu, 1 Oct 2026 13:13:03 +0200 Subject: [PATCH] fix(retrieval): version the daemon by code hash Derive the daemon address and start lock from a hash of server.py, so an updated server never reuses a daemon still running older code. Co-Authored-By: Claude Opus 5.5 --- scripts/lib/semantic-retrieval.py | 2 +- scripts/tests/test-retrieval-server.py | 9 +++++++++ shared/retrieval/server.py | 10 ++++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/lib/semantic-retrieval.py b/scripts/lib/semantic-retrieval.py index 75829fc..6e5ef8e 100644 --- a/scripts/lib/semantic-retrieval.py +++ b/scripts/lib/semantic-retrieval.py @@ -97,7 +97,7 @@ def check_runtime(target, state): if path.name == 'data' and (not path.is_dir() or any( child.is_symlink() or not ( child.is_file() and (child.name in ('daemon.key', 'daemon.starting') or re.fullmatch( - r'[0-9a-f]{64}\.sqlite3(?:-(?:wal|shm|journal))?', child.name)) + r'[0-9a-f]{64}\.sqlite3(?:-(?:wal|shm|journal))?|daemon-[0-9a-f]{12}\.starting', child.name)) or child.is_socket() and re.fullmatch(r'daemon-[0-9a-f]{16}\.sock', child.name)) for child in path.iterdir())): raise ValueError('Foreign or linked semantic retrieval path: data') diff --git a/scripts/tests/test-retrieval-server.py b/scripts/tests/test-retrieval-server.py index 07618c6..9194b0e 100644 --- a/scripts/tests/test-retrieval-server.py +++ b/scripts/tests/test-retrieval-server.py @@ -124,6 +124,15 @@ class RetrievalServerTests(unittest.TestCase): self.assertEqual(stopped.exception.code, 3) self.assertEqual(calls[0][2:], ['search', '--query', 'two words']) + def test_daemon_identity_changes_with_code_version(self): + with TemporaryDirectory() as temporary: + data_dir = Path(temporary) + with patch.object(MODULE, 'code_version', return_value='aaaaaaaaaaaa'): + old_address, old_lock = MODULE.daemon_address(data_dir), MODULE.daemon_lock_path(data_dir) + with patch.object(MODULE, 'code_version', return_value='bbbbbbbbbbbb'): + self.assertNotEqual(MODULE.daemon_address(data_dir), old_address) + self.assertNotEqual(MODULE.daemon_lock_path(data_dir), old_lock) + def test_preferred_device_uses_cuda_with_cpu_fallback(self): with patch.dict('sys.modules', torch=SimpleNamespace(cuda=SimpleNamespace(is_available=lambda: True))): self.assertEqual(MODULE.preferred_device(), 'cuda') diff --git a/shared/retrieval/server.py b/shared/retrieval/server.py index cfa2abd..f3038c4 100644 --- a/shared/retrieval/server.py +++ b/shared/retrieval/server.py @@ -226,12 +226,18 @@ def reciprocal_rank_fusion(*rankings): return sorted(scores, key=lambda identifier: (-scores[identifier], identifier)) +def code_version() -> str: + """Hash of this file, so a daemon started by older code is never reused after an update.""" + return hashlib.sha256(Path(__file__).read_bytes()).hexdigest()[:12] + + def daemon_lock_path(data_dir: Path) -> Path: - return data_dir / "daemon.starting" + return data_dir / f"daemon-{code_version()}.starting" def daemon_address(data_dir: Path) -> str: - digest = hashlib.sha256(os.path.normcase(str(data_dir.resolve())).encode()).hexdigest()[:16] + identity = os.path.normcase(str(data_dir.resolve())) + "|" + code_version() + digest = hashlib.sha256(identity.encode()).hexdigest()[:16] if os.name == "nt": return r"\\.\pipe\ai-config-retrieval-" + digest return str(data_dir / f"daemon-{digest}.sock")