# Model-free check: does thread/unsubscribe (what T3 sends on settle) stop the thread's MCP servers?
# usage: driver.py <outdir> <seconds_to_watch> [extra codex app-server args...]
import json, os, subprocess, sys, time, threading
out, watch, extra = sys.argv[1], float(sys.argv[2]), sys.argv[3:]
os.makedirs(out, exist_ok=True)
home = os.path.join(out, "codex-home"); os.makedirs(home, exist_ok=True)
stub = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub_mcp.py")
stublog = os.path.join(out, "stub.log")
with open(os.path.join(home, "config.toml"), "w") as f:
    f.write(f'[mcp_servers.stub]\ncommand = "python3"\nargs = ["-I", "{stub}"]\nenv = {{ STUB_LOG = "{stublog}" }}\n')
t0 = time.time()
def log(msg):
    line = f"+{time.time()-t0:7.1f}s {msg}"
    print(line, flush=True)
    with open(os.path.join(out, "driver.log"), "a") as f: f.write(line + "\n")
env = dict(os.environ, CODEX_HOME=home)
p = subprocess.Popen(["codex", "app-server", *extra], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=open(os.path.join(out, "app-server.stderr"), "w"), env=env, text=True, cwd=out)
log(f"spawned codex app-server pid={p.pid} args={extra}")
pending, nid = {}, [0]
def reader():
    for line in p.stdout:
        msg = json.loads(line)
        if "id" in msg and msg["id"] in pending: pending[msg["id"]] = msg
        elif "method" in msg:
            if msg["method"] in ("thread/closed", "thread/started", "thread/status/changed"): log(f"notification {msg['method']} {json.dumps(msg.get('params'))[:160]}")
threading.Thread(target=reader, daemon=True).start()
def req(method, params):
    nid[0] += 1; i = nid[0]; pending[i] = None
    p.stdin.write(json.dumps({"id": i, "method": method, "params": params}) + "\n"); p.stdin.flush()
    while pending[i] is None: time.sleep(0.05)
    r = pending.pop(i); log(f"{method} -> {json.dumps(r.get('result', r.get('error')))[:200]}"); return r
def stub_children():
    r = subprocess.run(["pgrep", "-f", "-P", str(p.pid), "stub_mcp.py"], capture_output=True, text=True)
    # pgrep -P lists direct children; stub may be a grandchild, so walk the tree via ps
    ps = subprocess.run(["ps", "-eo", "pid,ppid,args"], capture_output=True, text=True).stdout.splitlines()[1:]
    kids = {}
    for l in ps:
        pid, ppid, args = l.split(None, 2)
        kids.setdefault(int(ppid), []).append((int(pid), args))
    found, stack = [], [p.pid]
    while stack:
        for pid, args in kids.get(stack.pop(), []):
            stack.append(pid)
            if "stub_mcp.py" in args and "pgrep" not in args: found.append(pid)
    return found
req("initialize", {"clientInfo": {"name": "repro", "title": "repro", "version": "0"}})
p.stdin.write(json.dumps({"method": "initialized"}) + "\n"); p.stdin.flush()
r = req("thread/start", {"cwd": out})
tid = r["result"]["thread"]["id"]
time.sleep(3)
log(f"stub MCP processes under app-server after thread/start: {stub_children()}")
req("thread/unsubscribe", {"threadId": tid})
end = time.time() + watch
last = None
while time.time() < end:
    cur = stub_children()
    if cur != last: log(f"stub MCP processes under app-server: {cur}"); last = cur
    time.sleep(1)
log(f"end of watch; app-server alive={p.poll() is None}; stub processes {stub_children()}")
p.terminate(); p.wait(10)
