fix: serialize on-demand slash-worker spawn per session

With the eager pre-warm removed (PR #66783), slash.exec is the only spawn
path — and it runs on the RPC thread pool, so two concurrent worker-routed
commands on a fresh session could both see slash_worker=None and each fork
a full stdio-MCP-fleet worker (the _attach_worker race loser leaking
unclosed). Add a per-session spawn lock with a double-check, plus a
regression test racing two slash.exec calls through handle_request.

Also maps Ne0teric's contributor email.
This commit is contained in:
teknium1 2026-07-24 12:06:11 -07:00 committed by Teknium
parent 5aa3536b32
commit 3b762ba695
3 changed files with 75 additions and 9 deletions

View file

@ -0,0 +1 @@
Ne0teric

View file

@ -12045,6 +12045,60 @@ def test_restart_slash_worker_noop_without_worker(monkeypatch):
server._sessions.pop("lazy-noop", None)
def test_slash_exec_concurrent_first_use_spawns_single_worker(monkeypatch):
"""With eager pre-warm removed, slash.exec is the only spawn path — two
concurrent worker-routed commands on a fresh session must not each fork a
full MCP-fleet worker. The per-session spawn lock serializes first use."""
import time as _time
spawned = []
barrier = threading.Barrier(2, timeout=5)
class _SlowWorker:
def __init__(self, *a, **k):
spawned.append(self)
_time.sleep(0.05) # widen the None-observation window
def run(self, cmd):
return f"ran {cmd}"
def close(self):
pass
monkeypatch.setattr(server, "_SlashWorker", _SlowWorker)
monkeypatch.setattr(server, "_mirror_slash_side_effects", lambda *a, **k: None)
session = _session(slash_worker=None)
server._sessions["race-spawn"] = session
results = []
def _exec(n):
barrier.wait()
resp = server.handle_request(
{
"id": str(n),
"method": "slash.exec",
"params": {"command": "/context", "session_id": "race-spawn"},
}
)
results.append(resp)
try:
threads = [threading.Thread(target=_exec, args=(i,)) for i in range(2)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=10)
assert len(spawned) == 1, (
f"concurrent slash.exec spawned {len(spawned)} workers — first-use "
f"spawn must be serialized per session"
)
assert session["slash_worker"] is spawned[0]
assert all("result" in r for r in results), results
finally:
server._sessions.pop("race-spawn", None)
def test_session_close_rpc_claims_then_tears_down(monkeypatch):
seen = []
claimed = {"session_key": "k"}

View file

@ -16064,15 +16064,26 @@ def _(rid, params: dict) -> dict:
worker = session.get("slash_worker")
if not worker:
try:
worker = _SlashWorker(
session["session_key"],
getattr(session.get("agent"), "model", _resolve_model()),
profile_home=session.get("profile_home"),
)
_attach_worker(params.get("session_id", ""), session, worker)
except Exception as e:
return _err(rid, 5030, f"slash worker start failed: {e}")
# On-demand spawn is now the ONLY spawn path for a fresh session
# (eager pre-warm removed), and slash.exec handlers run on the RPC
# thread pool — two concurrent slash commands on the same session
# could both observe slash_worker=None and each fork a full
# MCP-fleet worker (the loser of the _attach_worker race would leak
# unclosed). Serialize first-use spawn per session.
with _sessions_lock:
spawn_lock = session.setdefault("_slash_spawn_lock", threading.Lock())
with spawn_lock:
worker = session.get("slash_worker")
if not worker:
try:
worker = _SlashWorker(
session["session_key"],
getattr(session.get("agent"), "model", _resolve_model()),
profile_home=session.get("profile_home"),
)
_attach_worker(params.get("session_id", ""), session, worker)
except Exception as e:
return _err(rid, 5030, f"slash worker start failed: {e}")
try:
output = worker.run(cmd)