diff --git a/src/basic_memory/cli/analytics.py b/src/basic_memory/cli/analytics.py index 4d3d88e57..4a7f4b707 100644 --- a/src/basic_memory/cli/analytics.py +++ b/src/basic_memory/cli/analytics.py @@ -108,7 +108,7 @@ def _send(): except Exception: pass # Never break the CLI for analytics - # Non-daemon so the process waits for the request to complete. - # The 3s urllib timeout caps the worst-case exit delay. - t = threading.Thread(target=_send) + # Analytics must never keep a one-shot CLI process alive during interpreter + # shutdown. The request is best-effort and already has a bounded timeout. + t = threading.Thread(target=_send, daemon=True) t.start() diff --git a/tests/cli/test_analytics.py b/tests/cli/test_analytics.py index 056bb6f00..30f707a3e 100644 --- a/tests/cli/test_analytics.py +++ b/tests/cli/test_analytics.py @@ -68,6 +68,7 @@ def test_sends_when_using_defaults(self, monkeypatch): mock_thread.return_value = MagicMock() track("test-event") mock_thread.assert_called_once() + assert mock_thread.call_args.kwargs["daemon"] is True def test_sends_event_when_configured(self, monkeypatch): monkeypatch.delenv("BASIC_MEMORY_NO_PROMOS", raising=False) @@ -76,7 +77,7 @@ def test_sends_event_when_configured(self, monkeypatch): captured_target = None - def fake_thread(target): + def fake_thread(target, **kwargs): nonlocal captured_target captured_target = target mock = MagicMock() @@ -103,7 +104,7 @@ def fake_urlopen(req, timeout=None): with patch("basic_memory.cli.analytics.urllib.request.urlopen", fake_urlopen): with patch("basic_memory.cli.analytics.threading.Thread") as mock_thread: # Capture the target function and call it directly - def run_target(target): + def run_target(target, **kwargs): target() # Execute synchronously return MagicMock() @@ -130,7 +131,7 @@ def fake_urlopen(req, timeout=None): with patch("basic_memory.cli.analytics.urllib.request.urlopen", fake_urlopen): with patch("basic_memory.cli.analytics.threading.Thread") as mock_thread: - def run_target(target): + def run_target(target, **kwargs): target() # Should not raise return MagicMock()