From a13db76eaa65714a0625bb980f730910c2ca8e80 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 2 Jun 2026 18:28:24 -0500 Subject: [PATCH] fix(desktop): signal loopback worker to stop on cancel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shutting down the callback server stopped the serve thread but left the worker spinning in _xai_wait_for_callback (which polls callback_result) until the timeout. Flag callback_result as cancelled on DELETE so the wait returns promptly and the daemon thread exits — avoids thread buildup on repeated cancel/retry. --- hermes_cli/web_server.py | 9 +++++++++ tests/hermes_cli/test_web_oauth_dispatch.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 59b2042d4..baf1dbcc3 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -4088,6 +4088,15 @@ async def cancel_oauth_session(session_id: str, request: Request): # _xai_wait_for_callback times out (up to 5 min). Free it immediately so # an orphaned listener can't block a subsequent sign-in attempt. if sess.get("flow") == "loopback": + # The worker is blocked in _xai_wait_for_callback, which polls + # callback_result rather than the server state. Flag the result as + # cancelled so that loop returns on its next tick instead of spinning + # until the timeout — otherwise repeated cancel/retry piles up daemon + # threads. (_cancelled() in the worker then short-circuits before any + # persist.) + result = sess.get("callback_result") + if isinstance(result, dict): + result["error"] = result.get("error") or "cancelled" server = sess.get("server") thread = sess.get("thread") try: diff --git a/tests/hermes_cli/test_web_oauth_dispatch.py b/tests/hermes_cli/test_web_oauth_dispatch.py index 68e9175d7..4200363ba 100644 --- a/tests/hermes_cli/test_web_oauth_dispatch.py +++ b/tests/hermes_cli/test_web_oauth_dispatch.py @@ -550,6 +550,8 @@ def test_cancel_loopback_session_shuts_down_callback_server(): def join(self, timeout=None): shutdown_calls["join"] += 1 + # callback_result is the dict the worker's _xai_wait_for_callback polls. + callback_result = {"code": None, "error": None} session_id = "xai-loopback-cancel-shutdown-test" ws._oauth_sessions[session_id] = { "session_id": session_id, @@ -559,6 +561,7 @@ def test_cancel_loopback_session_shuts_down_callback_server(): "status": "pending", "server": _FakeServer(), "thread": _FakeThread(), + "callback_result": callback_result, } try: @@ -568,6 +571,9 @@ def test_cancel_loopback_session_shuts_down_callback_server(): assert resp.status_code == 200, resp.text assert resp.json()["ok"] is True assert shutdown_calls == {"shutdown": 1, "close": 1, "join": 1} + # The waiting worker must be signalled so it returns promptly instead + # of spinning until the timeout. + assert callback_result["error"] == "cancelled" assert session_id not in ws._oauth_sessions finally: ws._oauth_sessions.pop(session_id, None)