diff --git a/tests/tui_gateway/test_protocol.py b/tests/tui_gateway/test_protocol.py index c0003dfc818..170e78aec64 100644 --- a/tests/tui_gateway/test_protocol.py +++ b/tests/tui_gateway/test_protocol.py @@ -1736,3 +1736,37 @@ def test_dispatch_unknown_long_method_still_goes_inline(server): resp = server.dispatch({"id": "r4", "method": "some.method", "params": {}}) assert resp["result"] == {"ok": True} + + +@pytest.mark.parametrize("completion_method", ["complete.path", "complete.slash"]) +def test_completion_handlers_are_pool_routed(completion_method, server): + """complete.path/complete.slash must run on the pool, never the reader thread. + + Regression for #21123: completion ran inline, so a slow git ls-files / + skill-scan blocked prompt.submit and froze the TUI for the 120s RPC timeout. + """ + assert completion_method in server._LONG_HANDLERS + + +@pytest.mark.parametrize("completion_method", ["complete.path", "complete.slash"]) +def test_slow_completion_does_not_block_fast_handler(completion_method, server): + """A slow completion RPC must not block a concurrent fast handler (#21123).""" + released = threading.Event() + + def slow_completion(rid, params): + released.wait(timeout=5) + return server._ok(rid, {"items": []}) + + server._methods[completion_method] = slow_completion + server._methods["fast.ping"] = lambda rid, params: server._ok(rid, {"pong": True}) + + t0 = time.monotonic() + assert server.dispatch({"id": "slow", "method": completion_method, "params": {}}) is None + + fast_resp = server.dispatch({"id": "fast", "method": "fast.ping", "params": {}}) + fast_elapsed = time.monotonic() - t0 + + assert fast_resp["result"] == {"pong": True} + assert fast_elapsed < 0.5, f"fast handler blocked for {fast_elapsed:.2f}s behind {completion_method}" + + released.set() diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 2c926b43d18..1bb29397206 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -178,6 +178,16 @@ _LONG_HANDLERS = frozenset( "billing.step_up", "browser.manage", "cli.exec", + # Completion RPCs run inline on the reader thread by default, but both + # can block it for seconds: complete.path spawns `git ls-files` and + # fuzzy-ranks the whole repo (slow on large repos / WSL2 mounts), and + # complete.slash does first-call prompt_toolkit imports + a skill-dir + # scan. While either runs inline, prompt.submit / session.interrupt sit + # unread in the stdin pipe — the TUI appears frozen until the 120s RPC + # timeout fires (#21123). Routing them to the pool keeps the fast path + # responsive; completion is read-only and write_json is lock-guarded. + "complete.path", + "complete.slash", "llm.oneshot", # Pet RPCs hit the network (manifest fetch / spritesheet download) or do # per-frame PNG decode/encode (pet.cells): inline they serialize on the